Code generation AOP
View MarkdownStatus: Stable Package:
dev.vertique.codegen.aopArtifact:vertique-codegen-aopDepends on:vertique-codegen-core(compile),vertique-aop(compile) API reference: javadoc.io
vertique-codegen-aop is an annotation processor that generates reflection-free AOP proxy classes at compile time. For every bean that has at least one method annotated with an @Aspect-meta-annotated annotation, it generates:
Bean$AopProxy extends Bean— a subclass proxy that overrides each intercepted method to run the aspect chain viaInvocations.run(...).- A
MethodMetadataimplementation per intercepted method (viaMetadataEmitterfromvertique-codegen-core), including a nested per-parameterParameterMetadataimpl whosefindAnnotation/hasAnnotationare backed by generated parameter-level annotation literals — not reflection. - An AOP-owned annotation-literal class (
Ann$AopLiteral implements Ann) per distinct aspect annotation type, plus one per distinct runtime-retained annotation type found on an intercepted method’s parameters. - A single
GeneratedAopModuleDagger@Modulethat substitutes each proxy for its bean via a@Bindsmethod, replicating the bean’s declared scope.
When To Use It
Section titled “When To Use It”This processor owns proxy generation for beans whose methods carry @Timed or another
@Aspect-meta-annotated annotation. Without generated proxies, those annotations have no runtime
effect.
Adoption Recipe
Section titled “Adoption Recipe”Step 1 — use the application processor boundary
Section titled “Step 1 — use the application processor boundary”Applications inheriting vertique-app-parent declare vertique-aop and the runtime modules that
provide their aspect annotations, then receive the complete processor facade automatically.
Custom-parent applications import vertique-bom and configure only the versionless Dagger and
vertique-codegen-all processor paths. See docs/packaging.md.
Step 2 — add GeneratedAopModule to @Component
Section titled “Step 2 — add GeneratedAopModule to @Component”@Singleton@Component(modules = { VertxModule.class, AppModule.class, GeneratedAopModule.class, // generated by the AOP processor})interface AppComponent { ... }If GeneratedAopModule is omitted, no proxy is substituted for any bean — the original unproxied beans are injected and aspects silently have no effect. Including the class in @Component turns processor misconfiguration into a compile-time error (the generated class does not exist if the processor is absent from annotationProcessorPaths).
Binding Origin
Section titled “Binding Origin”The processor generates a proxy only for beans bound by their own @Inject constructor. Beans supplied by a user @Provides or @Binds method cannot be proxied:
- Same compilation unit. When the user
@Providesmethod is in the same compilation unit as the bean, the processor detects the conflict and emits a compile error naming the bean. - Cross-module. When the
@Providesis in a different module, the generated@Bindsand the user binding both target the same Dagger key, producing a deterministic Dagger duplicate-binding error at the component — never a silent or corrupt graph.
Proxyability Diagnostics
Section titled “Proxyability Diagnostics”The following bean and method shapes are rejected with a compile error:
| Shape | Reason |
|---|---|
final class |
Cannot be subclassed by Bean$AopProxy extends Bean |
final intercepted method |
Cannot be overridden by the proxy subclass |
private intercepted method |
Not visible to a subclass |
static intercepted method |
Not an instance method; cannot be overridden |
Method with a type-variable throws clause (e.g., <E extends IOException> ... throws E) |
The sync guard would emit a non-reifiable instanceof E |
@AssistedInject bean or bean with @Assisted constructor parameters |
Built by a Dagger factory; the subclass-proxy constructor cannot replicate the factory wiring |
Raw Future return on an intercepted method |
Misclassified as sync-returning; meters wrong outcome |
Wildcard Future<? ...> return on an intercepted method |
Would require an invalid back-cast in the generated override |
All legal annotation member kinds are materialized into generated literals: boolean, byte, short, int, long, char, float, double, String, Class, enum constants, nested annotations, and arrays of those. char uses a four-hex-digit \uXXXX literal. float and double use Float.intBitsToFloat(...) and Double.longBitsToDouble(...) with the exact raw bit pattern, preserving NaN, infinities, and negative zero. Literal equality uses Float.compare/Double.compare for floating-point members and their hash codes use Float.valueOf(...).hashCode()/Double.valueOf(...).hashCode(); arrays use Arrays.equals/Arrays.hashCode.
Generated Code Shape
Section titled “Generated Code Shape”Bean$AopProxy
Section titled “Bean$AopProxy”@Generated("dev.vertique.codegen.aop.AopProcessor")public final class UserService$AopProxy extends UserService {
// MethodMetadata constant per intercepted method private static final MethodMetadata GREET_0_METADATA = new GreetMethod0Metadata();
// Annotation-literal instance per aspect occurrence per method private static final Timed GREET_0_TIMED = new Timed$AopLiteral("user.greet");
// Interceptor chain field per intercepted method (resolved once in the constructor) private final MethodInterceptor[] greet_0_chain;
@Inject public UserService$AopProxy( // Verbatim replica of UserService's @Inject constructor parameters UserRepository repo, // AspectProvider<A> appended per distinct aspect annotation used on this bean AspectProvider<Timed> timedProvider) { super(repo); this.greet_0_chain = new MethodInterceptor[] { timedProvider.interceptor(GREET_0_METADATA, GREET_0_TIMED) }; }
@Override public Future<String> greet(String name) { return Invocations.run(this, GREET_0_METADATA, new Object[] {name}, greet_0_chain, () -> super.greet(name).map(r -> (Object) r)); }}For sync-returning methods (non-Future return type), the generated override calls Invocations.run(...) and synchronously unwraps the completed future result. An interceptor that returns a not-yet-complete future on a sync-returning method causes an IllegalStateException at runtime.
Annotation-Literal
Section titled “Annotation-Literal”public final class Timed$AopLiteral implements Timed { private final String value;
public Timed$AopLiteral(String value) { this.value = value; }
@Override public String value() { return value; } @Override public Class<? extends Annotation> annotationType() { return Timed.class; }
@Override public boolean equals(Object other) { if (!(other instanceof Timed that)) return false; return Objects.equals(value, that.value()); }
@Override public int hashCode() { // Per java.lang.annotation.Annotation contract return (127 * "value".hashCode()) ^ value.hashCode(); }}One AOP-owned Ann$AopLiteral class is written per distinct aspect annotation type (not per occurrence). Per-method literal instances are baked with the attribute values read from that method’s annotation mirror. Two methods sharing the same aspect type but carrying different attribute values materialize distinct literal instances from the same literal class. The Aop namespace is processor ownership: it keeps these generated types distinct from a literal emitted for the same annotation by another processor.
The literal’s equals/hashCode conform to the java.lang.annotation.Annotation contract, making a literal usable as a map key in caching-style aspects.
GeneratedAopModule
Section titled “GeneratedAopModule”@Generated("dev.vertique.codegen.aop.AopProcessor")@Modulepublic abstract class GeneratedAopModule {
@Binds @Singleton // scope replicated from UserService's declared scope abstract UserService bindUserService$AopProxy(UserService$AopProxy proxy);}The @Binds method replicates the target bean’s declared scope — it never hard-codes @Singleton. An unscoped bean remains unscoped in the proxy binding.
Key Classes
Section titled “Key Classes”AopProcessor
Section titled “AopProcessor”AbstractProcessor registered via META-INF/services/javax.annotation.processing.Processor.
@SupportedAnnotationTypes("*")@SupportedSourceVersion(SourceVersion.RELEASE_21)@SupportedOptions({ "vertique.codegen.package" })Runs @SupportedAnnotationTypes("*") (wildcard) so it receives all annotations and can discover user-defined aspect triggers — annotations meta-annotated with @Aspect — without being configured for each one. Returns false to avoid claiming annotations; Dagger, Lombok, and other processors see the same elements.
Trigger methods enclosed by an ElementKind.CLASS are collected on that class. A trigger annotation on an interface method — including an inherited default method — is woven onto every concrete implementing class in the same compilation that inherits or overrides the method: the implementor receives the $AopProxy, and proxyability / constructor validation run against that class. An interface alone is never treated as a subclass-proxy bean; when no concrete implementor is present, no proxy is generated for the interface-hosted trigger.
The module (GeneratedAopModule) is emitted in the first round that yields any binding, not in the processingOver() round. This ensures the module exists before Dagger’s ComponentProcessingStep validates a @Component that references it.
AopProxyEmitter
Section titled “AopProxyEmitter”Generates the Bean$AopProxy class, its MethodMetadata implementations (via MetadataEmitter), and the annotation-literal instances. Per-method member names (metadata constant, chain field, nested metadata type) are disambiguated by an ordinal (position in the intercepted-method list) so overloaded intercepted methods — same simple name, different parameter types — never collide.
For each intercepted method, materializeParameterAnnotations walks every parameter’s @Retention(RUNTIME) annotations and emits one <Ann>$AopLiteral per distinct annotation type encountered (deduplicated per compilation, same as the method-level literals), passing the per-parameter literal refs into MetadataEmitter.methodMetadataType so the nested MethodMetadata’s per-parameter ParameterMetadata.findAnnotation/hasAnnotation resolve from generated literals. All legal annotation member kinds are supported on parameters as well as methods; the same exact-bit-pattern and Annotation-contract equality/hash behavior applies, rather than falling back to reflection.
GeneratedAopModuleEmitter
Section titled “GeneratedAopModuleEmitter”Accumulates (bean, proxy, scope) entries and emits the single GeneratedAopModule containing one abstract @Binds method per entry.
Processor Option
Section titled “Processor Option”| Option | Default | Description |
|---|---|---|
vertique.codegen.package |
LCP of bean packages | Override the output package for GeneratedAopModule only |
The option relocates only GeneratedAopModule. The Bean$AopProxy is always generated in the
bean’s own package, regardless of this option. This is required so the proxy can call
package-private super-constructors and override package-private intercepted methods — access that
would be denied if the proxy were placed in a different package. Cross-package @Binds declarations
in the relocated module are legal because the proxy is public.
Multi-Aspect Ordering
Section titled “Multi-Aspect Ordering”When a method carries multiple aspect annotations, the processor resolves the chain order at compile time:
- Sort by descending
@Aspect.ordering()— higher is outermost. - Tiebreak by ascending fully-qualified annotation name — deterministic across compilations.
Example: @Outer (ordering=1000) + @Timed (ordering=2000) → chain is [Timed interceptor, Outer interceptor] — Timed wraps Outer which wraps the real method.
Dependencies
Section titled “Dependencies”| Artifact | Scope | Purpose |
|---|---|---|
vertique-codegen-core |
compile | CodegenContext, MetadataEmitter, AnnotationLiteralEmitter, InjectConstructorValidator, Identifiers, DaggerModuleWriter |
vertique-aop |
compile | @Aspect, AspectProvider, MethodInterceptor, Invocation, Invocations — referenced as ClassName constants in generated source |
javapoet |
compile | Source generation (via DaggerModuleWriter and inline TypeSpec/MethodSpec construction) |
Test-only: vertique-codegen-test, dagger (for Dagger wiring tests).