[Spring MVC] 统一功能与拦截器实践总结

在 Web 项目中,Controller 通常会面对很多重复问题:接口返回格式不统一、异常处理分散、登录校验到处写。

本篇文章结合一个图书管理系统 book-demo,总结 Spring MVC 中常见的统一功能和拦截器实践。

项目中主要涉及三类能力:

  • 统一返回结果:Result<T>ResultCode
  • 统一响应包装:ResponseBodyAdvice
  • 统一异常处理:@ControllerAdvice + @ExceptionHandler
  • 登录拦截器:HandlerInterceptor + WebMvcConfigurer

一、项目背景

book-demo 是一个简单的图书管理系统,包含登录、图书列表分页、添加图书、修改图书、删除图书等接口。

例如 :

java 复制代码
@RestController
@RequestMapping("/book")
public class BookController {

    @RequestMapping("getBookListByPage")
    public Result<PageResponse<BookInfo>> getBookListByPage(PageRequest pageRequest) {
        PageResponse<BookInfo> response = bookService.getListByPage(pageRequest);
        return Result.success(response);
    }
}

这些接口有一个共同特点:都需要返回给前端稳定的数据结构。

同时,除了登录接口和静态资源外,大部分接口都应该要求用户已经登录。

因此,项目中引入了统一返回、统一异常处理和登录拦截器。

二、什么是 Spring 统一功能

所谓"统一功能",本质上是把 Controller 中重复、横切的逻辑抽取出来,让业务接口只关注业务本身。

常见统一功能包括:

  • 统一返回格式:所有接口都返回固定 JSON 结构
  • 统一异常处理:异常不直接暴露给前端,而是转换成统一错误响应
  • 统一登录校验:不在每个接口里手动判断用户是否登录
  • 统一参数校验、日志、权限校验等

在 Spring MVC 中,这些能力通常通过以下机制实现:

  • ResponseBodyAdvice
  • @ControllerAdvice
  • @ExceptionHandler
  • HandlerInterceptor
  • WebMvcConfigurer

三、统一返回结果 Result

项目中定义了统一响应对象 Result<T>

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
    private int code;
    private String errMsg;
    private T data;

    public static Result unlogin() {
        return new Result(ResultCode.UNLOGIN.getCode(), "用户未登录", null);
    }

    public static <T> Result<T> success(T data) {
        return new Result(ResultCode.SUCCESS.getCode(), null, data);
    }

    public static <T> Result fail(String errMsg) {
        return new Result(ResultCode.FAIL.getCode(), errMsg, null);
    }
}

对应状态码枚举:

java 复制代码
@Getter
@AllArgsConstructor
public enum ResultCode {
    SUCCESS(200),
    FAIL(-2),
    UNLOGIN(-1);

    private int code;
}

这样前端可以按照统一格式处理响应:

json 复制代码
{
  "code": 200,
  "errMsg": null,
  "data": {}
}

统一返回结构的好处是:

  • 前端不用针对每个接口猜返回格式
  • 成功、失败、未登录等状态表达清晰
  • 后端接口风格统一,维护成本更低

四、使用 ResponseBodyAdvice 统一包装返回值

虽然可以在每个 Controller 中手动返回 Result.success(...),但更进一步,可以使用 ResponseBodyAdvice 对响应结果进行统一处理。

项目中的 ResponseAdvice

java 复制代码
@Slf4j
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {

        if (body instanceof Result) {
            return body;
        }

        if (body instanceof String) {
            Result result = Result.success(body);
            return objectMapper.writeValueAsString(result);
        }

        return Result.success(body);
    }
}

它的执行逻辑是:

  1. Controller 方法返回结果
  2. Spring MVC 准备把返回值写入响应体
  3. beforeBodyWrite 被调用
  4. 如果返回值已经是 Result,直接返回
  5. 如果不是 Result,自动包装成 Result.success(body)

这里有一个特殊点:String 类型需要单独处理。

原因是 Spring MVC 对 String 返回值通常会使用 StringHttpMessageConverter,它期望最终返回的仍然是字符串。

如果直接返回 Result 对象,可能出现类型转换异常。因此项目中先把 Result 序列化成 JSON 字符串再返回。

五、统一异常处理

项目中使用 @ControllerAdvice@ExceptionHandler 做全局异常处理:

java 复制代码
@Slf4j
@ResponseBody
@ControllerAdvice
public class ErrorAdvice {

    @ExceptionHandler
    public Result exceptionHandler(Exception e) {
        log.error("异常信息: {}", e);
        return Result.fail("服务器异常");
    }

    @ExceptionHandler
    public Result exceptionHandler(NoResourceFoundException e) {
        log.error("异常信息: {}", e);
        return Result.fail("资源不存在: " + e.getResourcePath());
    }
}

它的作用是:当 Controller 或后续业务代码抛出异常时,不让异常堆栈直接返回给前端,而是转换成统一的失败响应。

例如服务端出现异常时,前端得到的是:

json 复制代码
{
  "code": -2,
  "errMsg": "服务器异常",
  "data": null
}

这样做有几个好处:

  • 避免异常堆栈暴露给前端
  • 接口失败格式统一
  • 日志中保留异常详情,方便后端排查
  • Controller 中不需要到处写重复的 try-catch

六、登录拦截器 LoginInterceptor

除了统一返回和异常处理,项目还使用拦截器完成登录校验。

核心代码如下:

java 复制代码
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        log.info("用户请求: {}", request.getRequestURI());

        response.setContentType("application/json;charset=utf-8");
        HttpSession session = request.getSession();

        if (session == null ||
            session.getAttribute(Constants.SESSION_USER_KEY) == null) {

            log.warn("用户未登录: {}", request.getRequestURI());

            Result result = Result.unlogin();
            response.setStatus(401);
            response.getOutputStream().write(objectMapper.writeValueAsBytes(result));
            return false;
        }

        return true;
    }
}

preHandle 是请求进入 Controller 之前执行的方法。

它的返回值很关键:

  • 返回 true:请求继续执行,进入 Controller
  • 返回 false:请求被拦截,不再进入 Controller

在这个项目中,登录成功后会把用户信息放到 Session 中:

java 复制代码
session.setAttribute(Constants.SESSION_USER_KEY, userInfo);

所以拦截器只需要判断 Session 中是否存在这个 key:

java 复制代码
Constants.SESSION_USER_KEY

如果不存在,说明用户未登录,直接返回:

json 复制代码
{
  "code": -1,
  "errMsg": "用户未登录",
  "data": null
}

同时 HTTP 状态码设置为 401,表示未认证。

七、注册拦截器 WebConfig

拦截器定义之后,还需要注册到 Spring MVC 中。

项目中的配置类如下:

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

    @Autowired
    private LoginInterceptor loginInterceptor;

    private List<String> excludePaths = List.of(
            "/user/login",
            "/**/*.js",
            "/**/*.css",
            "/**/*.jpg",
            "/**/*.png",
            "/**/*.html"
    );

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludePaths);
    }
}

这里有两个核心配置:

java 复制代码
.addPathPatterns("/**")

表示拦截所有请求。

java 复制代码
.excludePathPatterns(excludePaths)

表示放行指定路径。

项目中需要放行:

  • /user/login:否则用户还没登录就无法访问登录接口
  • /**/*.html:登录页、图书页面等静态页面
  • /**/*.js/**/*.css/**/*.png:前端静态资源

如果没有正确放行静态资源,页面可能会出现 HTML 能打开,但 CSS、JS、图片加载失败的问题。

八、整体请求执行流程

结合项目中的实现,一个普通图书接口请求大致流程如下:

text 复制代码
浏览器发起请求
        |
        v
LoginInterceptor.preHandle
        |
        |-- 未登录:返回 401 + Result.unlogin(),请求结束
        |
        |-- 已登录:继续执行
        v
Controller 处理业务
        |
        v
Service / Mapper 访问业务数据
        |
        v
Controller 返回结果
        |
        v
ResponseAdvice.beforeBodyWrite 统一包装响应
        |
        v
返回 JSON 给前端

如果执行过程中抛出异常,则会进入:

text 复制代码
Controller / Service 抛出异常
        |
        v
ErrorAdvice 捕获异常
        |
        v
返回 Result.fail(...)

九、常见坑

1. 登录接口必须放行

如果 /user/login 没有配置到 excludePathPatterns 中,登录请求也会被拦截。

结果就是用户永远无法登录。

2. 静态资源也要放行

前端页面依赖 HTML、CSS、JS、图片等资源。

如果这些资源被登录拦截器拦截,页面样式和交互会异常。

3. String 返回值需要特殊处理

使用 ResponseBodyAdvice 统一包装返回值时,String 是一个特殊类型。

因为它默认使用字符串消息转换器,所以最好手动序列化成 JSON 字符串。

4. 前后端字段名要统一

项目中的统一返回字段是:

java 复制代码
private int code;
private String errMsg;
private T data;

前端读取错误信息时也应该使用 errMsg

如果前端读取的是 message,就会出现错误提示拿不到的问题。

十、总结

Spring MVC 中的统一功能和拦截器,本质上都是为了减少重复代码,让业务接口更加专注。

book-demo 项目中:

  • Result<T> 统一了接口响应格式
  • ResponseBodyAdvice 实现了返回值自动包装
  • @ControllerAdvice@ExceptionHandler 实现了全局异常处理
  • HandlerInterceptor 实现了登录校验
  • WebMvcConfigurer 负责注册拦截器并配置放行路径

这些能力组合起来后,Controller 就不需要关心大量通用逻辑,只需要处理具体业务。

这也是 Spring MVC 在实际项目中非常常见、非常实用的一套开发模式。

相关推荐
Full Stack Developme2 小时前
Spring Boot 事务管理完整教程
java·数据库·spring boot
城管不管2 小时前
前后端远程协作
java
青云计划2 小时前
Feed流
java·后端·spring
☞遠航☜2 小时前
搭建基础的springcloud alibaba项目练习
后端·spring·spring cloud
java1234_小锋3 小时前
String、StringBuilder、StringBuffer的区别?
java·开发语言
星原望野3 小时前
JAVA集合:List、Set和Map
java·开发语言·list·set·map·集合
2601_957787583 小时前
星链引擎矩阵系统:插件化多平台 API 网关与账号级隔离技术实践
java·矩阵·插件化架构
多敲代码防脱发3 小时前
Spring进阶(容器实现)
java·开发语言·后端·spring
星辰_mya4 小时前
彩云之上——[特殊字符]的架构师
java·后端·微服务·面试·架构