Skip to content
Chapter 18 · Spring Boot

Custom Interceptors and Custom Annotations ​

This chapter starts the module on requests and responses. Its first tool is the interceptor: in simple words, a mediator that runs before or after your actual code. Later features (logging, caching, authentication and authorization) are built from interceptors, so it is worth knowing how to write your own.

There are two places you may want to intercept:

  1. Before a request reaches a controller: with a Spring MVC HandlerInterceptor.
  2. Around a specific method, after the controller has been called: with AOP (chapter 12) and an annotation of your own that marks the methods.

Part 1: intercepting before the controller ​

Recall the request flow from chapter 2: the servlet container (Tomcat) passes every request to the DispatcherServlet, which picks the controller method through handler mapping and calls it. A HandlerInterceptor plugs in between the DispatcherServlet and the controller:

Step 1: the controller ​

java
@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/getUser")
    public String getUser() {
        System.out.println("fetching the user details");
        return "user details";
    }
}

Step 2: the interceptor ​

Implement HandlerInterceptor and override its three methods:

java
@Component
public class MyCustomInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) {
        System.out.println("inside pre handle method");
        return true;                    // true: continue to the controller
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) {
        System.out.println("inside post handle method");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, Exception ex) {
        System.out.println("inside after completion method");
    }
}
MethodRuns
preHandlebefore the controller method. Returning false stops the request there.
postHandleafter the controller method, only if it succeeded
afterCompletionat the very end, always, even after an exception (like finally)

Step 3: register it, and choose its URLs ​

A @Component alone does not make an interceptor run. Register it in a configuration class that implements WebMvcConfigurer:

java
@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Autowired
    MyCustomInterceptor myCustomInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(myCustomInterceptor)
                .addPathPatterns("/api/*")                                 // apply to these URLs
                .excludePathPatterns("/api/updateUser", "/api/deleteUser"); // except these
    }
}
  • addPathPatterns: the URL patterns the interceptor applies to. /api/* matches /api/getUser, /api/anything. (/api/** would also match deeper paths such as /api/a/b.)
  • excludePathPatterns: URLs to leave out, even though they match.

(MyCustomInterceptor is a @Component, so it is a bean and can be @Autowired here. Without @Component, create it with new or an @Bean method.)

Step 4: run it ​

GET /api/getUser:

text
inside pre handle method
fetching the user details
inside post handle method
inside after completion method

GET /api/updateUser (excluded) prints only the controller's line: the interceptor did not run.

Where these calls come from ​

Inside Spring's DispatcherServlet.doDispatch(), simplified:

java
if (!mappedHandler.applyPreHandle(request, response)) {   // your preHandle
    return;                                                // false: stop here
}
mv = ha.handle(request, response, handler);                // YOUR CONTROLLER METHOD
mappedHandler.applyPostHandle(request, response, mv);      // your postHandle
...
processDispatchResult(...);                                // ends with your afterCompletion
// and on an exception: triggerAfterCompletion(..., ex)

postHandle vs afterCompletion ​

Both run after the controller. The difference shows when the controller throws:

java
@GetMapping("/fail")
public String fail() {
    throw new IllegalStateException("boom");
}
text
inside pre handle method
inside after completion method, ex = java.lang.IllegalStateException: boom

postHandle was skipped; afterCompletion ran and received the exception (ex is null on success). So put clean-up and timing in afterCompletion, where it always runs.

preHandle returning false ​

If preHandle decides the request must not go on (for example, no valid token), it sets the response and returns false:

java
if (request.getRequestURI().endsWith("/blocked")) {
    response.setStatus(403);
    return false;
}
text
inside pre handle method

The client gets 403. Nothing else ran: no controller, no postHandle, no afterCompletion for this interceptor. This is the shape of an authentication check.

Part 2: intercepting a specific method with a custom annotation ​

Sometimes you want to intercept not "every request to /api/*" but specific methods, anywhere in the code, called after the controller. With AOP that is easy if the methods carry a marker: your own annotation. So first, how to write annotations.

Step 1: create a custom annotation ​

An annotation is declared with @interface:

java
public @interface MyCustomAnnotation {
}

and used like any annotation:

java
@MyCustomAnnotation
public void getUser() { ... }

On its own it does nothing; it is a label that other code can look for. Two meta-annotations (annotations placed on an annotation) decide how it can be used.

@Target: where it may be used ​

java
@Target(ElementType.METHOD)                         // only on methods
public @interface MyCustomAnnotation { }

@Target({ElementType.METHOD, ElementType.TYPE,      // several places
         ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.PARAMETER})
public @interface MyOtherAnnotation { }

Using the annotation anywhere else is a compile error.

@Retention: how long it is kept ​

PolicyKept inReadable at run time?
RetentionPolicy.SOURCEthe source code only; the compiler discards itno
RetentionPolicy.CLASSthe .class file too, but the JVM ignores itno
RetentionPolicy.RUNTIMEthe .class file, and loaded by the JVMyes, through reflection

@Override is a SOURCE annotation: it only helps the compiler check your code and is not even in the .class file. An annotation that a framework reads while the application runs, like ours, must be RUNTIME.

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomAnnotation { }

Fields: passing information through the annotation ​

An interceptor usually needs some information from the annotated method (a cache name, a log level). Annotations carry it in fields, written as methods with no parameters:

java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomAnnotation {
    String name() default "";          // a field, with a default value
}
java
@MyCustomAnnotation(name = "user")
public String getUser() { ... }

The return types are restricted to keep annotations lightweight: the eight primitives (int, boolean, double, ...), String, enums, Class (such as String.class), other annotations, and arrays of these. A field may have a default; a field without one must be given a value wherever the annotation is used.

java
public @interface Example {
    int intKey() default 0;
    String stringKey() default "defaultString";
    Class<?> classTypeKey() default String.class;
    MyEnum enumKey() default MyEnum.FIRST;
    String[] stringArrayKey() default {"first", "second"};
}

Step 2: mark the method ​

java
@Component
public class User {

    @MyCustomAnnotation(name = "user")
    public String getUser() {
        System.out.println("get the user details");
        return "user details";
    }
}

The controller calls it, from another bean (AOP only intercepts calls that go through the proxy, chapter 12):

java
@GetMapping("/getUser")
public String getUser() {
    System.out.println("fetching the user details");
    return user.getUser();
}

Step 3: the AOP interceptor ​

An aspect whose pointcut is @annotation(...): any method carrying this annotation (chapter 12):

java
@Aspect
@Component
public class MyCustomInterceptorAop {

    @Around("@annotation(com.example.demo.MyCustomAnnotation)")
    public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("do something before actual method");

        // the intercepted method, and the annotation on it
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
            MyCustomAnnotation annotation = method.getAnnotation(MyCustomAnnotation.class);
            System.out.println("name from annotation: " + annotation.name());
        }

        Object result = joinPoint.proceed();            // the actual method runs here
        System.out.println("do something after actual method");
        return result;
    }
}

From the join point you get the intercepted Method, and from it, through reflection, the annotation and its fields. This is why the annotation needs RetentionPolicy.RUNTIME: with SOURCE or CLASS, method.getAnnotation(...) would find nothing.

Step 4: run it ​

With both parts in place, GET /api/getUser shows the two interceptors nested:

text
inside pre handle method                 ← HandlerInterceptor (part 1)
fetching the user details                ← controller
do something before actual method        ← AOP interceptor (part 2)
name from annotation: user
get the user details                     ← the annotated method
do something after actual method
inside post handle method                ← HandlerInterceptor
inside after completion method, ex = null

Which to use ​

HandlerInterceptorAOP + custom annotation
InterceptsHTTP requests, before/after the controllerany bean method carrying the annotation
Chosen byURL patternsthe annotation
Can stop the requestyes, preHandle returns falseyes, by not calling proceed()
Typical useauthentication, request logging and timingcaching, auditing or logging specific methods

Next: Interceptors are not the only way to act on a request. Servlet filters run even earlier, before the request reaches Spring at all. Chapter 19 compares the two and follows a request through both.

Check yourself ​

  1. What is an interceptor, and what are the two kinds in this chapter?
  2. Where does a HandlerInterceptor sit in the request flow?
  3. Name the three HandlerInterceptor methods and when each runs.
  4. Is a @Component interceptor applied automatically? How do you apply it to /api/* except /api/updateUser?
  5. The controller throws an exception. Which of postHandle and afterCompletion runs, and what does afterCompletion receive?
  6. What happens when preHandle returns false?
  7. How do you declare an annotation? What do @Target and @Retention control?
  8. Which retention does an annotation read by an aspect need? What would happen with CLASS?
  9. Which return types are allowed for annotation fields?
  10. How does the aspect find the methods to intercept, and how does it read name?
  11. Why must the annotated method be called from another bean?
Answers
  1. A mediator that runs before or after your actual code. A HandlerInterceptor (around controllers) and an AOP aspect triggered by a custom annotation (around specific methods).
  2. Between the DispatcherServlet and the controller method.
  3. preHandle before the controller; postHandle after it, only on success; afterCompletion at the end, always.
  4. No. Register it in a WebMvcConfigurer's addInterceptors: registry.addInterceptor(i).addPathPatterns("/api/*").excludePathPatterns("/api/updateUser").
  5. Only afterCompletion, which receives the exception as its ex parameter (null on success).
  6. The request stops: no controller, no postHandle, no afterCompletion for that interceptor; the response is whatever preHandle set.
  7. With @interface. @Target: where the annotation may be used. @Retention: how long it is kept (source, class file, or run time).
  8. RUNTIME. With CLASS, the annotation is in the class file but not loaded by the JVM, so getAnnotation returns null.
  9. Primitives, String, enums, Class, annotations, and arrays of these.
  10. The pointcut @annotation(...MyCustomAnnotation). In the advice, get the Method from the MethodSignature and call getAnnotation(MyCustomAnnotation.class).name().
  11. AOP works through a proxy; a call within the same class goes to this and bypasses it.