Creating an Interceptor with Annotations
The first example you are going to run consists in designing a simple interception system for service methods. This is a system that exists in Java EE, Jakarta EE, and other enterprise application frameworks. The concept is simple: you add an annotation on a method, this annotation takes a class as an attribute, and a specific method of this class is called instead of your service method.
What is an Interceptor?
The concept of intercepting a method call is very simple. it relies on two elements: a service that has a service method, and an interceptor, that also carries a method. When you call your service method, what you want is that this interceptor method is called instead of it.
Then, this method can do many things.
- It can validate the arguments sent to your service method.
- It can change these arguments.
- Beside some validation, it can also enforce some security rules.
- It can decide to call your service method, or not.
- It can then get the result of your service method, and decide to return it as is, or to modify it.
- And it can catch the exceptions thrown by your service method, and act upon them.
This kind of interceptor can be implemented using Proxy
objects, that are beyond the scope of this chapter. So we are going to implement this feature in a simpler way.
Designing the Intercept Interface and Annotation
Let us start with the annotation you need. This annotation needs to tell you what class of interceptor is to be used. For the sake of simplicity, we are going to impose that interceptors implement a specific interface.
Note that this interface uses parameterized types for the object you intercept, and the returned type of the method you intercept.
public interface Interceptor<T, R> {
R intercept(T interceptedObject, Method method, Object... arguments);
}
Then you can design the annotation you need.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Intercept {
Class<? extends Interceptor<?, ?>> value();
}
Designing a Service and an Intercepted Method
Let us now create a service, with a method that you need to intercept. The MessageInterceptor
class is an implementation of the Interceptor
interface that you are going to write in the next section.
public class SomeInterceptedService {
@Intercept(MessageInterceptor.class)
public String message(String input) {
return input.toUpperCase();
}
}
Having written this code, what you expect is the following.
- You need a way to call the
message()
method of yourSomeInterceptedService
class. - When you call it, you expect your implementation of the
intercept()
method from yourMessageInterceptor
class to be called before themessage()
method is called. - You then expect to get the result of this call.
The code you write can look like the following. Note that the invoke()
method takes the class of your service, the name of the method, and the arguments you need to pass to this method. In a Proxy
based system, this call would be much simpler, as a dynamic proxy can build implementations for you. So your call would look like a regular method call on a regular class.
void main() {
String output = ServiceFactory.invoke(SomeInterceptedService.class, "message", "Hello");
System.out.println("output = " + output);
}
At this point you need to write two classes: the MessageInterceptor
class, and the ServiceFactory
class.
Writing the MessageInterceptor Class
This class is an implementation of the Interceptor
interface that has the responsibility to decide, or not, to call your intercepted service.
You could write it specifically to call the method of the service you need. What we do here, is that the intercepted method is a parameter of the intercept()
method. So, as you saw in the previous sections of this chapter, you need three elements to reflectively call this method:
- The method itself, in the form of a
Method
object. Note that you could also pass the name of this method. - The object on which this method is invoked. This is actually your service.
- And the arguments you need to pass to this method.
Then what you need to do in this method is to implement your business logic: why do you need to intercept this method, what do you need to do in this interceptor?
Here, we are first doing some argument validation. The arguments
array should contain only one element of type String
. If this is not the case, we throw an exception, and the intercepted method is not called.
Then we call the intercepted method and get the result it produced. Note that we could have modified the arguments passed to this method, if needed.
Then we modify the result by adding the [was intercepted]
message to it.
public class MessageInterceptor implements Interceptor<SomeInterceptedService, String> {
@Override
public String intercept(
SomeInterceptedService service, Method interceptedMethod, Object... arguments) {
try {
if (arguments.length == 1) {
// validating the arguments
String input = (String) arguments[0];
Objects.requireNonNull(input, "Input is null");
if (input.isEmpty()) {
throw new IllegalArgumentException("Input is empty");
}
// calling the service method
String result = (String)interceptedMethod.invoke(service, arguments);
return result + " [was intercepted]";
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
throw new IllegalArgumentException(
"Arguments should contain exactly one argument of type String");
}
}
Writing the ServiceFactory Class
This class is more technical, and leverages the Reflection API.
It takes the class on which the method is declared, the name of the method that is to be intercepted, and the arguments you need to pass to this method.
First, it needs to create an instance of the service. This is done by getting the empty constructor of this service. This constructor needs to be there, if not an exception is thrown.
Then, it needs to locate the method it needs to call. For that, it needs two elements: the name of the method, and the types of its parameters in an array. This is what the stream is doing: it creates an array of classes from the array of arguments. With these two elements, it can then locate the right method.
Then, it needs to check if the annotation is present on this method. If it is, then it needs to get the instance of this annotation and to call the value()
method on it, that returns the class of the interceptor. Note that you need to cast it to the right type, as annotations do not support generics. This cast will issue a compiler warning, as the compiler cannot check if it is correct or not.
Once it knows the class of the interceptor, it needs to create an instance of it, to call its intercept()
method with the right arguments. This is a direct call, not a reflective call.
Note that if the annotation is not found, then it reflectively calls the service method, without any interception.
In both cases, it needs to get the returned object, and to return it.
public class ServiceFactory {
public static <T, R> R invoke(
Class<? extends T> serviceClass, String methodName, Object... arguments) {
try {
// Getting an instance of the service
T service = serviceClass.getConstructor().newInstance();
// Locating the service method
Class<?>[] parameterClasses =
Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
Method method = serviceClass.getDeclaredMethod(methodName, parameterClasses);
// locating the Intercept annotation
if (method.isAnnotationPresent(Intercept.class)) {
Intercept intercept = method.getDeclaredAnnotation(Intercept.class);
Class<? extends Interceptor<T, R>> interceptorClass =
(Class<? extends Interceptor<T, R>>) intercept.value();
// creating an instance of the interceptor
Interceptor<T, R> interceptor = interceptorClass.getConstructor().newInstance();
// intercepting the service method
R returnedObject = interceptor.intercept(service, method, arguments);
return returnedObject;
} else {
// invoking the service method
R returnedObject = (R) method.invoke(service, arguments);
return returnedObject;
}
} catch (InstantiationException | IllegalAccessException |
InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
}
As you can see, this code is rather complex, somehow obscure, and not type safe at compile time. This is most of the time the case when you are using the Reflection API on real application use cases.
Now that you have all the elements, you can finally run this code.
Let us add another class, to show that the interception mechanism is working correctly. This SomeNonInterceptedService
class is the same as the SomeInterceptedService
class. The only difference is that its service method does not declare the interceptor.
public static class SomeNonInterceptedService {
String message(String input) {
return input.toUpperCase();
}
}
Now that you have all the elements, you can run the following code.
public static void main(String[] args) {
String interceptedOutput =
ServiceFactory.invoke(SomeInterceptedService.class, "message", "Hello");
System.out.println("Intercepted output = " + interceptedOutput);
String nonInterceptedOutput =
ServiceFactory.invoke(SomeNonInterceptedService.class, "message", "Hello");
System.out.println("Non intercepted output = " + nonInterceptedOutput);
}
It prints the following result.
Intercepted output = HELLO [was intercepted]
Non intercepted output = HELLO
Last update: July 25, 2024