后端开发中,我们总会遇到三类重复通用需求:

  1. 登录校验、权限拦截,每个接口重复写 Session 判断,代码冗余难维护;
  2. 接口返回格式五花八门,有的返回对象、有的返回布尔、有的返回字符串,前端解析成本高;
  3. 代码空指针、运算异常直接抛出原生报错,页面展示杂乱,无法统一提示。

SpringBoot 提供三套方案一次性解决以上问题:

  1. 拦截器统一鉴权
  2. @ControllerAdvice + ResponseBodyAdvice 统一返回格式
  3. @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
/**任意多级路径匹配 /useruser/listuser/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;  
    }  

测试:

  1. 未登录访问 /book/getList:接口返回 401;
  2. 先调用登录接口存入 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 核心调度类 DispatcherServletdoDispatch 方法

执行顺序:

  1. 根据 URL 匹配处理器执行链 HandlerExecutionChain(包含拦截器列表);
  2. 循环执行所有拦截器 preHandle(),任意拦截器返回 false,直接终止流程;
  3. preHandle 全部放行后,执行 Controller 业务方法;
  4. Controller 执行完毕,倒序执行 postHandle()
  5. 最后执行 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:

  1. RequestMappingHandlerAdapter 启动时扫描所有 @ControllerAdvice,缓存 ResponseBodyAdvice
  2. ExceptionHandlerExceptionResolver 缓存所有 @ExceptionHandler 异常处理方法;
  3. Controller 返回/抛出异常时,自动调用对应 Advice 逻辑完成包装。

总结

  1. 拦截器
    • 两步实现:自定义 HandlerInterceptor + WebMvcConfigurer 注册;
    • preHandle 负责鉴权阻断,false 直接终止请求;
    • 通过 excludePathPatterns 放行登录、静态资源。
  2. 统一返回格式
    • @ControllerAdvice + ResponseBodyAdvice 全局包装;
    • 必处理 String 类型,解决类型转换异常;
    • 判断返回值为 Result 时跳过包装,避免嵌套 JSON。
  3. 全局异常
    • 分层捕获异常:精准子类优先,Exception 兜底;
    • 所有异常统一返回 Result,前端无需处理多种报错格式;
  4. 底层核心:全部依托 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查找类记得类名,直接打开源码类
双击 ShiftSearch Everywhere 万能搜索模糊记忆,啥都搜
Ctrl+Shift+Alt+N搜索方法 / 变量符号只记得方法片段
Ctrl+F12当前类结构视图浏览大源码类的全部方法
Alt+F7查找用法看方法 / 变量在哪被调用
Ctrl+Alt+H调用层次树追踪完整调用链
Ctrl+Alt+B跳转到接口实现类Spring 接口找实现
Alt+← / Alt+→跳转历史前进后退源码来回跳转,防止迷路
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐