Previous in the Series
Current Tutorial
Creating an Interceptor with Annotations

Previous in the Series: Reading Annotations

Next in the Series: Creating a Dependency Injection Framework

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.

  1. You need a way to call the message() method of your SomeInterceptedService class.
  2. When you call it, you expect your implementation of the intercept() method from your MessageInterceptor class to be called before the message() method is called.
  3. 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:

  1. The method itself, in the form of a Method object. Note that you could also pass the name of this method.
  2. The object on which this method is invoked. This is actually your service.
  3. 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


Previous in the Series
Current Tutorial
Creating an Interceptor with Annotations

Previous in the Series: Reading Annotations

Next in the Series: Creating a Dependency Injection Framework