Exploring Interceptors and Custom Annotations in Spring Boot with a Practical Scenario

Rakib Ahmed
7 min readOct 14, 2023

--

In this article, we will delve into the effective utilization of Interceptors and Custom Annotations within a Spring Boot application. As always I try to explain any topic using a practical scenario, this time I will not be any different.

Let’s start by outlining the scenario that will serve as the backdrop for our discussion. Imagine you are tasked with constructing a service or solution that necessitates the integration of multiple secured REST APIs from a third-party source. Among all of the APIs offered by this third-party service, there exists a crucial Authentication API responsible for authenticating your service. This Authentication API expects to receive a user ID and password, subsequently responding with a validation token and its expiration time upon successful validation. Notably, this token is a prerequisite for all other interactions with the third-party service, as it must be included in the request header.

In our application, we will employ an Interceptor to trigger the authentication process by calling the previously mentioned third-party Authentication API before initiating any other API request to that external service. We will then create a custom annotation to conveniently access and manage this token within our controller.

Let’s start by adding all the required dependencies to our project pom.xml file.

Pom.xml Dependencies:

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

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

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

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

well, there are many options available for performing API calls such as Rest Template, Web Client, and Feign Client. You can choose according to your needs and requirements.

Now, before starting with how we can use interceptor in Spring Boot we need to know a little about it and what it does.!

Interceptor in Spring Boot:

In Spring Boot, an interceptor allows you to intercept and perform pre-processing or post-processing logic for HTTP requests before they reach a controller’s request mapping method or after they have been processed by the controller method. To create an interceptor in Spring Boot, we typically implement the “HandlerInterceptor” interface, which has methods like “preHandle”, “postHandle”, and “afterCompletion” that allow us to hook into the request processing flow.

  • The “preHandle” method is called before the actual handler method (controller method) is executed. It allows you to perform pre-processing logic for the request. If “preHandle” returns “true”, the request processing continues, and the handler method is called. If “preHandle” returns “false”, the request is halted, and the handler method is not executed.
  • The “postHandle” method is called after the handler method has been executed but before the view is rendered or the response is sent to the client. It allows you to perform post-processing logic and unlike “preHandle”, the return value of “postHandle” doesn’t affect the request processing flow.
  • The “afterCompletion” method is called after the response has been sent to the client, and any view rendering is complete. The method is called regardless of the outcome of the request.

In this article, we will work with the “preHandle” method as it best fits to achieve our requirements. So let’s start by writing our Interceptor class and its configuration.

GetAuthTokenInterceptor Class:

public class GetAuthTokenInterceptor implements HandlerInterceptor {

private final AuthTokenService authTokenService;

public GetAuthTokenInterceptor(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String authToken = authTokenService.getAuthToken();
if (authToken == null) {
// Token is null; return an error response
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // Set the HTTP status code to 401 Unauthorized
response.getWriter().write("Authentication token not found or generated"); // Set the response message
return false; // Stop further processing
}
request.setAttribute("authToken", authToken);
return true; // Continue processing for other requests
}
}

This “GetAuthTokenInterceptor” class is an implementation of the “HandlerInterceptor” interface now we can override the “preHandle” method according to our need. We want to get the valid authToken by calling the previously mentioned third-party Authentication API and we will do that by invoking the “getAuthToken” method of “AuthTokenService” class. It then checks whether authToken is null. If the authToken is null it returns an unauthorized HTTP response and stops processing the request otherwise it sets this authentication token as an attribute in the HttpServletRequest object using the “request.setAttribute” method. This allows other parts of the application, such as controllers, to access this token.

WebMvcConfig Class:

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

private final AuthTokenService authTokenService;

@Autowired
public WebMvcConfig(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GetAuthTokenInterceptor(authTokenService))
.addPathPatterns("/api/thirdPartyService1/**");
}
}

This “WebMvcConfig” class is annotated with @Configuration and @EnableWebMvc, indicating that it is a Spring configuration class responsible for configuring the Spring MVC framework in a Spring Boot application. It implements the “WebMvcConfigurer” interface, which allows for customizing the behavior of the MVC framework. The “addInterceptors” method is overridden to configure an instance of “GetAuthTokenInterceptor”. This interceptor is added to the “InterceptorRegistry” and is configured to apply only to URLs that match the pattern “/api/thirdPartyService1/**”. It has a constructor that takes an instance of “AuthTokenService” as a parameter, which is injected using the @Autowired annotation. This service is used to pass the authentication token to the interceptor.

Now Let's look at our AuthTokenService Class …..

AuthTokenService Class:

@Service
public class AuthTokenService {
@Autowired
public ThirdPartyAuthCredentialRepository thirdPartyAuthCredentialRepository;

@Value("${THIRD_PARTY_SERVICE_USERNAME}")
private String userName;

@Value("${THIRD_PARTY_SERVICE_PASSWORD}")
private String password;

public String getAuthToken(){
ThirdPartyAuthCredential credential = thirdPartyAuthCredentialRepository.getThirdPartyAuthCredentialByUserNameAndPassword(userName, password);

if(credential == null){
return null;
}
if(credential.getToken() == null || credential.getExpiration() == null || this.isTokenExpired(credential.getExpiration())){
return this.getNewToken(credential);
}else {
return credential.getToken();
}
}
}

getAuthToken” method of “AuthTokenService” class is responsible for providing the authToken. We may use our logic according to our requirements and the functionality of the third-party service APIs. In our case, the method performs the following tasks:

  • It checks whether a valid ThirdPartyAuthCredential exists in the database for a username and password.
  • If the credential doesn’t exist, it returns null, indicating that no valid token can be generated.
  • If a credential exists but either the token is null or has expired, it generates a new token using the getNewToken method. This getNewToken calls the third-party Authentication API and saves the authToken and expiration time in our local DB, so that we can reuse the token before it expires, without calling the Authentication API more frequently.
  • If a valid token exists, it returns the stored token.
  • isTokenExpired” method this method checks if a token has expired

Custom Annotation:

Now here at this point, we need to create our custom annotation. We had managed to access the authToken in the “preHandle” method but we could not return it to the controller or somewhere, that is why we set this authentication token as an attribute in the HttpServletRequest object. Now we will create a custom annotation, it then retrieves the custom value, which is set in the interceptor, from the HttpServletRequest using the key “authToken” and then uses them as method arguments in our controllers.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ThirdPartyAuthToken {
}

The “ThirdPartyAuthTokenArgumentResolver” class implements the “HandlerMethodArgumentResolver” interface, which allows custom handling of method arguments by overriding two methods “supportsParameter” and “resolveArgument”.

@Component
public class ThirdPartyAuthTokenArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(String.class) &&
parameter.hasParameterAnnotation(ThirdPartyAuthToken.class);
}

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
// Retrieve the custom value from wherever it was set in your interceptor
String authToken = (String) request.getAttribute("authToken");
return authToken;
}
}

The “supportsParameter” method is responsible for determining if the resolver can be applied to a specific method parameter. It checks if the parameter’s type is String, it also checks if the parameter has the @ThirdPartyAuthToken annotation. This part ensures that this resolver is applied only to method parameters annotated with @ThirdPartyAuthToken. If both conditions are met, it returns true, indicating that this resolver can handle the parameter.

The “resolveArgument” method is responsible for actually resolving the method argument. It provides the logic for extracting the value of the argument. This “resolveArgument” method retrieves the underlying HttpServletRequest object from the provided NativeWebRequest. The NativeWebRequest is a more abstract representation of a web request, and the HttpServletRequest is extracted from it. It then retrieves the custom value, from the HttpServletRequest using the key “authToken” which was set by our custom interceptor. Finally, it returns the retrieved authToken, which will be used as the value for the method argument of type String.

We are left with one last essential step to complete our custom annotation, and that is to register our custom argument resolver “ThirdPartyAuthTokenArgumentResolver” in the “WebMvcConfig” class.

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
//configuration for custom interceptor
private final AuthTokenService authTokenService;
@Autowired
public WebMvcConfig(AuthTokenService authTokenService) {
this.authTokenService = authTokenService;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new GetAuthTokenInterceptor(authTokenService))
.addPathPatterns("/api/thirdPartyService1/**");
}

// configuration for custom annotation
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new ThirdPartyAuthTokenArgumentResolver());
}
}

Finally, let's create our controller and test if we are getting the auth token from our custom annotation. we can use this token to perform other secured API calls by passing it to other services.

@RestController
public class TestController {
@GetMapping(value = "/api/thirdPartyService1/getProducts")
public String getProducts(@ThirdPartyAuthToken String authToken) {
return authToken;
}
}

Conclusion:

Please note that the approach presented in this article may not precisely align with your specific project context or requirements. It’s possible that a more straightforward approach could have sufficed. However, the primary objective here was to provide an in-depth exploration of how to effectively utilize interceptors and custom annotations. The concepts are explained in a simplified manner, with some deviation from industry standards. I trust that you found this article informative and enjoyable to read.

--

--