Interceptors
Interceptors add compile-time lifecycle hooks around calls made through Sting-published service interfaces. Sting generates direct service-interface proxies, so interception does not use reflection, dynamic proxies, bytecode generation, runtime annotation lookup, or runtime classpath scanning.
Interception applies at the service-interface boundary. A component that calls one of its own methods directly is not intercepted; consumers that request the published service interface receive the generated proxy.
Binding Annotations
An interceptor binding is a normal annotation annotated with
@InterceptorBinding. All public interceptor annotations live in
the sting.interceptors package.
@InterceptorBinding( implementedBy = "sting.doc.examples.interceptors.TimingInterceptor", priority = 100 )
@Retention( RetentionPolicy.CLASS )
@Target( { ElementType.TYPE, ElementType.METHOD } )
public @interface Timed
{
}
The implementedBy value is the canonical dotted name of an @Injectable interceptor
implementation. Lower priority values run outermost. Equal effective priorities for one intercepted service are
compile errors.
Bindings may be placed on:
- service interface types,
- injectable implementation classes, and
- fragment provider methods.
Method-level bindings on service methods or implementation methods are not supported.
Lifecycle Methods
Generic interceptor implementations use public instance methods annotated with
@Before, @After, or
@AfterException.
@Injectable
public final class TimingInterceptor
{
@Before
public void before( @ServiceType final String serviceType, @MethodName final String methodName )
{
}
@After
public void after( @ServiceType final String serviceType,
@MethodName final String methodName,
@Result final Object result )
{
}
@AfterException
public void afterException( @ServiceType final String serviceType,
@MethodName final String methodName,
@Thrown final Throwable thrown )
{
}
}
The lifecycle order is:
@Before: outer-to-inner.@After: inner-to-outer after a successful target call.@AfterException: inner-to-outer when the target or an inner interceptor fails.
An interceptor's own @AfterException method does not observe failures from that same interceptor's @Before or
@After method. Outer interceptors still observe failures from inner interceptors.
Metadata Parameters
Every lifecycle method parameter must have exactly one marker annotation from sting.interceptors.
Supported metadata:
@ServiceTypeString: the intercepted service interface name.@MethodNameString: the service method name.@BindingValuescalar values from the binding annotation.@ArgumentsObject[]: the original target arguments.@ResultObject: successful return value for@After.@ThrownThrowable: thrown failure for@AfterException.
@Injectable
public final class AuditInterceptor
{
@Before
public void before( @BindingValue( "action" ) final String action, @Arguments final Object[] arguments )
{
}
}
@Arguments is metadata only. Mutating the array does not rewrite the arguments passed to the target method.
Binding Locations
A binding on a service interface applies to every binding that publishes that service interface.
@Audited( action = "accounts" )
@Timed
public interface AccountService
{
void updateAccount( String accountId );
}
A binding on a fragment provider method applies to the service interfaces published by that provider method.
@Fragment
public interface PaymentFragment
{
@Timed
@Typed( PaymentGateway.class )
default PaymentGateway providePaymentGateway()
{
return ( accountId, amount ) -> {
};
}
}
When one binding publishes multiple service interfaces, each intercepted service interface receives its own cached proxy.
@Fragment
public interface NotificationFragment
{
@Audited( action = "notifications" )
@Typed( { NotificationSender.class, NotificationAuditTrail.class } )
default NotificationService provideNotificationService()
{
return new NotificationService();
}
}
Injector Requests
All request kinds for an intercepted service coordinate receive the proxy: direct instance requests, optional requests, suppliers, supplier optionals, collections, supplier collections, and supplier optional collections. The raw target remains internal to the generated injector and proxy.
@Injector(
fragmentOnly = false,
includes = { AccountServiceImpl.class,
PaymentFragment.class,
NotificationFragment.class } )
public interface InterceptorsInjector
{
@Nonnull
static InterceptorsInjector create()
{
return new Sting_InterceptorsInjector();
}
@Nonnull
AccountService accountService();
@Nonnull
PaymentGateway paymentGateway();
@Nonnull
NotificationSender notificationSender();
@Nonnull
NotificationAuditTrail notificationAuditTrail();
}
Plugin Bindings
Processor-path plugins may claim a binding and emit dependency-free lifecycle code directly. A binding intended only
for plugins leaves implementedBy empty and must be claimed by exactly one plugin for every effective intercepted
service coordinate.
@InterceptorBinding( priority = 200 )
@Retention( RetentionPolicy.CLASS )
@Target( { ElementType.TYPE, ElementType.METHOD } )
public @interface PluginOnly
{
}
Plugin emission is intentionally lifecycle-only. Plugins cannot add graph dependencies, fields, helper methods, constructor parameters, imports, or runtime service requests.