Enterprise Java

Implementing and Testing a Spring MVC HandlerInterceptor

In a Spring MVC application, interceptors provide a powerful mechanism to execute custom logic before and after processing HTTP requests. They are commonly used for cross-cutting concerns such as logging, authentication, authorization, request validation, performance monitoring, and auditing. Spring MVC provides the HandlerInterceptor interface to intercept incoming HTTP requests handled by Spring MVC controllers. Unlike servlet filters, interceptors work at the Spring MVC layer and have access to handler information such as the controller method being executed. Testing interceptors is important because interceptor logic often controls application behavior. For example, an authentication interceptor can prevent unauthorized users from accessing APIs. Using Spring Boot testing support with MockMvc, we can verify whether our interceptor executes correctly for different scenarios.

1. Understanding the HandlerInterceptor Interface

The HandlerInterceptor interface belongs to the org.springframework.web.servlet package and defines callback methods that are executed during the request processing lifecycle. The interface provides three main methods:

public interface HandlerInterceptor {

    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
      }

    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}

    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
}

Each of the three default methods on HandlerInterceptor corresponds to a distinct point in the request lifecycle: preHandle() executes before the controller method is invoked and is commonly used for authentication checks, request validation, or logging, where returning true allows the request to continue while returning false stops further processing; postHandle() executes after the controller completes successfully but before the response is sent back to the client; and afterCompletion() executes after the complete request lifecycle finishes, making it useful for cleanup activities and final logging.

2. Implementing a Request-Logging Interceptor

Let’s create a Spring Boot application that contains a custom interceptor which logs incoming requests and measures execution time.

2.1 Add the Required Maven Dependencies

Before implementing the interceptor, we need the core Spring Boot dependencies for building and testing a web application.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

The spring-boot-starter-web dependency brings in Spring MVC along with an embedded Tomcat server so the application can handle HTTP requests, while spring-boot-starter-test provides the testing libraries (JUnit, Mockito, and Spring Test) needed to verify the interceptor’s behavior later on.

2.2 Implementing the LoggingInterceptor Class

First, create a custom interceptor by implementing the HandlerInterceptor interface.

package com.example.demo.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class LoggingInterceptor implements HandlerInterceptor {

  @Override
  public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    long startTime = System.currentTimeMillis();
    request.setAttribute("startTime", startTime);
    
    System.out.println("Incoming Request : " + request.getRequestURI());
    return true;
  }

  @Override
  public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, org.springframework.web.servlet.ModelAndView modelAndView) {
    System.out.println("Controller execution completed");
  }

  @Override
  public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
    long startTime = (Long) request.getAttribute("startTime");
    long executionTime = System.currentTimeMillis() - startTime;

    System.out.println("Request completed in " + executionTime + " ms");
  }
}

This LoggingInterceptor class hooks into the three lifecycle stages of a request: preHandle() runs before the controller method executes and records the current timestamp as a request attribute while logging the incoming URI, postHandle() runs right after the controller finishes but before the response is rendered, and afterCompletion() runs once the entire request-response cycle is done, retrieving the stored start time to calculate and log the total execution time.

2.3 Registering the Interceptor with WebMvcConfigurer

Spring MVC needs to know which requests should use the interceptor. We register it using WebMvcConfigurer.

package com.example.demo.config;

import com.example.demo.interceptor.LoggingInterceptor;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

  private final LoggingInterceptor interceptor;

  public WebConfig(LoggingInterceptor interceptor) {
    this.interceptor = interceptor;
  }

  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(interceptor).addPathPatterns("/employees/**");
  }
}

The WebConfig class implements WebMvcConfigurer and overrides addInterceptors() to register the injected LoggingInterceptor bean with Spring’s InterceptorRegistry, restricting its execution to only those requests matching the /employees/** path pattern.

2.4 Building the REST Controller

Finally, let’s expose a simple REST endpoint that the interceptor will be triggered for.

package com.example.demo.controller;

import org.springframework.web.bind.annotation. * ;

@RestController@RequestMapping("/employees")
public class EmployeeController {

  @GetMapping("/{id}")
  public String getEmployee(@PathVariable Integer id) {
    return "Employee ID : " + id;
  }
}

This EmployeeController class is annotated with @RestController and mapped to the /employees base path via @RequestMapping, exposing a single GET /employees/{id} endpoint that extracts the id path variable and returns it as part of a plain text response, which will trigger the LoggingInterceptor registered against the same path pattern.

2.5 Code Run and Output

Now let’s run the application and hit the endpoint with curl to see the interceptor in action.

$ curl http://localhost:8080/employees/101
Employee ID : 101

The curl command sends a GET request to the /employees/101 endpoint, and the server responds with the plain text body Employee ID : 101 produced by the EmployeeController.

Incoming Request : /employees/101
Controller execution completed
Request completed in 4 ms

This is the console output logged by the LoggingInterceptor for the same request: preHandle() prints the incoming request URI before the controller runs, postHandle() confirms the controller has finished executing, and afterCompletion() prints the total time taken to process the request once the response has been fully sent.

3. Verifying Interceptor Behavior with MockMvc

Since interceptor logic can influence whether a request is even allowed to reach the controller, it’s important to verify it behaves correctly. Spring Boot’s @WebMvcTest slice lets us test the EmployeeController and LoggingInterceptor together, using MockMvc to simulate HTTP requests without starting a real server.

package com.example.demo.controller;

import com.example.demo.config.WebConfig;
import com.example.demo.interceptor.LoggingInterceptor;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(EmployeeController.class)
@Import(WebConfig.class)
class EmployeeControllerInterceptorTest {

  @Autowired
  private MockMvc mockMvc;

  @SpyBean
  private LoggingInterceptor loggingInterceptor;

  @Test
  void whenGetEmployee_thenInterceptorRunsAndControllerRespondsSuccessfully() throws Exception {
    mockMvc.perform(get("/employees/101"))
      .andExpect(status().isOk())
      .andExpect(content().string("Employee ID : 101"));

    verify(loggingInterceptor).preHandle(any(), any(), any());
    verify(loggingInterceptor).postHandle(any(), any(), any(), any());
    verify(loggingInterceptor).afterCompletion(any(), any(), any(), any());
  }
}

The @WebMvcTest(EmployeeController.class) annotation loads only the web layer needed for this controller, and @Import(WebConfig.class) pulls in the interceptor registration so it participates in the simulated request; @SpyBean wraps the real LoggingInterceptor bean so its actual logic still executes while allowing Mockito to record and verify each call. The test then performs a GET request through MockMvc, asserts the controller returned 200 OK with the expected body, and finally uses verify() to confirm that preHandle(), postHandle(), and afterCompletion() were each invoked exactly once during that request lifecycle.

Because @SpyBean lets the real LoggingInterceptor logic run, its System.out.println statements still fire during the simulated MockMvc request, so the console shows the same log lines as the live application did in Section 2.5.

Incoming Request : /employees/101
Controller execution completed
Request completed in 2 ms

This confirms that even inside the @WebMvcTest slice, the interceptor’s preHandle(), postHandle(), and afterCompletion() methods executed against the mocked request exactly as they would in production, just without a running server.

$ mvn -Dtest=EmployeeControllerInterceptorTest test

[INFO] Running com.example.demo.controller.EmployeeControllerInterceptorTest
Incoming Request : /employees/101
Controller execution completed
Request completed in 2 ms
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] BUILD SUCCESS

Running the test class with Maven shows the same interceptor log lines interleaved with the JUnit test lifecycle, followed by Surefire’s summary confirming 1 test ran with no failures or errors, and the overall build reporting BUILD SUCCESS.

4. Conclusion

The Spring MVC HandlerInterceptor provides a clean way to implement request-level cross-cutting functionality. It executes within the Spring MVC lifecycle and provides access to HTTP requests, responses, and controller handlers. Using Spring Boot’s MockMvc, developers can easily test interceptor behavior without starting a real web server. Proper interceptor testing ensures that important application features such as security checks, logging, auditing, and request validation work correctly. Understanding how to implement and test HandlerInterceptor is an essential skill for building maintainable and production-ready Spring Boot applications.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button