-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
141 additions
and
171 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,200 +1,170 @@ | ||
# Interceptor | ||
|
||
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. | ||
|
||
## Advice and 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` and `after`that are respectively called | ||
before and after a call. | ||
```java | ||
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 (or `null` is there is no argument). | ||
The last parameter of the method `after`, `result` is the returned value of the method call. | ||
|
||
- one single method that takes as last parameter a way to call the next interceptor | ||
```java | ||
@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). | ||
``java | ||
@FunctionalInterface | ||
public interface Invocation { | ||
Object proceed(Object instance, Method method, Object[] args) throws Throwable; | ||
} | ||
``` | ||
# Dependency Injection | ||
|
||
The interceptor API is more powerful and can be used to simulate the around advice API. | ||
An `InjectorRegistry` is a class able to provide instances of classes from recipes of object creation. | ||
There are two ways to get such instances either explicitly using `lookupInstance(type)` or | ||
implicitly on constructors or setters annotated by the annotation `@Inject`. | ||
|
||
|
||
## Interceptors and/or Aspect Oriented Programming | ||
### Protocols of injection | ||
|
||
The interface we are implementing here, is very similar to | ||
[Spring method interceptor](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/aopalliance/intercept/MethodInterceptor.html), | ||
[CDI interceptor](https://docs.oracle.com/javaee/6/tutorial/doc/gkhjx.html) or | ||
[Guice interceptor](https://www.baeldung.com/guice). | ||
Usual injection framework like [Guice](https://github.com/google/guice), | ||
[Spring](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies) | ||
or [CDI/Weld](https://docs.oracle.com/javaee/6/tutorial/doc/giwhb.html) | ||
provides 3 ways to implicitly get an instance of a class | ||
- constructor based dependency injection, the constructor annotated with | ||
[@Inject](https://javax-inject.github.io/javax-inject/) | ||
is called with the instances as arguments, it's considered as the best way to get a dependency | ||
- setter based dependency injection, after a call to the default constructor, the setters are called. | ||
The main drawback is that the setters can be called in any order (the order may depend on | ||
the version of the compiler/VM used) | ||
- field based dependency injection, after a call to the default constructor, the fields are filled with the instances, | ||
this methods bypass the default security model of Java using **deep reflection**, relying on either | ||
not having a module declared or the package being open in the module-info.java. Because of that, | ||
this is not the recommended way of doing injection. | ||
|
||
All of them are using the same API provided by the | ||
[Aspect Oriented Programming Alliance](http://aopalliance.sourceforge.net/) | ||
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`. | ||
We will only implement the constructor based and setter based dependency injection. | ||
|
||
[Aspect Oriented Programming, AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming) is a more general | ||
conceptual framework from the beginning of 2000s, an interceptor is equivalent to the around advice. | ||
|
||
### Early vs Late checking | ||
|
||
## An example | ||
When injecting instances, an error can occur if the `InjectorRegistry` has no recipe to create | ||
an instance of a class. Depending on the implementation of the injector, the error can be | ||
detected either | ||
- when a class that asks for injection is registered | ||
- when an instance of a class asking for injection is requested | ||
|
||
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. | ||
The former is better than the later because the configuration error are caught earlier, | ||
but here, because we want to implement a simple injector, all configuration errors will appear | ||
late when an instance is requested. | ||
|
||
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 | ||
|
||
```java | ||
@Retention(RUNTIME) | ||
@Target(METHOD) | ||
@interface CheckNotNull { } | ||
``` | ||
### Configuration | ||
|
||
If we want to check the argument of a method of an interface, we need to annotate it with `@CheckNotNull` | ||
```java | ||
interface Hello { | ||
@CheckNotNull String say(String message, String name); | ||
} | ||
``` | ||
There are several ways to configure an injector, it can be done | ||
- using an XML file, this the historical way (circa 2000-2005) to do the configuration. | ||
- using classpath/modulepath scanning. All the classes of the application are scanned and classes with | ||
annotated members are added to the injector. The drawback of this method is that this kind of scanning | ||
is quite slow, slowing down the startup time of the application. | ||
Recent frameworks like Quarkus or Spring Native move the annotation discovery at compile time using | ||
an annotation processor to alleviate that issue. | ||
- using an API to explicitly register the recipe to get the dependency. | ||
|
||
We will implement the explicit API while the classpath scanning is implemented in [the part 2](README2.md). | ||
|
||
|
||
### Our injector | ||
|
||
The class `InjectorRegistry` has 4 methods | ||
- `lookupInstance(type)` which returns an instance of a type using a recipe previously registered | ||
- `registerInstance(type, object)` register the only instance (singleton) to always return for a type | ||
- `registerProvider(type, supplier)` register a supplier to call to get the instance for a type | ||
- `registerProviderClass(type, class)` register a bean class that will be instantiated for a type | ||
|
||
As an example, suppose we have a record `Point` and a bean `Circle` with a constructor `Circle` annotated | ||
with `@Inject` and a setter `setName` of `String` also annotated with `@Inject`. | ||
|
||
We also have an implementation of that interface, that provides the behavior the user want | ||
```java | ||
class HelloImpl implements Hello { | ||
@Override | ||
public String say(String message, String name) { | ||
return message + " " + name; | ||
} | ||
record Point(int x, int y) {} | ||
|
||
class Circle { | ||
private final Point center; | ||
private String name; | ||
|
||
@Inject | ||
public Circle(Point center) { | ||
this.center = center; | ||
} | ||
|
||
@Inject | ||
public void setName(String name) { | ||
this.name = name; | ||
} | ||
} | ||
``` | ||
|
||
Step 1, we create an interceptor registry and add an around advice that checks that the arguments are not null | ||
We can register the `Point(0, 0)` as the instance that will always be returned when an instance of `Point` is requested. | ||
We can register a `Supplier` (here, one that always return "hello") when an instance of `String` is requested. | ||
We can register a class `Circle.class` (the second parameter), that will be instantiated when an instance of `Circle` | ||
is requested. | ||
|
||
```java | ||
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) {} | ||
}); | ||
var registry = new InjectorRegistry(); | ||
registry.registerInstance(Point.class, new Point(0, 0)); | ||
registry.registerProvider(String.class, () -> "hello"); | ||
registry.registerProviderClass(Circle.class, Circle.class); | ||
|
||
var circle = registry.lookupInstance(Circle.class); | ||
System.out.println(circle.center); // Point(0, 0) | ||
System.out.println(circle.name); // hello | ||
``` | ||
|
||
Step 2, we create a proxy in between the interface and the implementation | ||
```java | ||
var proxy = registry.createProxy(Hello.class, hello); | ||
|
||
assertAll( | ||
() -> assertEquals("hello around advice", proxy.say("hello", "around advice")), | ||
() -> assertThrows(NullPointerException.class, () -> proxy.say("hello", null)) | ||
); | ||
``` | ||
## Let's implement it | ||
|
||
We can test the proxy with several arguments, null or not | ||
```java | ||
assertAll( | ||
() -> assertEquals("hello around advice", proxy.say("hello", "around advice")), | ||
() -> assertThrows(NullPointerException.class, () -> proxy.say("hello", null)) | ||
); | ||
``` | ||
The unit tests are in [InjectorRegistryTest.java](src/test/java/com/github/forax/framework/injector/InjectorRegistryTest.java) | ||
|
||
1. Create a class `InjectorRegistry` and add the methods `registerInstance(type, instance)` and | ||
`lookupInstance(type)` that respectively registers an instance into a `Map` and retrieves an instance for | ||
a type. `registerInstance(type, instance)` should allow registering only one instance per type and | ||
`lookupInstance(type)` should throw an exception if no instance have been registered for a type. | ||
Then check that the tests in the nested class "Q1" all pass. | ||
|
||
## The interceptor registry | ||
Note: for now, the instance does not have to be an instance of the type `type`. | ||
You can use [Map.putIfAbsent()](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/util/Map.html#putIfAbsent(K,V)) | ||
to detect if there is already a pair/entry with the same key in the `Map` in one call. | ||
|
||
An `InterceptorRegistry` is a class that manage the interceptors, it defines three public methods | ||
- `addAroundAdvice(annotationClass, aroundAdvice)` register an around advice for an annotation | ||
- `addInterceptor(annotationClass, interceptor)` register an interceptor for an annotation | ||
- `createProxy(interfaceType, instance)` create a proxy that for each annotated methods will call | ||
the advices/interceptors before calling the method on the instance. | ||
|
||
2. We want to enforce that the instance has to be an instance of the type taken as parameter. | ||
For that, declare a `T` and say that the type of the `Class`and the type of the instance is the same. | ||
Then use the same trick for `lookupInstance(type)` and check that the tests in the nested class "Q2" all pass. | ||
|
||
Note: inside `lookupInstance(type)`, now that we now that the instance we return has to be | ||
an instance of the type, we can use | ||
[Class.cast()](https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/lang/Class.html#cast(java.lang.Object)) | ||
to avoid an unsafe cast. | ||
|
||
|
||
3. We now want to add the method `registerProvider(type, supplier)` that register a supplier (a function with | ||
no parameter that return a value) that will be called each time an instance is requested. | ||
An astute reader can remark that a supplier can always return the same instance thus we do not need two `Map`s, | ||
but only one that stores suppliers. | ||
Add the method `registerProvider(type, supplier)` and modify your implementation to support it. | ||
Then check that the tests in the nested class "Q3" all pass. | ||
|
||
|
||
4. In order to implement the injection using setters, we need to find all the | ||
[Bean properties](../COMPANION.md#java-bean-and-beaninfo) | ||
that have a setter [annotated](../COMPANION.md#methodisannotationpresent-methodgetannotation-methodgetannotations) | ||
with `@Inject`. | ||
Write a helper method `findInjectableProperties(class)` that takes a class as parameter and returns a list of | ||
all properties (`PropertyDescriptor`) that have a setter annotated with `@Inject`. | ||
Then check that the tests in the nested class "Q4" all pass. | ||
|
||
Note: The class `Utils` already defines a method `beanInfo()`. | ||
|
||
|
||
5. We want to add a method `registerProviderClass(type, providerClass)` that takes a type and a class, | ||
the `providerClass` implementing that type and register a recipe that create a new instance of | ||
`providerClass` by calling the default constructor. This instance is initialized by calling all the | ||
setters annotated with `@Inject` with an instance of the corresponding property type | ||
(obtained by calling `lookupInstance`). | ||
Write the method `registerProviderClass(type, providerClass)` and | ||
check that the tests in the nested class "Q5" all pass. | ||
|
||
Note: The class `Utils` defines the methods `defaultConstructor()`, `newInstance()` and `invokeMethod()`. | ||
|
||
|
||
6. We want to add the support of constructor injection. | ||
The idea is that either only one of the | ||
[public constructors](../COMPANION.md#classgetmethod-classgetmethods-classgetconstructors) | ||
of the `providerClass` is annotated with `@Inject` or a public default constructor | ||
(a constructor with no parameter) should exist. | ||
Modify the code that instantiate the `providerClass` to use that constructor to | ||
creates an instance. | ||
Then check that the tests in the nested class "Q6" all pass. | ||
|
||
## Let's implement it | ||
|
||
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. | ||
|
||
|
||
1. Create a class `InterceptorRegistry` with two public methods | ||
- a method `addAroundAdvice(annotationClass, aroundAdvice)` that for now do not care about the | ||
`annotationClass` and store the advice in a field. | ||
- a method `createProxy(type, delegate)` that creates a [dynamic proxy](../COMPANION.md#dynamic-proxy) | ||
implementing the interface and calls the method `before` and `after` of the around advice | ||
(if one is defined) around the call of each method using `Utils.invokeMethod()`. | ||
Check that the tests in the nested class "Q1" all pass. | ||
|
||
|
||
2. Change the implementation of `addAroundAdvice` to store all advices by annotation class. | ||
And add a package private instance method `findAdvices(method)` that takes a `java.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](../COMPANION.md#annotation) of that method | ||
and find all corresponding advices. | ||
Once the method `findAdvices` works, modify the method `createProxy`to use it. | ||
Check that the tests in the nested class "Q2" all pass. | ||
|
||
|
||
3. 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 method | ||
`finInterceptors(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. | ||
|
||
|
||
4. 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 method `List.reversed()` | ||
which return a reversed list without moving the elements of the initial list. | ||
Add the method `getInvocation`. | ||
Check that the tests in the nested class "Q4" all pass. | ||
|
||
|
||
5. 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 of `addAroundAdvice` to use an interceptor, and modify the | ||
code of `createProxy` to use interceptors instead of advices. | ||
Check that the tests in the nested class "Q5" all pass. | ||
Note: given that the method `findAdvices` is now useless, the test Q2.findAdvices() should be commented. | ||
|
||
|
||
6. 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. | ||
|
||
|
||
7. 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. | ||
7. To finish, we want to add a user-friendly overload of `registerProviderClass`, | ||
`registerProviderClass(providerClass)` that takes only a `providerClass` | ||
and is equivalent to `registerProviderClass(providerClass, providerClass)`. | ||
|