SpringBoot 统一功能处理:拦截器、统一返回与异常处理

在 SpringBoot 开发中,统一功能处理是提升代码复用性、降低维护成本的关键。本文将围绕拦截器、统一数据返回格式、统一异常处理三大核心功能,结合实战案例与源码解析,带你掌握 SpringBoot 统一功能处理的实现方案。

一、拦截器:请求的 "守门人"

在传统开发中,若需对多个接口做登录校验,需在每个接口重复编写 Session 判断逻辑,冗余且难维护。拦截器作为 Spring 框架核心功能,可统一拦截请求,在目标方法执行前后嵌入自定义逻辑,完美解决此类问题。

1.1 拦截器核心概念

拦截器是 Spring MVC 提供的请求拦截机制,能在请求到达 Controller 前、Controller 执行后、视图渲染完成后执行自定义逻辑,主要用于:

  • 登录状态校验
  • 接口访问权限控制
  • 请求参数预处理 / 日志记录

1.2 拦截器使用步骤

拦截器的实现需两步:定义拦截器 + 注册配置拦截器

步骤 1:定义拦截器(实现 HandlerInterceptor 接口)

需重写三个核心方法,分别对应不同执行时机:

复制代码
@Slf4j
@Component // 注入Spring容器
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 目标方法执行前执行(核心:请求拦截逻辑)
     * @return true:放行;false:拦截(后续逻辑不执行)
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("LoginInterceptor:目标方法执行前执行");
        // 登录校验逻辑:从Session获取用户信息
        HttpSession session = request.getSession(false); // 不自动创建Session
        if (session != null && session.getAttribute("SESSION_USER_KEY") != null) {
            return true; // 已登录,放行
        }
        // 未登录,返回401(Unauthorized)
        response.setStatus(401);
        return false;
    }

    /** 目标方法执行后执行(视图渲染前,后端开发较少用) */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("LoginInterceptor:目标方法执行后执行");
    }

    /** 视图渲染完毕后执行(最后执行,可做资源清理) */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("LoginInterceptor:视图渲染完毕后执行");
    }
}
步骤 2:注册配置拦截器(实现 WebMvcConfigurer)

通过 addPathPatterns 指定拦截路径,excludePathPatterns 指定排除路径(如登录接口、静态资源):

复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**") // 拦截所有请求
                .excludePathPatterns( // 排除无需拦截的路径
                        "/user/login",       // 登录接口
                        "/**/*.js", "/**/*.css", // 静态资源(JS/CSS)
                        "/**/*.png", "/**/*.html"  // 静态资源(图片/HTML)
                );
    }
}
拦截路径规则

不同路径表达式对应不同拦截范围,具体如下:

拦截路径 含义 示例
/* 一级路径 匹配 /user、/book,不匹配 /user/login
/** 任意级路径 匹配 /user、/user/login、/book/add
/book/* /book 下一级路径 匹配 /book/add,不匹配 /book/add/1
/book/** /book 下任意级路径 匹配 /book、/book/add、/book/add/2

1.3 拦截器执行流程

结合 Spring MVC 核心组件 DispatcherServlet,拦截器的执行流程如下:

  1. 请求先进入 DispatcherServlet,由其调用 getHandler 获取请求对应的拦截器链;
  2. 执行拦截器链的 preHandle 方法:若返回 true,继续执行下一个拦截器;若返回 false,中断请求;
  3. 所有 preHandle 放行后,执行 Controller 目标方法;
  4. 目标方法执行完成后,反向执行拦截器链的 postHandle 方法;
  5. 视图渲染完成(后端开发几乎不涉及),反向执行 afterCompletion 方法;
  6. 最终通过 DispatcherServlet 向客户端返回响应。

1.4 源码解析:DispatcherServlet 核心作用

DispatcherServlet 是 Spring MVC 的 "中央调度器",所有请求都需经过它处理。其核心逻辑在 doDispatch 方法中,关键步骤包括:

  1. 获取执行链 :通过 getHandler 找到请求对应的拦截器链(HandlerExecutionChain);
  2. 获取适配器 :通过 getHandlerAdapter 找到适配 Controller 的 HandlerAdapter(适配模式应用);
  3. 执行拦截器 preHandle :调用 mappedHandler.applyPreHandle,遍历执行所有拦截器的 preHandle
  4. 执行目标方法 :通过 handlerAdapter.handle 调用 Controller 方法;
  5. 执行拦截器 postHandle 与 afterCompletion:目标方法执行后,反向执行后续拦截器逻辑。

二、统一数据返回格式:前后端协作的 "语言规范"

前后端分离项目中,若每个接口返回格式不一致(如有的返回实体类、有的返回 String、有的返回 Boolean),会增加前端解析成本。统一数据返回格式可解决此问题,让所有接口返回结构一致。

2.1 实现方案:@ControllerAdvice + ResponseBodyAdvice

通过 @ControllerAdvice(控制器通知类)结合 ResponseBodyAdvice(响应体增强接口),可在响应返回前统一封装数据。

步骤 1:定义统一返回实体类

先定义一个通用的返回结构 Result,包含状态码、错误信息、数据体:

复制代码
@Data
public class Result<T> {
    private String status;       // 状态:SUCCESS/FAIL
    private String errorMessage; // 错误信息(失败时非空)
    private T data;              // 数据体(成功时非空)

    // 成功响应静态方法
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setStatus("SUCCESS");
        result.setData(data);
        return result;
    }

    // 失败响应静态方法
    public static <T> Result<T> fail(String errorMessage) {
        Result<T> result = new Result<>();
        result.setStatus("FAIL");
        result.setErrorMessage(errorMessage);
        result.setData(null);
        return result;
    }
}
步骤 2:实现 ResponseBodyAdvice 统一封装

通过 ResponseBodyAdvicebeforeBodyWrite 方法,在响应返回前对数据进行封装:

复制代码
@Slf4j
@ControllerAdvice // 全局控制器通知
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

    private static final ObjectMapper MAPPER = new ObjectMapper(); // Jackson序列化工具

    /**
     * 是否对响应进行处理(true:处理;false:跳过)
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<?> converterType) {
        return true; // 对所有响应生效
    }

    /**
     * 响应返回前执行:统一封装为Result格式
     */
    @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类型,需手动序列化(解决StringHttpMessageConverter类型转换问题)
        if (body instanceof String) {
            return MAPPER.writeValueAsString(Result.success(body));
        }
        // 3. 其他类型,统一封装为成功响应
        return Result.success(body);
    }
}

2.2 关键问题:String 类型响应的特殊处理

若不处理 String 类型,会出现 ClassCastException: Result cannot be cast to String 异常。原因是:

  • Spring MVC 默认注册了 StringHttpMessageConverter(处理 String 响应)和 MappingJackson2HttpMessageConverter(处理 JSON 响应);
  • 当返回 String 时,Spring 优先使用 StringHttpMessageConverter,但该转换器无法将 Result 类型(封装后的数据)转为 String;
  • 解决方案:手动使用 Jackson 将 Result 序列化为 String,避免类型转换异常。

2.3 统一返回的优势

  1. 降低沟通成本:前后端无需针对每个接口协商返回格式;
  2. 简化前端解析 :前端可通过固定字段(如 status 判断成功 / 失败,data 获取数据)处理所有接口;
  3. 便于维护 :若需修改返回结构,只需调整 Result 类和 ResponseAdvice,无需修改所有接口。

三、统一异常处理:优雅应对程序 "意外"

程序运行中难免出现异常(如空指针、除零异常、自定义业务异常),若未统一处理,会返回默认的 500 错误页面或杂乱的异常信息。统一异常处理可捕获所有异常,返回结构化的错误响应。

3.1 实现方案:@ControllerAdvice + @ExceptionHandler

@ExceptionHandler 是异常处理器注解,结合 @ControllerAdvice 可全局捕获异常,并返回统一格式的错误响应。

步骤 1:全局异常处理器实现
复制代码
@Slf4j
@ControllerAdvice // 全局生效
@ResponseBody     // 确保返回JSON格式
public class GlobalExceptionHandler {

    /**
     * 捕获所有Exception(兜底处理)
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("全局异常捕获:", e); // 记录异常日志
        return Result.fail("系统异常:" + e.getMessage());
    }

    /**
     * 捕获NullPointerException(特定异常优先处理)
     */
    @ExceptionHandler(NullPointerException.class)
    public Result<Void> handleNullPointerException(NullPointerException e) {
        log.error("空指针异常捕获:", e);
        return Result.fail("空指针异常:" + e.getMessage());
    }

    /**
     * 捕获ArithmeticException(如除零异常)
     */
    @ExceptionHandler(ArithmeticException.class)
    public Result<Void> handleArithmeticException(ArithmeticException e) {
        log.error("算术异常捕获:", e);
        return Result.fail("算术异常:" + e.getMessage());
    }

    // 可扩展:添加自定义业务异常处理(如UserNotLoginException)
}
步骤 2:异常匹配规则

当程序抛出异常时,Spring 会按 "最具体异常优先" 的原则匹配处理器:

  • 例如抛出 NullPointerException,会优先匹配 handleNullPointerException,而非兜底的 handleException
  • 匹配逻辑由 ExceptionHandlerMethodResolver 实现,通过异常类型的继承深度排序(子类异常处理器优先)。

3.2 实战效果

  • 访问 /test/t2(执行 10/0),抛出 ArithmeticException,返回:

    json

    复制代码
    {
      "status": "FAIL",
      "errorMessage": "算术异常:/ by zero",
      "data": null
    }
  • 访问 /test/t3(执行 null.length()),抛出 NullPointerException,返回:

    json

    复制代码
    {
      "status": "FAIL",
      "errorMessage": "空指针异常:Cannot invoke \"String.length()\" because \"a\" is null",
      "data": null
    }

四、@ControllerAdvice 源码解析:全局能力的 "基石"

@ControllerAdvice 是实现统一数据返回和统一异常处理的核心注解,其本质是一个 @Component(可被 Spring 扫描注入),通过属性(如 basePackages)指定生效范围。

4.1 核心源码

复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 本质是Spring组件
public @interface ControllerAdvice {
    // 指定生效的包(默认所有包)
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    // 指定生效的类(如仅对UserController生效)
    Class<?>[] assignableTypes() default {};

    // 指定生效的注解(如仅对@RestController生效)
    Class<? extends Annotation>[] annotations() default {};
}

4.2 生效原理

  1. 统一数据返回DispatcherServlet 初始化时,通过 initHandlerAdapters 加载 RequestMappingHandlerAdapter;该适配器会扫描所有 @ControllerAdvice 标注的类,若实现 ResponseBodyAdvice,则加入 requestResponseBodyAdvice 列表;当响应返回时,调用 ResponseBodyAdvicebeforeBodyWrite 方法进行封装。
  2. 统一异常处理DispatcherServlet 初始化时,通过 initHandlerExceptionResolvers 加载 ExceptionHandlerExceptionResolver;该解析器会扫描所有 @ControllerAdvice 标注的类,提取 @ExceptionHandler 方法存入 exceptionHandlerAdviceCache;当抛出异常时,通过 ExceptionHandlerMethodResolver 匹配对应的处理器方法。

五、总结与实战建议

5.1 核心知识点总结

功能 实现方案 核心组件 / 注解 作用
拦截器 实现 HandlerInterceptor + 配置 WebMvcConfigurer HandlerInterceptorWebMvcConfigurer 统一请求拦截(登录校验、权限控制)
统一数据返回 @ControllerAdvice + ResponseBodyAdvice @ControllerAdviceResponseBodyAdvice 所有接口返回格式统一为 Result
统一异常处理 @ControllerAdvice + @ExceptionHandler @ControllerAdvice@ExceptionHandler 全局捕获异常,返回结构化错误响应

5.2 实战建议

  1. 拦截器排除静态资源:前端的 JS、CSS、图片等静态资源无需拦截,避免影响页面加载;
  2. 自定义业务异常 :在实际项目中,建议定义业务异常(如 UserNotLoginExceptionPermissionDeniedException),结合 @ExceptionHandler 实现更精细的异常处理;
  3. 日志记录 :在异常处理器中务必记录异常栈信息(如 log.error("异常信息:", e)),便于问题排查;
  4. 接口评审:统一功能处理需在项目初期确定(如返回格式、异常码规范),避免后期大规模修改。

通过本文的方案,可大幅提升 SpringBoot 项目的可维护性和前后端协作效率,是企业级开发的必备技能。

相关推荐
泉城老铁2 小时前
springboot+vue 文件下载,实现大文件的分片压缩和下载,避免内存溢出
前端·spring boot·后端
冬天vs不冷2 小时前
Java基础(十三):内部类详解
android·java·python
YQ_ZJH2 小时前
Java List列表创建方法大总结
java·开发语言·数据结构·算法·list
城管不管2 小时前
Spring + Spring MVC + MyBatis
java·spring·mvc
TDengine (老段)3 小时前
TDengine 聚合函数 ELAPSED 用户手册
java·大数据·数据库·sql·物联网·时序数据库·tdengine
泉城老铁3 小时前
springboot +mybatisplus的性能优化
后端
泉城老铁3 小时前
springboot开发中,如何提升代码的性能
后端
我不是混子3 小时前
数据误删了咋办?别怕,今天来教你如何恢复数据
java·后端