An interceptor is a function that is called before/after a method call to react to the arguments or return value of that method call. To select which interceptor will be run on which method call, we register an interceptor with an annotation class and all methods annotated with an annotation of that class will be intercepted by this interceptor.
There are more or less two different kind of API to intercept a method call.
-
the around advice, an interface with two methods,
before
andafter
that are respectively called before and after a call.public interface AroundAdvice { void before(Object instance, Method method, Object[] args) throws Throwable; void after(Object instance, Method method, Object[] args, Object result) throws Throwable; }
The
instance
is the object on which the method is be called,method
is the method called,args
are the arguments of the call (ornull
is there is no argument). The last parameter of the methodafter
,result
is the returned value of the method call. -
one single method that takes as last parameter a way to call the next interceptor
@FunctionalInterface public interface Interceptor { Object intercept(Method method, Object proxy, Object[] args, Invocation invocation) throws Throwable; }
with
Invocation
a functional interface corresponding to the next interceptor i.e. an interface with an abstract method bound to a specific interceptor (partially applied if you prefer).@FunctionalInterface public interface Invocation { Object proceed(Object instance, Method method, Object[] args) throws Throwable; }
The interceptor API is more powerful and can be used to simulate the around advice API.
The interface we are implementing here, is very similar to Spring method interceptor, CDI interceptor or Guice interceptor.
All of them are using the same API provided by the
Aspect Oriented Programming Alliance
which is a group created to define a common API for interceptors in Java.
Compared to the API we are about to implement, the AOP Alliance API encapsulates the parameters
(instance, method, args, link to the next interceptor) inside the interface MethodInvocation
.
Aspect Oriented Programming, AOP is a more general conceptual framework from the beginning of 2000s, an interceptor is equivalent to the around advice.
The API works in two steps, first register an advice (or an interceptor) for an annotation, then creates a proxy of an interface. When a method of the proxy is called through the interface, if the method is annotated, the corresponding advices/interceptors will be called.
For example, if we want to implement an advice that will check that the arguments of a method are not null. First we need to define an annotation
@Retention(RUNTIME)
@Target(METHOD)
@interface CheckNotNull { }
If we want to check the argument of a method of an interface, we need to annotate it with @CheckNotNull
interface Hello {
@CheckNotNull String say(String message, String name);
}
We also have an implementation of that interface, that provides the behavior the user want
class HelloImpl implements Hello {
@Override
public String say(String message, String name) {
return message + " " + name;
}
}
Step 1, we create an interceptor registry and add an around advice that checks that the arguments are not null
var registry = new InterceptorRegistry();
registry.addAroundAdvice(CheckNotNull.class, new AroundAdvice() {
@Override
public void before(Object delegate, Method method, Object[] args) {
Arrays.stream(args).forEach(Objects::requireNonNull);
}
@Override
public void after(Object delegate, Method method, Object[] args, Object result) {}
});
Step 2, we create a proxy in between the interface and the implementation
var proxy = registry.createProxy(Hello.class, hello);
assertAll(
() -> assertEquals("hello around advice", proxy.say("hello", "around advice")),
() -> assertThrows(NullPointerException.class, () -> proxy.say("hello", null))
);
We can test the proxy with several arguments, null or not
assertAll(
() -> assertEquals("hello around advice", proxy.say("hello", "around advice")),
() -> assertThrows(NullPointerException.class, () -> proxy.say("hello", null))
);
An InterceptorRegistry
is a class that manage the interceptors, it defines three public methods
addAroundAdvice(annotationClass, aroundAdvice)
register an around advice for an annotationaddInterceptor(annotationClass, interceptor)
register an interceptor for an annotationcreateProxy(interfaceType, instance)
create a proxy that for each annotated methods will call the advices/interceptors before calling the method on the instance.
The idea is to gradually implement the class InterceptorRegistry
, first by implementing the support
for around advice then add the support of interceptor and retrofit around advices to be implemented
as interceptors. To finish, add a cache avoiding recomputing of the linked list of invocations
at each call.
-
Create a class
InterceptorRegistry
with two public methods- a method
addAroundAdvice(annotationClass, aroundAdvice)
that for now do not care about theannotationClass
and store the advice in a field. - a method
createProxy(type, delegate)
that creates a dynamic proxy implementing the interface and calls the methodbefore
andafter
of the around advice (if one is defined) around the call of each method usingUtils.invokeMethod()
. Check that the tests in the nested class "Q1" all pass.
- a method
-
Change the implementation of
addAroundAdvice
to store all advices by annotation class. And add a package private instance methodfindAdvices(method)
that takes ajava.lang.reflect.Method
as parameter and returns a list of all advices that should be called. An around advice is called for a method if that method is annotated with an annotation of the annotation class on which the advice is registered. The idea is to gather all annotations of that method and find all corresponding advices. Once the methodfindAdvices
works, modify the methodcreateProxy
to use it. Check that the tests in the nested class "Q2" all pass. -
We now want to be support the interceptor API, and for now we will implement it as an addon, without changing the support of the around advices. Add a method
addInterceptor(annotationClass, interceptor)
and a methodfindInterceptors(method)
that respectively add an interceptor for an annotation class and returns a list of all interceptors to call for a method. Check that the tests in the nested class "Q3" all pass. -
We want to add a method
getInvocation(interceptorList)
that takes a list of interceptors as parameter and returns an Invocation which when it is called will call the first interceptor with as last argument an Invocation allowing to call the second interceptor, etc. The last invocation will call the method on the instance with the arguments. Because each Invocation need to know the next Invocation, the chained list of Invocation need to be constructed from the last one to the first one. To loop over the interceptors in reverse order, you can use the methodList.reversed()
which return a reversed list without moving the elements of the initial list. Add the methodgetInvocation
. Check that the tests in the nested class "Q4" all pass. -
We know want to change the implementation to only uses interceptor internally and rewrite the method
addAroundAdvice
to use an interceptor that will calls the around advice. Change the implementation ofaddAroundAdvice
to use an interceptor, and modify the code ofcreateProxy
to use interceptors instead of advices. Check that the tests in the nested class "Q5" all pass. Note: given that the methodfindAdvices
is now useless, the test Q2.findAdvices() should be commented. -
Add a cache avoiding recomputing a new
Invocation
each time a method is called. When the cache should be invalidated ? Change the code to invalidate the cache when necessary. Check that the tests in the nested class "Q6" all pass. -
We currently support only annotations on methods, we want to be able to intercept methods if the annotation is not only declared on that method but on the declaring interface of that method or on one of the parameter of that method. Modify the method
findInterceptors(method)
. Check that the tests in the nested class "Q7" all pass.