Spring Boot Web - Interceptor

Seunghwan Choi·2025년 8월 17일

Java Backend

목록 보기
14/16

Spring HandlerInterceptor intercepts HTTP requests at the controller level, i.e., after Spring has matched the request to a specific handler (controller method). Interceptors are mainly used for cross-cutting concerns like logging, authentication, metrics, etc., without touching the servlet layer directly

Key methods in HandlerInterceptor

  • preHandle(HttpServletRequest, HttpServletResponse, Object Handler):
    - When it runs: Before the controller method executes
    • Purpose: Can stop request processing (return false), log, authenticate, or modify the request
  • postHandle(HttpServletRequest, HttpServletResponse, Object Handler, ModelAndView):
    - When it runs: After the controller method executes, but before view rendering
    • Purpose: Can modify the ModelAndView or add attributes
  • afterCompletion(HttpServletRequest, HttpServletResponse, Object Handler, Exception):
    - When it runs: After the complete request is finished (response sent)
    • Purpose: Cleanup, logging, metrics, exception handling
@Slf4j
@Component
public class OpenApiInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("pre handle");
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("post Handle");
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("after completion");
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
  • Despite the above interceptor classes added, it will not log anything. Simply annotating our class with @Component does not automatically attach it to Spring MVC's request handling.
    - Spring detects HandlerInterceptor beans, but they do nothing until we register them via a configuration.
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private OpenApiInterceptor openApiInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(openApiInterceptor)
                .addPathPatterns("/**");
    }
}
  • @Configuration marks this class as a Spring configuration class. Spring will detect it and use it to configure MVC settings.
  • addInterceptors() is called by Spring MVC to register any interceptors.
  • registry.addInterceptor(openApiInterceptor) addes my OpenApiInterceptor to the interceptor chain.
  • .addPathPatterns("/**) tells Spring to apply this interceptor to all paths, but we can also limit it so specific paths, e.g., /api/**.
  • Now calling the POST method again after adding the configuration file, we get all the log() from the OpenApiInterceptor.

Overall flow

Incoming HTTP Request
         |
         v
+----------------------+
|   Servlet Filter(s)  |  <- e.g., LoggerFilter
|  (implements Filter) |
+----------------------+
         |
         v
+---------------------------+
|  DispatcherServlet        |
|  (Spring MVC front controller) |
+---------------------------+
         |
         v
+---------------------------+
|   HandlerInterceptor(s)   |  <- e.g., OpenApiInterceptor
|   preHandle()             |
+---------------------------+
         |
         v
+---------------------------+
|       Controller          |  <- e.g., UserApiController
+---------------------------+
         |
         v
+---------------------------+
| HandlerInterceptor(s)     |
| postHandle()              |
+---------------------------+
         |
         v
+---------------------------+
|  View Rendering / Response | (if any)
| afterCompletion()          | <- HandlerInterceptor
+---------------------------+
         |
         v
+----------------------+
|   Servlet Filter(s)  |  <- LoggerFilter can do post-processing here too
+----------------------+
         |
         v
   HTTP Response sent to client
  • This is in line with the below diagram:
  • With the POST request, the log is as below:
2025-08-19T00:17:38.895+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : before entry //filter
2025-08-19T00:17:38.912+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : pre handle //interceptor
2025-08-19T00:17:38.999+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] c.e.filter.controller.UserApiController  : UserRequest(name=Choi, phoneNumber=null, email=choi@gmail.com, age=45) //Controller
2025-08-19T00:17:39.028+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : post Handle //Interceptor
2025-08-19T00:17:39.029+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : after completion //Interceptor
2025-08-19T00:17:39.030+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : req : {
  "name": "Choi",
  "phoneNumber": "010-2321-2321",
  "email": "choi@gmail.com",
  "age": 45
} //Filter
2025-08-19T00:17:39.030+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : res : {"name":"Choi","phone_number":null,"email":"choi@gmail.com","age":45}
2025-08-19T00:17:39.030+08:00  INFO 22757 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : after entry

Application

  • We first create a marker annotation for demo purposes:
@Target(value = { ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface OpenApi {
}
  • @Target(value = {ElementType.METHOD, ElementType.TYPE})
    - Defines where this annotation can be applied.
    • ElementType.TYPE can be placed on classes, interfaces, enums
    • ElementType.METHOD can be placed on methods
  • @Retention(RetentionPolicy.RUNTIME)
    - Defines how long the annotation information is kept.
    • RUNTIME -> the annotation will be available runtime, and reflection (e.g. Spring interceptors, AOP, or custom code) can read it.
    • This means frameworks like Spring can detect @OpenApi annotation during execution
  • We modify the preHandle() method in our interceptor class as below:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    log.info("pre handle");

    var handlerMethod = (HandlerMethod) handler;

    // Check if the method itself has @OpenApi
    var methodLevel = handlerMethod.getMethodAnnotation(OpenApi.class);
    if(methodLevel != null){
    	log.info("method level");
        return true; // allow request to proceed
    }

    // Check if the class has @OpenApi
    var classLevel = handlerMethod.getBeanType().getAnnotation(OpenApi.class);
    if(classLevel != null){
    	log.info("class level");
        return true; // allow request to proceed
    }
	
    log.info("not @OpenApi : {}", request.getRequestURI());
    return false; // block request (controller method will not run)
}
  • In the line var handlerMethod = (HandlerMethod) handler;, what is a handler here?
    - In a Spring HandlerInterceptor, the preHandle method signature is:
    boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 
    • The handler object represents the thing that will handle the reqeust.
    • In SpringMVC, if the request is mapped to a controller method, handler will be of type HandlerMethod.
      • Why cast to HandlerMethod?
      • We want to inspect annotations on the controller class/method.
        • To do this, we need Spring's HandlerMethod API, which gives us reflective access.
    • HandlerMethod provides methods like:
      getMethod()              // the actual java.lang.reflect.Method
      			getBeanType()            // the controller's class
      			getMethodAnnotation(...) // check if a method has a certain annotation
      			getBean()                // the actual controller object
    • Without casting, we can't call these methods on handler (since it's just an Object by default).
  • Also the modified UserApiController:
public class UserApiController {

    @OpenApi
    @PostMapping("")
    public UserRequest register(
            @RequestBody
            UserRequest userRequest
    ){
        log.info("{}", userRequest);
        return userRequest;
    }

    @GetMapping("/hello")
    public void hello(){
        log.info("Hello");
    }
}
  • Once we send a request to /hello endpoint, we get the following log:
2025-08-19T00:46:39.377+08:00  INFO 24943 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : before entry
2025-08-19T00:46:39.396+08:00  INFO 24943 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : pre handle
2025-08-19T00:46:39.397+08:00  INFO 24943 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : not @OpenApi : /api/user/hello
2025-08-19T00:46:39.399+08:00  INFO 24943 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : req : 
2025-08-19T00:46:39.399+08:00  INFO 24943 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : res : 
2025-08-19T00:46:39.399+08:00  INFO 24943 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : after entry
  • And since the hello() method mapped to /hello endpoint was not annotated with @OpenApi and the UserApiController class was not annotated with @OpenApi, the preHandle() method returned false and the request was not proceeded.
  • But if we annotate the UserApiController class with @OpenApi annotation, all methods (including hello()) method will have the annotation, therefore the request will be proceeded, as shown below:
@OpenApi
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserApiController {

    @OpenApi
    @PostMapping("")
    public UserRequest register(
            @RequestBody
            UserRequest userRequest
    ){
        log.info("{}", userRequest);
        return userRequest;
    }

    @GetMapping("/hello")
    public void hello(){
        log.info("Hello");
    }
}
2025-08-19T00:52:01.599+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : before entry
2025-08-19T00:52:01.620+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : pre handle
2025-08-19T00:52:01.621+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : class level
2025-08-19T00:52:01.637+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] c.e.filter.controller.UserApiController  : Hello
2025-08-19T00:52:01.656+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : post Handle
2025-08-19T00:52:01.656+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] c.e.f.interceptor.OpenApiInterceptor     : after completion
2025-08-19T00:52:01.657+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : req : 
2025-08-19T00:52:01.659+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : res : 
2025-08-19T00:52:01.659+08:00  INFO 25315 --- [filter] [nio-8080-exec-1] com.example.filter.filter.LoggerFilter   : after entry

When we have multiple interceptors:

  • In WebConfig configuration:
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private OpenApiInterceptor openApiInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(openApiInterceptor)
                .addPathPatterns("/**");
        
        //we can add more interceptor(s)
        registry.addInterceptor(someOtherInterceptor)
                .addPathPatterns("/**");
    }
}
  • And the interceptors will run based on the order of addition to registry. OR we can order them using .order().

0개의 댓글