Mastering Unified Login, Interceptors, and Exception Handling in Spring Boot
This tutorial walks through implementing unified user login verification, request interception, global exception handling, and a consistent response format in Spring Boot using AOP, HandlerInterceptor, @ControllerAdvice, and ResponseBodyAdvice, complete with code examples and architectural explanations.
Introduction
The following module introduces the unified functionality handling in Spring Boot, focusing on three goals: unified user login permission verification, unified data format return, and unified exception handling.
1. User Login Permission Verification
Initially, each controller method performed its own login check, leading to duplicated code, higher maintenance cost, and no clear separation from business logic. A common AOP solution was considered, but it cannot access HttpSession and cannot easily exclude specific endpoints such as login and registration.
1.1 Original Login Verification
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/m1")
public Object method(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
return true; // logged in
} else {
return false; // not logged in
}
}
@RequestMapping("/m2")
public Object method2(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
return true;
} else {
return false;
}
}
// other methods ...
}Problems of this approach:
Every method must repeat the same login check, even if extracted to a utility method.
Adding more controllers increases the number of calls to the login check, raising maintenance cost.
The login logic is unrelated to the actual business logic of each method.
Therefore, a common AOP method is needed.
1.2 Spring AOP Login Verification
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserAspect {
@Pointcut("execution(* com.example.demo.controller..*.*(..))")
public void pointcut() {}
@Before("pointcut()")
public void doBefore() {}
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
System.out.println("Around method start");
try {
obj = joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("Around method end");
return obj;
}
}The AOP approach faces two issues:
HttpSession cannot be obtained inside the aspect.
Excluding specific methods (e.g., login, registration) from interception is difficult.
1.3 Spring Interceptor Solution
Spring provides HandlerInterceptor to solve the above problems.
Create a custom interceptor that implements the preHandle method for login checks.
Register the interceptor in WebMvcConfigurer via addInterceptors.
package com.example.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/** Login interceptor */
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null && session.getAttribute("userinfo") != null) {
return true; // logged in
}
log.error("Current user has no access permission");
response.setStatus(401);
return false; // block request
}
} package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
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 MyConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // intercept all requests
.excludePathPatterns("/user/login", "/user/reg"); // exclude login & registration
}
}Key points: addPathPatterns defines URLs to intercept ("/**" means all). excludePathPatterns defines URLs to skip.
1.4 Interceptor Implementation Principle
All controller calls go through DispatcherServlet. During doDispatch, applyPreHandle invokes each registered HandlerInterceptor 's preHandle method before the controller logic runs.
After the pre‑handle step, the controller method executes, then post‑handle and after‑completion steps follow.
1.5 Extension: Adding a Global API Prefix
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("api", c -> true); // enable prefix for all controllers
}
}2. Unified Exception Handling
Use @ControllerAdvice together with @ExceptionHandler to catch exceptions globally.
package com.example.demo.config;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
/** Global exception handler */
@ControllerAdvice
public class ErrorAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public HashMap<String, Object> exceptionAdvice(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", "-1");
result.put("msg", e.getMessage());
return result;
}
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public HashMap<String, Object> arithmeticAdvice(ArithmeticException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", "-2");
result.put("msg", e.getMessage());
return result;
}
}When an exception occurs, the corresponding method returns a HashMap with a custom error code and message. Multiple handlers are matched from the most specific to the most general.
3. Unified Data Return Format
Two approaches are shown:
3.1 Using @ControllerAdvice + ResponseBodyAdvice
package com.example.demo.config;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.HashMap;
/** Global response wrapper */
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true; // apply to all responses
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
HashMap<String, Object> result = new HashMap<>();
result.put("state", 1);
result.put("msg", "");
result.put("data", body);
return result;
}
}This wrapper forces every response into a JSON object with state, msg, and data. When the controller returns a String, the wrapper would cause a conversion error; the article explains the cause and a workaround.
3.2 Preferred Soft Constraint: Custom Result Class
Define a utility class AjaxResult with static methods success and fail that build the standard map. Controllers return AjaxResult.success(data) or AjaxResult.fail(code, msg) explicitly.
package com.example.demo.common;
import java.util.HashMap;
/** Unified response object */
public class AjaxResult {
public static HashMap<String, Object> success(Object data) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "");
result.put("data", data);
return result;
}
public static HashMap<String, Object> success(String msg, Object data) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", msg);
result.put("data", data);
return result;
}
public static HashMap<String, Object> fail(int code, String msg) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", code);
result.put("msg", msg);
result.put("data", "");
return result;
}
public static HashMap<String, Object> fail(int code, String msg, Object data) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", code);
result.put("msg", msg);
result.put("data", data);
return result;
}
}Controllers can then use AjaxResult.success(...) or AjaxResult.fail(...) to produce a consistent JSON structure without forcing a global wrapper.
3.3 Source Code Insight of @ControllerAdvice
The article briefly explores the internal Spring MVC flow: @ControllerAdvice is a @Component that gets registered during RequestMappingHandlerAdapter.afterPropertiesSet(), which builds a cache of advice beans. When an exception occurs or a response is about to be written, the corresponding advice methods are invoked.
Overall, the article demonstrates how to achieve a clean, maintainable backend architecture by centralizing authentication, error handling, and response formatting in Spring Boot.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
