Appearance
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:
- Before a request reaches a controller: with a Spring MVC
HandlerInterceptor. - 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");
}
}| Method | Runs |
|---|---|
preHandle | before the controller method. Returning false stops the request there. |
postHandle | after the controller method, only if it succeeded |
afterCompletion | at 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 methodGET /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: boompostHandle 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 methodThe 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
| Policy | Kept in | Readable at run time? |
|---|---|---|
RetentionPolicy.SOURCE | the source code only; the compiler discards it | no |
RetentionPolicy.CLASS | the .class file too, but the JVM ignores it | no |
RetentionPolicy.RUNTIME | the .class file, and loaded by the JVM | yes, 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 = nullWhich to use
HandlerInterceptor | AOP + custom annotation | |
|---|---|---|
| Intercepts | HTTP requests, before/after the controller | any bean method carrying the annotation |
| Chosen by | URL patterns | the annotation |
| Can stop the request | yes, preHandle returns false | yes, by not calling proceed() |
| Typical use | authentication, request logging and timing | caching, 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
- What is an interceptor, and what are the two kinds in this chapter?
- Where does a
HandlerInterceptorsit in the request flow? - Name the three
HandlerInterceptormethods and when each runs. - Is a
@Componentinterceptor applied automatically? How do you apply it to/api/*except/api/updateUser? - The controller throws an exception. Which of
postHandleandafterCompletionruns, and what doesafterCompletionreceive? - What happens when
preHandlereturnsfalse? - How do you declare an annotation? What do
@Targetand@Retentioncontrol? - Which retention does an annotation read by an aspect need? What would happen with
CLASS? - Which return types are allowed for annotation fields?
- How does the aspect find the methods to intercept, and how does it read
name? - Why must the annotated method be called from another bean?
Answers
- 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). - Between the
DispatcherServletand the controller method. preHandlebefore the controller;postHandleafter it, only on success;afterCompletionat the end, always.- No. Register it in a
WebMvcConfigurer'saddInterceptors:registry.addInterceptor(i).addPathPatterns("/api/*").excludePathPatterns("/api/updateUser"). - Only
afterCompletion, which receives the exception as itsexparameter (nullon success). - The request stops: no controller, no
postHandle, noafterCompletionfor that interceptor; the response is whateverpreHandleset. - With
@interface.@Target: where the annotation may be used.@Retention: how long it is kept (source, class file, or run time). RUNTIME. WithCLASS, the annotation is in the class file but not loaded by the JVM, sogetAnnotationreturnsnull.- Primitives,
String, enums,Class, annotations, and arrays of these. - The pointcut
@annotation(...MyCustomAnnotation). In the advice, get theMethodfrom theMethodSignatureand callgetAnnotation(MyCustomAnnotation.class).name(). - AOP works through a proxy; a call within the same class goes to
thisand bypasses it.