Background
Take Lombok as an example: at compile time, it reads annotations like @Getter and @Setter through an Annotation Processor (APT, Annotation Processing Tool), then directly modifies the AST (Abstract Syntax Tree) in memory, injecting getter/setter methods for fields. Developers only need to annotate fields, and the compiled .class files automatically include these methods.
Advantages of APT
APT is an extension mechanism natively supported by the Java compiler. Annotation processors run at compile time and can read annotation information from source code to generate new source files. Its advantages are:
-
Standardized — Simply implement the
Processorinterface; no need to hack the compiler - Composable — Multiple annotation processors can work together in the same compilation process
-
Generated code is visible — Source files generated through the
FilerAPI are located in thebuild/generated/directory, and IDEs can navigate to them after configuring the source path
Disadvantages of APT
APT has a fundamental limitation: it cannot generate a class with the same fully qualified name as the source code. The Java compiler explicitly specifies that two classes with the same fully qualified name are not allowed in the same compilation unit. Source code generated by APT participates in the same round of compilation as the original source code, so it can never generate a class with the same fully qualified name as the original class. This means APT cannot truly "modify" a class; it can only generate new classes (such as Builder, Factory, etc.).
Advantages of AST
Tools like Lombok bypass the APT limitation by directly modifying the AST in the compiler's memory — injecting methods for fields, injecting constructors for classes, etc. The advantages of AST modification are:
- Can modify existing classes — Methods are added directly to the AST in memory, breaking through the limitation that APT cannot generate Same-Name Classes
- Zero intrusion — Classes written by developers remain concise; all enhancements are completed at compile time
Disadvantages of AST
The cost of AST modification is: the enhanced classes are hidden and cannot be debugged.
- Unobservable — The modified content exists only in memory during compilation and is not output as source files. Developers always see the original class without the generated methods. Lombok writes dedicated plugins for major IDEs to make the IDE "pretend" to see the generated methods — but this is essentially a hack on the IDE.
- Undebuggable — The enhanced code does not exist in any source file. Developers cannot set breakpoints on generated methods or step through them.
-
Unreviewable — If you need to know what the final compiled class looks like, you can only view it by decompiling the
.classfile.
Combining the Strengths
APT can generate source files (observable, debuggable) but cannot generate Same-Name Classes (cannot modify existing classes); AST can modify existing classes (breaking through the Same-Name Class limitation) but the modification results are unobservable and undebuggable. If we combine the two — use AST to modify classes, then output the modified AST as source files — we can gain the advantages of both: the ability to modify existing classes, with enhancement results that are observable and debuggable.
This is the core idea of Source View Technology. In the same project, the classes written by developers and the Same-Name View Classes generated after AST enhancement coexist. At compile time, View Classes replace classes, and the final artifact contains only the enhanced version. Since View Classes are real source files, developers can directly open, read, and debug them.
Source View Technology is implemented through the design of the source-view project, which needs to solve two problems:
- Running of Same-Name Classes — In the same Java project, let classes and View Classes coexist, with View Classes replacing classes at compile time
-
Unified Processing via Compiler Plugin — Through a Compiler Plugin, unify the output of AST-enhanced classes from various Annotation Processors as View Class source code, generated near the
mainmodule for easy reading and debugging
Running of Same-Name Classes
The first problem Source View Technology must solve is: how to let classes and View Classes coexist in the same project, and ensure that View Classes replace classes at compile time.
Project Structure
The initial approach does not rely on Annotation Processors; View Classes are written manually by developers:
source-view/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
├── main/java/com/example/ # Classes written by developers
│ ├── App.java # Application entry point
│ ├── Student.java # Only fields + annotation declarations
│ ├── Getter.java # @Getter annotation
│ └── Setter.java # @Setter annotation
└── generated/java/com/example/ # View Classes
└── Student.java # Contains complete getter/setter implementation
src/main/java contains classes written by developers, and src/generated/java contains View Classes with the same fully qualified name. At compile time, View Classes replace classes.
Classes and View Classes
Student.java — Developers only need to write field declarations and annotations, without manually writing getter/setter:
package com.example;
public class Student {
@Getter
@Setter
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
}
View Class Student.java — Same fully qualified name com.example.Student, containing complete method implementations. In the initial approach, this is written manually by developers:
package com.example;
public class Student {
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
The View Class is a regular Java class that can be edited, debugged, and refactored normally in an IDE.
Application entry point — App.java directly calls setName() and getName(), as if these methods were originally on Student:
package com.example;
public class App {
public static void main(String[] args) {
Student student = new Student();
student.setName("Tom");
System.out.println(student.getName());
}
}
Evolution of Build Logic
The core challenge of Source View Technology lies in Build Logic: how to let classes and View Classes coexist in the same project, and ensure that View Classes replace classes at compile time? This section starts from a naive approach, analyzes its problems, and gradually evolves to the final solution.
Naive Approach: main depends on generated
The most intuitive approach is to set up src/main/java and src/generated/java as two separate SourceSets — main and generated — with main depending on the compilation output of generated:
sourceSets {
create("generated") {
java { srcDir("src/generated/java") }
}
}
dependencies {
"implementation"(sourceSets["generated"].output)
}
tasks.named<JavaCompile>("compileJava") {
dependsOn("compileGeneratedJava")
exclude { }
}
tasks.named<Jar>("jar") {
from(sourceSets["main"].output)
from(sourceSets["generated"].output)
}
The compilation flow is three steps: first compile generated, then compile main after excluding same-name classes, and finally merge the outputs of both modules into the package. This approach works without Annotation Processors, but has two fundamental problems.
Problem 1: Annotation Processor Introduces Circular Dependency
Currently, View Classes are written manually, so the naive approach still works. But once an Annotation Processor is introduced — which reads @Getter and @Setter annotations from the main module and automatically generates View Class source code to src/generated/java — a circular dependency becomes inevitable:
main module ──depends on──▶ generated module
▲ │
│ │
└── Annotation Processor reads annotations ─┘
Before compiling a module, the build tool first compiles its dependencies. Therefore, to compile main, generated must be compiled first, but the source code of generated needs the Annotation Processor to read annotations from main before it can be generated — main depends on generated, and generated depends on main. The build tool cannot determine the compilation order.
Problem 2: IDE Compilation Error Prompts
Whether or not an Annotation Processor is introduced, the IDE will report errors. When the IDE resolves the main module, the Student class it sees has only field declarations, without getName()/setName() methods. Therefore, student.getName() in App.java will be marked as a compilation error by the IDE, even though these calls are valid at build time (because at build time, main's classpath includes the View Classes from generated).
Final Approach: Remove main's Dependency on generated
The root cause of both problems is the same: main depends on generated. Remove this dependency, and the circular dependency is naturally eliminated. But the question is: after removing the dependency, how can App.java resolve Student's methods? The answer is: instead of depending on generated's compilation output, directly include generated's source code in main's compilation scope.
sourceSets {
create("generated") {
java { srcDir("src/generated/java") }
resources { srcDir("src/generated/resources") }
}
}
tasks.named<JavaCompile>("compileJava") {
val generatedJavaDir = file("src/generated/java")
val generatedRelativePaths = generatedJavaDir.walkTopDown()
.filter { it.isFile && it.name.endsWith(".java") }
.map { it.relativeTo(generatedJavaDir).path.replace('\\', '/') }
.toSet()
val mainJavaDir = file("src/main/java")
exclude { element ->
val file = element.file
file.absolutePath.startsWith(mainJavaDir.absolutePath) &&
generatedRelativePaths.contains(file.relativeTo(mainJavaDir).path.replace('\\', '/'))
}
}
tasks.named<JavaCompile>("compileGeneratedJava") {
enabled = false
}
tasks.named<Jar>("jar") {
from(sourceSets["main"].output)
}
Key design decisions:
-
maindoes not depend ongenerated— Nodependenciesblock, completely eliminating the possibility of circular dependencies. -
generatedsource code included inmaincompilation — ThroughsrcDir("src/generated/java"), the View Class source code is added tomain's compilation source directories.maincompiles all source code at once, soApp.javacan naturally resolveStudent's methods. -
Only exclude same-name classes in
src/main/java— Theexcludechecks whether a file is located in thesrc/main/javadirectory, ensuring only the developer-written duplicate classes are excluded while the View Class version participates in compilation normally. -
Disable
compileGeneratedJava—generatedis no longer compiled separately; nobuild/classes/java/generated/directory is produced. All.classfiles are output uniformly tobuild/classes/java/main/. -
Jar packages only from
main— The final artifact contains only themainmodule's output, with no conflict between two Same-Name Classes.
The compilation flow is simplified from three steps in the naive approach to a single step:
┌──────────────────────────────────────────────────────┐
│ compileJava │
│ │
│ Source scope: │
│ src/main/java ──exclude same-name──▶ App.java │
│ │ Getter.java │
│ │ Setter.java │
│ │ (Student.java excluded) │
│ │ │
│ src/generated/java ───────────────▶ Student.java │
│ │
│ Compilation output: │
│ build/classes/java/main/ │
│ ├── App.class │
│ ├── Getter.class │
│ ├── Setter.class │
│ └── Student.class (from View Class) │
└──────────────────────────────────────────────────────┘
Resolving IDE Compilation Error Prompts
The above approach solves the build-level problem, but IDE compilation error prompts still exist — when the IDE resolves Student in src/main/java, it sees the developer-written class without getter/setter methods. This needs to be resolved by writing an IDE plugin: the plugin recognizes @Getter, @Setter, and other annotations in the class, suppresses compilation error prompts for corresponding method calls, and lets the IDE know that these methods will be provided by View Classes at compile time.
Unified Processing via Compiler Plugin
The running of Same-Name Classes solves the problem of "how View Classes replace classes," but View Classes still need to be written manually by developers. If View Classes could be automatically generated by an Annotation Processor, the development experience would be further improved. This requires introducing a Compiler Plugin — to unify the output of AST-enhanced classes from various Annotation Processors as View Class source code, transforming the enhanced classes from "hidden in-memory state" to "observable source files."
Design Philosophy
Source View Technology uses a Compiler Plugin to achieve source code output of AST enhancement results. A Compiler Plugin is an extension mechanism provided by javac, loaded via the -Xplugin parameter, which can insert custom logic at various stages of compilation.
Design approach:
-
Annotation Processor is responsible for modifying the AST — This is what existing AST class enhancement technologies already do; no change is needed.
LombokProcessorreads@Getter/@Setterannotations and generates getter/setter methods for fields in memory throughLombokTreeTranslator. -
Compiler Plugin is responsible for outputting source code — This is the new capability added by Source View Technology.
GeneratedSourcePlugin, as a Compiler Plugin, listens to compilation events and outputs the modified AST as View Class source code to thesrc/generated/javadirectory after annotation processing is complete. -
Source code output is near the main module — View Class source code is generated in the
src/generated/javadirectory, alongsidesrc/main/java, rather than hidden in thebuild/directory. Developers can directly open, read, and debug these files in the IDE.
Project Structure
After introducing the Compiler Plugin, the project structure adds a core module containing annotation definitions, the Annotation Processor, and the Compiler Plugin:
source-view/
├── build.gradle.kts # Root project build logic
├── settings.gradle.kts
├── core/ # Annotation Processor module
│ ├── build.gradle.kts
│ └── src/main/java/mock/lombok/
│ ├── annotation/
│ │ ├── Getter.java # @Getter annotation
│ │ └── Setter.java # @Setter annotation
│ └── processor/
│ ├── LombokProcessor.java # Annotation Processor: reads annotations, modifies AST
│ ├── LombokTreeTranslator.java # AST transformer: generates getter/setter for fields
│ └── GeneratedSourcePlugin.java # Compiler Plugin: outputs enhanced AST as View Class source
└── src/
├── main/java/com/example/ # Classes written by developers
│ ├── App.java # Application entry point
│ └── Student.java # Only fields + annotation declarations
└── generated/java/com/example/ # View Classes (automatically generated by Compiler Plugin)
├── App.java
└── Student.java # Contains complete getter/setter implementation
The core module contains annotation definitions and the Annotation Processor, and is the core of the collaboration between Source View Technology and the Compiler Plugin. View Classes no longer need to be written manually; they are automatically generated by the Compiler Plugin.
Upgrade of Build Logic
After introducing the Annotation Processor, the simple build approach from the Same-Name Class running section faces the circular dependency problem (see "Problem 1: Annotation Processor Introduces Circular Dependency"). The solution is to split compilation into two phases:
-
Annotation processing phase (
processAnnotationtask): Uses-proc:onlyto run only the Annotation Processor, combined with-Xpluginto run the Compiler Plugin, outputting the enhanced AST as View Class source code tosrc/generated/java -
Compilation phase (
compileJavatask): Uses-proc:noneto skip annotation processing, includesgeneratedsource code inmain's compilation scope, and excludes developer-written Same-Name Classes
sourceSets {
create("generated") {
java { srcDir("src/generated/java") }
resources { srcDir("src/generated/resources") }
}
}
dependencies {
implementation(project(":core"))
annotationProcessor(project(":core"))
}
tasks.register<JavaCompile>("processAnnotation") {
options.encoding = "UTF-8"
options.compilerArgs.add("-proc:only")
options.compilerArgs.add("-nowarn")
options.compilerArgs.add("-Xplugin:mock.lombok.processor.GeneratedSourcePlugin " +
"-mainJavaPath=${sourceSets.main.get().java.sourceDirectories.asPath} " +
"-generatedJavaPath=${sourceSets["generated"].java.sourceDirectories.asPath}")
source = sourceSets.main.get().java
options.sourcepath = sourceSets.main.get().java
classpath = tasks.compileJava.get().classpath
options.annotationProcessorPath = tasks.compileJava.get().options.annotationProcessorPath
destinationDirectory.set(sourceSets.main.get().java.outputDir)
}
tasks.compileJava {
dependsOn("processAnnotation")
options.compilerArgs.add("-proc:none")
options.encoding = "UTF-8"
doFirst {
val generatedSrcDirs = sourceSets["generated"].java.sourceDirectories
val mainSrcDirs = sourceSets.main.get().java.sourceDirectories
val generatedSrcDirPaths = generatedSrcDirs.asFileTree
.filter { it.isFile && it.name.endsWith(".java") }
.map { it.toRelativeString(generatedSrcDirs.singleFile) }
val filterMainSrcDirs = mainSrcDirs.asFileTree
.filter {
it.isFile && it.name.endsWith(".java")
&& !generatedSrcDirPaths.contains(it.toRelativeString(mainSrcDirs.singleFile))
}
source = (generatedSrcDirs + filterMainSrcDirs).asFileTree
options.sourcepath = generatedSrcDirs + filterMainSrcDirs
}
}
tasks.named<JavaCompile>("compileGeneratedJava") {
enabled = false
}
tasks.named<Jar>("jar") {
from(sourceSets["main"].output)
}
tasks.named<Delete>("clean") {
delete(fileTree(sourceSets["generated"].java.sourceDirectories.asPath))
}
Key changes:
-
Two-phase compilation —
processAnnotationonly runs the Annotation Processor and Compiler Plugin without compiling code;compileJavaonly compiles code without running the Annotation Processor. Their responsibilities are separated and do not interfere with each other. -
Introduced
coremodule dependency —implementation(project(":core"))provides annotation definitions,annotationProcessor(project(":core"))provides the Annotation Processor. -
cleanclears the generated directory — Deletes auto-generated files undersrc/generated/java, ensuring the Annotation Processor regenerates them on the next build.
The compilation flow changes from a single step in the Same-Name Class running approach to two steps:
┌─────────────────────────────────────────────────────────────┐
│ Phase 1: processAnnotation │
│ │
│ Read annotations in classes │
│ LombokProcessor modifies AST (adds getter/setter) │
│ GeneratedSourcePlugin outputs enhanced AST as View Class │
│ source code │
│ │
│ -proc:only -Xplugin:GeneratedSourcePlugin │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Phase 2: compileJava │
│ │
│ Source scope: │
│ src/main/java ──exclude same-name──▶ App.java │
│ │ (Student.java excluded) │
│ │
│ src/generated/java ───────────────▶ Student.java (View)│
│ App.java │
│ │
│ Compilation output: │
│ build/classes/java/main/ │
│ ├── App.class │
│ └── Student.class (from View Class) │
│ │
│ -proc:none │
└─────────────────────────────────────────────────────────────┘
Source Code Implementation
After introducing the Compiler Plugin, the developer-written Student.java uses annotations from the core module:
package com.example;
import mock.lombok.annotation.Getter;
import mock.lombok.annotation.Setter;
public class Student {
@Getter
@Setter
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
}
The View Class Student.java is automatically generated by the Compiler Plugin, containing complete getter/setter implementations:
package com.example;
import mock.lombok.annotation.Getter;
import mock.lombok.annotation.Setter;
public class Student {
@Getter()
@Setter()
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(final String name) {
this.name = name;
}
}
Annotation Processor: LombokProcessor
LombokProcessor is a standard Annotation Processor that reads annotations in classes and modifies the AST through LombokTreeTranslator:
@AutoService(Processor.class)
@SupportedAnnotationTypes("*")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class LombokProcessor extends AbstractProcessing {
private Trees trees;
private TreeMaker treeMaker;
private Names names;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.trees = Trees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
this.names = Names.instance(context);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
for (Element element : roundEnv.getRootElements()) {
if (element.getKind().isClass()) {
JCTree tree = (JCTree) trees.getTree(element);
tree.accept(new LombokTreeTranslator(treeMaker, names));
}
}
}
return false;
}
}
AST Transformer: LombokTreeTranslator
LombokTreeTranslator extends TreeTranslator, traversing field definitions in the AST and generating corresponding methods for fields annotated with @Getter/@Setter:
public class LombokTreeTranslator extends TreeTranslator {
private TreeMaker treeMaker;
private Names names;
private List<JCTree> getters = List.nil();
private List<JCTree> setters = List.nil();
@Override
public void visitClassDef(JCClassDecl jcClassDecl) {
getters = List.nil();
setters = List.nil();
super.visitClassDef(jcClassDecl);
if (!getters.isEmpty()) {
jcClassDecl.defs = jcClassDecl.defs.appendList(this.getters);
}
if (!setters.isEmpty()) {
jcClassDecl.defs = jcClassDecl.defs.appendList(this.setters);
}
this.result = jcClassDecl;
}
@Override
public void visitVarDef(JCVariableDecl jcVariableDecl) {
super.visitVarDef(jcVariableDecl);
JCModifiers modifiers = jcVariableDecl.getModifiers();
List<JCAnnotation> annotations = modifiers.getAnnotations();
if (annotations == null || annotations.size() <= 0) {
return;
}
for (JCAnnotation annotation : annotations) {
String annotationType = annotation.type.toString();
if ("mock.lombok.annotation.Getter".equals(annotationType)
|| "Getter".equals(annotationType)) {
JCMethodDecl getterMethod = createGetterMethod(jcVariableDecl);
this.getters = this.getters.append(getterMethod);
}
if ("mock.lombok.annotation.Setter".equals(annotationType)
|| "Setter".equals(annotationType)) {
JCMethodDecl setterMethod = createSetterMethod(jcVariableDecl);
this.setters = this.setters.append(setterMethod);
}
}
}
}
Compiler Plugin: GeneratedSourcePlugin
GeneratedSourcePlugin is the key component of Source View Technology. It serves as a Compiler Plugin for javac, listening to compilation events through a TaskListener, and outputs the modified AST as View Class source code after annotation processing is complete:
@AutoService(Plugin.class)
public class GeneratedSourcePlugin implements Plugin {
private final Map<JavaFileObject, JCTree.JCCompilationUnit> fileMap = new LinkedHashMap<>();
@Override
public String getName() {
return getClass().getName();
}
@Override
public void init(JavacTask task, String... args) {
if (ObjectUtils.isEmpty(args)) {
return;
}
Map<String, String> paramMap = parseArgs(args);
String generatedJavaPath = paramMap.get("generatedJavaPath");
String mainJavaPath = paramMap.get("mainJavaPath");
Trees trees = Trees.instance(task);
task.addTaskListener(new TaskListener() {
@Override
public void started(TaskEvent taskEvent) {}
@Override
public void finished(TaskEvent taskEvent) {
if (taskEvent.getKind() == TaskEvent.Kind.ENTER) {
JavaFileObject sourceFile = taskEvent.getSourceFile();
CompilationUnitTree compilationUnitTree = taskEvent.getCompilationUnit();
if (sourceFile != null && compilationUnitTree instanceof JCTree.JCCompilationUnit) {
fileMap.putIfAbsent(sourceFile, (JCTree.JCCompilationUnit) compilationUnitTree);
}
}
else if (taskEvent.getKind() == TaskEvent.Kind.ANNOTATION_PROCESSING) {
for (Map.Entry<JavaFileObject, JCTree.JCCompilationUnit> entry : fileMap.entrySet()) {
JavaFileObject sourceFile = entry.getKey();
String path = new File(sourceFile.toUri()).getPath();
if (!path.startsWith(mainJavaPath)) {
throw new IllegalArgumentException(path + " not start with mainJavaPath");
}
String relativePath = path.substring(mainJavaPath.length());
String newPath = generatedJavaPath + relativePath;
generateSource(newPath, entry.getValue());
}
}
}
});
}
private void generateSource(String filePath, JCTree.JCCompilationUnit unit) {
File directory = new File(filePath).getParentFile();
if (!directory.exists() && !directory.mkdirs()) {
throw new IllegalArgumentException("Failed to create directory: " + directory);
}
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
Pretty pretty = new Pretty(writer, true);
pretty.printExpr(unit);
} catch (IOException e) {
throw new IllegalArgumentException("generate source failed", e);
}
}
}
Workflow:
-
ENTER phase: Collect the mapping between all source files and their ASTs in the
maindirectory -
ANNOTATION_PROCESSING phase: The Annotation Processor has completed AST modification; at this point, output the modified AST as View Class source code to the
generateddirectory using thePrettyprinter
The Compiler Plugin is loaded via the -Xplugin parameter and configured in the build script:
options.compilerArgs.add("-Xplugin:mock.lombok.processor.GeneratedSourcePlugin " +
"-mainJavaPath=${sourceSets.main.get().java.sourceDirectories.asPath} " +
"-generatedJavaPath=${sourceSets["generated"].java.sourceDirectories.asPath}")
-mainJavaPath specifies the source directory for developer-written classes, and -generatedJavaPath specifies the output directory for View Classes. The plugin only processes source files under mainJavaPath, outputting the enhanced versions to the corresponding relative paths under generatedJavaPath.
Annotation Definitions
@Getter and @Setter are located in the core module with a retention policy of SOURCE. They serve only as markers in classes and do not exist at runtime:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Getter {}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {}
Collaboration Workflow
The Annotation Processor, Compiler Plugin, and Build Logic work together to complete the full process from classes to View Classes:
-
LombokProcessorreads@Getter/@Setterannotations and modifies the AST in memory throughLombokTreeTranslator(adding getter/setter methods) -
GeneratedSourcePluginlistens to compilation events and outputs the enhanced AST as View Class source code tosrc/generated/javaafter annotation processing is complete - Build Logic includes the View Class source code in compilation, excludes developer-written Same-Name Classes, and completes the replacement
Running Results
$ ./gradlew clean run
> Task :run
Tom
BUILD SUCCESSFUL in 4s
App.java successfully calls Student.setName() and Student.getName() — these methods exist only in the View Class, while the developer-written Student has been automatically excluded at compile time.
Source View Technology combines the strengths of APT and AST: APT's ability to generate source files makes enhancement results observable and debuggable, while AST's ability to modify existing classes breaks through the Same-Name Class limitation. Through the Compiler Plugin, classes enhanced by Annotation Processors using AST technology are output as View Class source code; through Build Logic, View Classes replace developer-written Same-Name Classes at compile time. The Annotation Processor is responsible for reading annotations and modifying the AST, the Compiler Plugin is responsible for materializing the enhanced AST into View Class source code, and Build Logic is responsible for Same-Name Class replacement — the three work together to make class enhancement technology transparent, observable, and debuggable.
Project Repository: Source View
Top comments (0)