Code generation events
View MarkdownStatus: Stable Package:
dev.vertique.codegen.eventsArtifact:vertique-codegen-eventsDepends on:vertique-codegen-core(compile),vertique-events(compile) API reference: javadoc.io
vertique-codegen-events is an annotation processor that generates typed event publishers and observer registrations at compile time. For each event type in the inventory it generates a concrete X$Event extends Event<X> publisher class, and for each @Observes observer method it generates a @Provides @IntoSet ObserverRegistration provider. All generated bindings land in a single GeneratedEventsModule Dagger @Module.
The processor requires no configuration for common cases — it discovers event types automatically from @Observes parameters and Event<T> injection points.
When To Use It
Section titled “When To Use It”This processor owns event generation for modules that:
- declares observer methods (
@Observes Tparameter on a method), or - injects
Event<T>in an@Injectconstructor.
A module that only fires events (injects Event<T>) and has no observers still needs the processor so the X$Event publisher is generated and bindable.
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-events as a runtime dependency
and 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 GeneratedEventsModule to @Component
Section titled “Step 2 — add GeneratedEventsModule to @Component”@Singleton@Component(modules = { VertxModule.class, AppModule.class, GeneratedEventsModule.class, // generated by the events processor})interface AppComponent { ... }When GeneratedEventsModule is omitted:
- No
Event<X>binding exists → injection ofEvent<X>fails at Dagger graph construction. Set<ObserverRegistration>has no@Multibindsanchor →ObserverRegistryinjection may fail unless another module provides the multibinding.
Listing GeneratedEventsModule.class in @Component turns processor misconfiguration into a compile error: the generated class will not exist if the processor is absent from annotationProcessorPaths.
Event-Type Inventory
Section titled “Event-Type Inventory”The processor builds the inventory from two sources in the same compilation unit:
@Observesparameters — every method that has a parameter annotated with@Observes TcontributesTto the inventory.@Injectconstructor parameters — everyEvent<T>,Provider<Event<T>>, orLazy<Event<T>>parameter on an@Injectconstructor contributesTto the inventory.
Field injection, method injection, and @Component provision methods are not scanned. This matches the framework’s constructor-injection convention.
For each type in the inventory, the processor generates an X$Event extends Event<X> publisher. An unobserved type that is only injected as Event<X> still gets a valid no-op publisher.
Observer Validations
Section titled “Observer Validations”The processor enforces the following at compile time:
| Validation | Error |
|---|---|
Observer method must return void |
@Observes on a non-void method is a compile error |
| Observer method must have exactly one parameter | A method with multiple parameters (even if one carries @Observes) is rejected |
@Observes Object is rejected |
The registry’s superclass walk excludes Object; such an observer would silently never fire |
Generated Code Shape
Section titled “Generated Code Shape”X$Event Publisher
Section titled “X$Event Publisher”// placed in the event type's own package (e.g. com.example.events)@Generated("dev.vertique.codegen.events.EventsProcessor")public final class OrderCreated$Event extends Event<OrderCreated> {
@Inject public OrderCreated$Event(ObserverRegistry registry) { super(registry, OrderCreated.class); }}Each publisher is placed in the event type’s own package (not a shared LCP package). This guarantees that two distinct-FQN event types sharing the same simple name — for example com.acme.OrderCreated and com.shop.OrderCreated — emit com.acme.OrderCreated$Event and com.shop.OrderCreated$Event respectively, with no name collision.
GeneratedEventsModule
Section titled “GeneratedEventsModule”@Generated("dev.vertique.codegen.events.EventsProcessor")@Modulepublic abstract class GeneratedEventsModule {
// One @Binds Event<X> per inventory type // Method name: bind<SimpleName>_<ordinal>Event (ordinal = FQN-sorted position) @Binds abstract Event<OrderCreated> bindOrderCreated_0Event(OrderCreated$Event impl);
@Binds abstract Event<OrderShipped> bindOrderShipped_1Event(OrderShipped$Event impl);
// Always emitted — satisfies ObserverRegistry even when zero observers exist @Multibinds abstract Set<ObserverRegistration> observerRegistrations();
// One @Provides @IntoSet ObserverRegistration per observer method @Provides @IntoSet static ObserverRegistration provideOrderServiceOrderCreated_0ObserverRegistration( OrderService bean) { return new ObserverRegistration( OrderCreated.class, 1000, e -> bean.onOrderCreated((OrderCreated) e)); }}Deterministic Naming
Section titled “Deterministic Naming”@Binds method names — bind<SimpleName>_<ordinal>Event — where the ordinal is the zero-based position of the event type in the inventory sorted by fully-qualified name (natural String order). FQN sorting is reproducible across compilation runs and JVM implementations, independent of the non-deterministic RoundEnvironment.getRootElements() iteration order.
@Provides method names — provide<BeanSimple><EventSimpleName>_<ordinal>ObserverRegistration — where the ordinal is the zero-based position of the observer in a list sorted by (bean-FQN, method-name, event-FQN). The ordinal alone guarantees uniqueness; the simple-name prefix is included only for human readability.
The ordinal disambiguates all collision scenarios:
- Multiple observer methods in the same bean sharing the same simple name (overloads like
void on(@Observes EventA e)andvoid on(@Observes EventB e)) - Two event types sharing the same simple name but different FQNs (
com.acme.OrderCreatedvscom.shop.OrderCreated) - FQNs differing only in casing or underscore/dot placement
@Multibinds Anchor
Section titled “@Multibinds Anchor”GeneratedEventsModule always emits @Multibinds abstract Set<ObserverRegistration> observerRegistrations(). This is required even when observers exist in the module, because Dagger requires an explicit @Multibinds declaration when the set may be empty in any compilation unit that includes the module. Without this anchor, a module that has no observers would fail to compile because ObserverRegistry’s Set<ObserverRegistration> constructor parameter has no binding.
Key Classes
Section titled “Key Classes”EventsProcessor
Section titled “EventsProcessor”AbstractProcessor registered via META-INF/services/javax.annotation.processing.Processor.
@SupportedAnnotationTypes("*")@SupportedSourceVersion(SourceVersion.RELEASE_21)@SupportedOptions({ "vertique.codegen.package" })Runs @SupportedAnnotationTypes("*") (wildcard) so it observes all elements in the compilation without being pre-configured with specific annotation types. Returns false to avoid claiming annotations; sibling processors (Dagger, Lombok) see the same elements.
Processing occurs in a single pass per compilation round:
- Scan
@Observesparameters — validate return type (void), parameter count (exactly 1), and event type (!= Object). - Scan
@Injectconstructor parameters — unwrapEvent<T>,Provider<Event<T>>,Lazy<Event<T>>. - Resolve output package (option override → LCP of inventory types →
vertique.generated.eventsfallback). - Emit one
X$Eventpublisher per inventory type (deduplicated by publisher FQN). - Emit
GeneratedEventsModulewith@Binds,@Multibinds, and@Provides @IntoSetmethods.
Processor Option
Section titled “Processor Option”| Option | Default | Description |
|---|---|---|
vertique.codegen.package |
LCP of inventory-type packages | Override the output package for GeneratedEventsModule |
The publisher classes (X$Event) are always placed in the event type’s own package regardless of this option; only the module’s output package is affected.
Multi-Module Aggregation
Section titled “Multi-Module Aggregation”@Observes is SOURCE-retained and therefore invisible across module boundaries. Cross-module observer aggregation works through Dagger multibinding:
- Each module’s
GeneratedEventsModulecontributes its observers via@Provides @IntoSet ObserverRegistration. - An observer in a library module fires in the application if and only if the library’s
GeneratedEventsModuleis included in the application’s@Component. - An observer whose
GeneratedEventsModuleis omitted silently has no effect — the same “omitted-module” caveat that applies toGeneratedServicesModuleandGeneratedAopModule.
Failure Modes
Section titled “Failure Modes”Observer silently never fires
Section titled “Observer silently never fires”When GeneratedEventsModule is not included in @Component, the Set<ObserverRegistration> multibinding is empty (or absent) and ObserverRegistry.match() returns no registrations for any event type. Firing an event is a no-op. There is no compile error from the events module itself — the failure is detectable only if ObserverRegistry injection also fails (no @Multibinds anchor).
Mitigation: include GeneratedEventsModule.class explicitly in @Component. This turns processor misconfiguration (missing annotationProcessorPaths entry) into a compile error because the generated class will not exist.
Event<X> injection fails
Section titled “Event<X> injection fails”When a bean injects Event<X> but the processor is absent from annotationProcessorPaths, no X$Event class is generated and no @Binds Event<X> binding exists. Dagger fails at component compilation with a missing binding error.
Dependencies
Section titled “Dependencies”| Artifact | Scope | Purpose |
|---|---|---|
vertique-codegen-core |
compile | CodegenContext, AnnotationMirrors, Diagnostics, PackageResolver, DaggerModuleWriter |
vertique-events |
compile | Event, ObserverRegistry, ObserverRegistration, @Observes — FQN constants and ClassName references for generated source |
javapoet |
compile | Source generation (via DaggerModuleWriter and inline TypeSpec/MethodSpec construction) |
Test-only: vertique-codegen-test, dagger (for Dagger wiring tests).