AOP and events
View MarkdownVertique’s two in-process cross-cutting mechanisms share one design: annotate plain code, let the compile-time processor generate the wiring, and dispatch with no reflection:
- Aspects are annotations. Meta-annotate your own annotation with
@Aspect, or use the built-in@Timed, and implement one aspect provider. Every annotated method is then wrapped by a subclass proxy generated at compile time. No runtime weaver is involved. - Framework capabilities share the seam.
@Resilient,@Cacheable,@CacheEvict,@RateLimited, and@Timedare framework-provided aspect triggers. Their family modules supply the providers;vertique-codegen-aopgenerates the proxy and its Dagger binding. - Events are typed. Fire a plain record through an injected typed publisher. Any bean method
with an
@Observesparameter receives it, matched by subtype routing. - No reflection at dispatch time. Generated proxies call the original method directly, observers register through generated Dagger bindings, and aspect annotation values are captured as generated literals, so annotations are not read at runtime.
- Deterministic aspect ordering. Interceptors order by the aspect’s declared
ordering, outermost first, with stable tie-breaking across compilations. - Notification-only event semantics. Firing an event always succeeds. A throwing observer does not fail the fan-out or prevent later observers from running.
Reach for an aspect when the same around-behavior (timing, auditing, guarding) belongs on methods across unrelated beans. A per-operation concern on a service contract belongs in a services interceptor. Reach for an event when beans inside one application must react to each other without coupling. An event is not cross-verticle dispatch, which is a service contract, and it is not durable messaging, which is inbox/outbox.
Cache and rate-limit annotations use the same selector vocabulary: key is an ordered list of
parameter or accessor paths, while subject selects the caller dimension (CacheIdentity for
caching and RateLimitSubject for rate limiting). Use the current subject attribute when
composing these annotations. See the vertique-cache-aop module reference
and vertique-rate-limit-aop module reference
for their complete contracts.
Time a method with an aspect
Section titled “Time a method with an aspect”The public vertique-example-aop example applies the built-in @Timed aspect (from
vertique-micrometer-core) to plain bean methods. This excerpt is abridged from its Greeter:
@Singletonpublic class Greeter {
@Inject public Greeter() {}
@Timed("vertique.example.greet") public Future<String> greet(String name) { return Future.succeededFuture("Hello, " + name + "!"); }
@Timed( value = "vertique.example.farewell", extraTags = {"tone", "formal"}) public Future<String> farewell(String name) { return Future.succeededFuture("Goodbye, " + name + "."); }}At compile time the AOP processor generates a subclass proxy for every bean with at least one aspect-annotated method, plus a generated Dagger module that binds the bean type to its proxy. Injectors receive the proxied instance. The only application wiring is installing the generated module and binding the aspect’s provider, as the example’s own component does.
Apply resilience to a bean
Section titled “Apply resilience to a bean”@Resilient is the method-level anchor for resilience on a concrete, Dagger-managed bean. Add it
to the method together with @Retry, @Timeout, @CircuitBreaker, or @Bulkhead. The generated
proxy then runs the common resilience pipeline. A concrete method carrying a resilience declaration
without @Resilient fails at compile time, so a declaration cannot be left inert. The method
must return a Vert.x future because the resilience components are asynchronous.
This abridged method comes from the public
vertique-example-services resilience probe:
@Resilient(policy = "probe")@Retry(maxRetries = 2, delayMs = 1, maxDelayMs = 1)public Future<String> probe(String key) { // The example fails the first four attempts and succeeds on the fifth.}policy is optional. With a non-empty policy name, configuration supplies a named tier. Without a
policy name, at least one inline resilience declaration is required. The
Services page shows the complete service-handler path, including the runtime and
generated modules that must be installed in the component.
Framework aspects use reserved ordering bands, with higher values outermost. In the shipped composition, rate-limit admission surrounds cache lookup, and resilience runs inside the cache. A cache hit therefore bypasses retry and breaker execution, and retries on a cache miss do not consume additional quota. The AOP module reference owns the complete ordering table and the edge-case rules.
Define your own aspect
Section titled “Define your own aspect”An aspect is two pieces: an annotation meta-annotated with @Aspect, and an aspect provider that
builds the interceptor for each annotated method. From the vertique-aop module reference:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Aspect(ordering = 2000)public @interface Timed { String value() default "";}@ProvidesAspectProvider<Timed> timedAspectProvider(MeterRegistry registry) { return (target, timed) -> invocation -> { Timer.Sample sample = Timer.start(registry); return invocation.proceed().onComplete(ar -> { String name = timed.value().isEmpty() ? target.name() : timed.value(); sample.stop(registry.timer(name, "status", ar.succeeded() ? "ok" : "error")); }); };}The interceptor is around-advice: run logic before and after invocation.proceed(), read the
method’s reflection-free metadata from target, and read the annotation’s attribute values from
the generated literal. An aspect on a method that does not return a future must complete
synchronously. A deferring interceptor on a sync-returning method fails with an
IllegalStateException at call time, and no thread is blocked. The
vertique-aop module reference
documents the interceptor chain, ordering, and every invariant.
Publish and observe typed events
Section titled “Publish and observe typed events”An event is a plain record. The publisher is an injected typed interface, and an observer is any
bean method with an @Observes parameter. From the public vertique-example-events example:
public record OrderCreated(String orderId) {}
public class OrderService {
private final Event<OrderCreated> events;
@Inject public OrderService(Event<OrderCreated> events) { this.events = events; }
public Future<Void> placeOrder(String orderId) { return events.fire(new OrderCreated(orderId)); }}
@Singletonpublic class OrderObserver {
public void onOrder(@Observes OrderCreated event) { // react to the event }}The events processor generates the publisher implementation and the observer registrations into a
generated Dagger module the application installs. The application does not assemble an observer
list by hand, and no classpath scanning happens at runtime. Dispatch is notification-only: fire
completes successfully even when an observer throws, and every remaining observer still runs.
Wiring
Section titled “Wiring”Applications parented on vertique-app-parent already run the complete annotation-processor
facade, so declaring the runtime modules is all that is needed:
| Add | For |
|---|---|
vertique-aop |
The @Aspect meta-annotation, AspectProvider, and the interceptor chain runtime |
vertique-resilience |
@Resilient, the resilience runtime, named-policy registry, and ResilienceAopModule |
vertique-events |
@Observes, the typed event publisher, and the observer registry |
Install each generated module in your application component alongside your own modules. The AOP processor’s generated module binds each proxied bean, and the events processor’s generated module registers publishers and observers. Both examples’ components show the shape.
Learn more
Section titled “Learn more”vertique-aopmodule reference: the aspect model, interceptor chain,MethodMetadata, and invariants.vertique-eventsmodule reference: the publisher, observer registry, subtype routing, and dispatch semantics.vertique-resiliencemodule reference: the resilience anchor, declaration annotations, named policy tiers, and execution pipeline.Greeter: the timed bean the aspect excerpt above abridges, in thevertique-example-aopexample.OrderService: the publisher the events excerpt above abridges, in thevertique-example-eventsexample.- Services: per-operation interceptors and cross-verticle dispatch.
- Code generation: every processor and what it generates.
- What is Vertique?
Continue reading
Section titled “Continue reading”- Previous: JSON and profiles
- Next: Code generation