【Java EE】SpringBoot 统一功能处理
后端开发中,我们总会遇到三类重复通用需求:
- 登录校验、权限拦截,每个接口重复写 Session 判断,代码冗余难维护;
- 接口返回格式五花八门,有的返回对象、有的返回布尔、有的返回字符串,前端解析成本高;
- 代码空指针、运算异常直接抛出原生报错,页面展示杂乱,无法统一提示。
SpringBoot 提供三套方案一次性解决以上问题:
- 拦截器统一鉴权
@ControllerAdvice + ResponseBodyAdvice统一返回格式@ControllerAdvice + @ExceptionHandler全局异常捕获。
一、SpringMVC 拦截器
1. 什么是拦截器
拦截器是 Spring 原生组件,在请求到达 Controller 前后 执行自定义通用逻辑,支持放行/阻断请求,适合登录校验、日志打印、请求耗时统计等全局操作。
简单类比:银行办业务,进门先取号校验身份证(preHandle 请求前),办完业务评价服务(postHandle 请求后),全部流程结束归档(afterCompletion)。
2. 拦截器三大方法
自定义拦截器需要实现 HandlerInterceptor 接口,重写三个生命周期方法:
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
// 1. Controller方法执行前,核心鉴权逻辑写这里
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("请求前置拦截:执行鉴权");
// true:放行,继续访问Controller;false:阻断请求,直接返回响应
return true;
}
// 2. Controller方法执行完成,视图渲染前
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("Controller执行完毕,视图渲染前");
}
// 3. 整个请求全部结束(视图渲染/响应返回完成),资源释放、日志收尾
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("请求完全结束");
}
}
preHandle 返回 false 时,后续 Controller、postHandle、afterCompletion 全部不会执行,直接中断请求。
3. 注册拦截器:WebMvc 配置
定义完拦截器后,需要实现 WebMvcConfigurer 完成注册,配置拦截/放行路径:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 拦截所有接口
// 放行登录接口、静态资源
.excludePathPatterns("/user/login", "/**/*.html", "/**/*.js", "/**/*.css", "/**/*.png");
}
}
路径匹配规则
| 匹配规则 | 含义 | 示例 |
|---|---|---|
/* | 一级路径 | 匹配 /login,不匹配 /user/login |
/** | 任意多级路径 | 匹配 /user、user/list、user/info/1 |
/book/* | book下一级接口 | 匹配 /book/add,不匹配 /book/info/1 |
/book/** | book下所有子接口 | 匹配所有 book 前缀接口 |
4. Session 登录校验拦截器
业务需求:未登录用户访问除登录外所有接口,返回 401 未认证状态码。
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("preHandle 目标方法执行前...");
//true 表示继续执行,false 表示中断执行
HttpSession session = request.getSession(false);
if(!checkUser(session)){
//字符串设置
//设置响应内容类型为UTF-8编码的HTML,防止返回中文提示时浏览器乱码
//如果是json数据则设置为application/json
response.setContentType("text/html;charset=utf-8");
//400-参数错误 401-未登录 403-没有权限
response.setStatus(401);
String msg = "用户未登录";
response.getOutputStream().write(msg.getBytes(StandardCharsets.UTF_8));
return false;
}
return true;
}
public boolean checkUser(HttpSession session){
if(session==null || session.getAttribute(Constants.USER_SESSION_KEY)==null){
log.warn("用户未登录");
return false;
}
UserInfo userInfo = (UserInfo)session.getAttribute(Constants.USER_SESSION_KEY);
if(userInfo.getId()<=0){
log.warn("用户ID无效");
return false;
}
return true;
}
测试:
- 未登录访问
/book/getList:接口返回 401; - 先调用登录接口存入 Session,再次访问图书接口:正常返回数据。
5. 拦截器底层
Servlet生命周期
public interface Servlet {
//启动服务时初始化
void init(ServletConfig config) throws ServletException;
ServletConfig getServletConfig();
//调用接口时会调用service方法,可能会调用多次
void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
String getServletInfo();
//关闭服务时销毁
void destroy();
}
DispatcherServlet doDispatch 源码
Tomcat启动后,DispatcherServlet来控制执行程序的顺序。

所有请求最终都会进入 SpringMVC 核心调度类 DispatcherServlet 的 doDispatch 方法
执行顺序:
- 根据 URL 匹配处理器执行链
HandlerExecutionChain(包含拦截器列表); - 循环执行所有拦截器
preHandle(),任意拦截器返回false,直接终止流程; preHandle全部放行后,执行 Controller 业务方法;- Controller 执行完毕,倒序执行
postHandle(); - 最后执行
afterCompletion(),统一处理异常与资源清理。
关键源码片段:
// 执行所有拦截器前置方法
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return; // 任意拦截器阻断,直接返回
}
// 执行Controller
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 执行后置拦截
mappedHandler.applyPostHandle(processedRequest, response, mv);
// 收尾,执行afterCompletion
this.processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
二、统一数据返回格式:ResponseBodyAdvice 全局包装
1. 为什么需要统一返回
项目中接口返回类型混乱,实体类、Boolean、String、数字,前端需要写多套解析逻辑,维护成本高。
统一格式后,所有响应结构标准化:
{
"status": "SUCCESS",
"errorMessage": "",
"data": { ... }
}
2. 基础统一返回实体 Result
import lombok.Data;
@Data
public class Result<T> {
private String status;
private String errorMessage;
private T data;
// 成功静态方法
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setStatus("SUCCESS");
result.setErrorMessage("");
result.setData(data);
return result;
}
// 失败静态方法
public static <T> Result<T> fail(String msg) {
Result<T> result = new Result<>();
result.setStatus("FAIL");
result.setErrorMessage(msg);
result.setData(null);
return result;
}
}
3. 全局响应包装
使用 @ControllerAdvice 扫描所有控制器,实现 ResponseBodyAdvice 对返回体统一封装:
@Slf4j
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
private static final ObjectMapper mapper = new ObjectMapper();
// 是否开启统一包装:返回true才执行beforeBodyWrite
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 1. 如果已经是Result对象,不再二次包装,避免嵌套
if (body instanceof Result) {
return body;
}
// 2. 特殊处理String类型,解决类型转换异常
if (body instanceof String) {
return mapper.writeValueAsString(Result.success(body));
}
// 3. 普通对象统一包装
return Result.success(body);
}
}
4. String 返回报 ClassCastException
当方法返回String类型时就会报错:

Spring 内置 HttpMessageConverter 执行有优先级:
StringHttpMessageConverter 优先级高于 Jackson 序列化器。
当 Controller 返回字符串,包装后返回 Result 对象,StringHttpMessageConverter 强制把返回值转为字符串,类型不匹配抛出异常。
解决方案:
手动使用 ObjectMapper 将 Result 序列化为 JSON 字符串返回,让字符串转换器正常识别。
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 1. 如果已经是Result对象,不再二次包装,避免嵌套
if (body instanceof Result) {
return body;
}
// 2. 特殊处理String类型,解决类型转换异常
if (body instanceof String) {
return mapper.writeValueAsString(Result.success(body));
}
// 3. 普通对象统一包装
return Result.success(body);
}
三、全局统一异常处理
1. 实现方案:@ControllerAdvice + @ExceptionHandler
拦截项目所有未捕获异常,统一封装为 Result 格式返回,无需在每个接口写 try-catch。
@ResponseBody
@ControllerAdvice
public class GlobalExceptionAdvice {
// 捕获空指针异常
@ExceptionHandler(NullPointerException.class)
public Result<?> handleNullPoint(NullPointerException e) {
log.error("空指针异常:", e);
return Result.fail("系统空指针:" + e.getMessage());
}
// 捕获除零运算异常
@ExceptionHandler(ArithmeticException.class)
public Result<?> handleMath(ArithmeticException e) {
log.error("运算异常:", e);
return Result.fail("计算错误:" + e.getMessage());
}
// 兜底捕获所有Exception(优先级最低)
@ExceptionHandler(Exception.class)
public Result<?> handleAll(Exception e) {
log.error("系统未知异常:", e);
return Result.fail("服务器异常,请稍后重试");
}
}
@RestControllerAdvice内包含@ResponseBody和@ControllerAdvice,因此这两个注解也可以用@RestControllerAdvice代替- 可以在注解上指定异常类型,也可以在方法参数中指定异常类型
2. 异常匹配规则
抛出异常时,优先匹配最精准的子类异常方法,没有对应方法才会走顶层 Exception 兜底:
- 抛出
NullPointerException→ 执行handleNullPoint; - 抛出自定义业务异常 → 匹配自定义异常处理器;
- 抛出 IO 异常、数组越界 → 走
Exception通用兜底。
3. @ControllerAdvice 底层原理
@ControllerAdvice 本质携带 @Component,启动时会被 Spring 扫描为 Bean:
RequestMappingHandlerAdapter启动时扫描所有@ControllerAdvice,缓存ResponseBodyAdvice;ExceptionHandlerExceptionResolver缓存所有@ExceptionHandler异常处理方法;- Controller 返回/抛出异常时,自动调用对应 Advice 逻辑完成包装。
总结
- 拦截器
- 两步实现:自定义
HandlerInterceptor+WebMvcConfigurer注册; preHandle负责鉴权阻断,false直接终止请求;- 通过
excludePathPatterns放行登录、静态资源。
- 两步实现:自定义
- 统一返回格式
@ControllerAdvice + ResponseBodyAdvice全局包装;- 必处理 String 类型,解决类型转换异常;
- 判断返回值为 Result 时跳过包装,避免嵌套 JSON。
- 全局异常
- 分层捕获异常:精准子类优先,Exception 兜底;
- 所有异常统一返回 Result,前端无需处理多种报错格式;
- 底层核心:全部依托
DispatcherServlet九大组件,ControllerAdvice作为控制器增强统一处理全局通用逻辑。
DispatcherServlet九大组件:
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
附:阅读源码快捷操作
| 快捷键 | 功能 | 使用场景 |
|---|---|---|
| Ctrl+F | 当前文件内查找 | 已经打开源码类,内部搜关键词 |
| Ctrl+Shift+F | 全局文本搜索 | 只记得字符串、日志、异常信息 |
| Ctrl+N | 查找类 | 记得类名,直接打开源码类 |
| 双击 Shift | Search Everywhere 万能搜索 | 模糊记忆,啥都搜 |
| Ctrl+Shift+Alt+N | 搜索方法 / 变量符号 | 只记得方法片段 |
| Ctrl+F12 | 当前类结构视图 | 浏览大源码类的全部方法 |
| Alt+F7 | 查找用法 | 看方法 / 变量在哪被调用 |
| Ctrl+Alt+H | 调用层次树 | 追踪完整调用链 |
| Ctrl+Alt+B | 跳转到接口实现类 | Spring 接口找实现 |
| Alt+← / Alt+→ | 跳转历史前进后退 | 源码来回跳转,防止迷路 |
更多推荐



所有评论(0)