Skip to content

响应拦截器

GlobalResponseAdvice 通过 Spring MVC 的 ResponseBodyAdvice<Object> 扩展点,在 HttpMessageConverter 序列化之前介入返回体。

ResponseBodyAdvice 契约速览

ResponseBodyAdvice<T> 是 Spring MVC 的标准扩展接口,配合 @ControllerAdvice / @RestControllerAdvice 生效,两个方法:

  • supports(MethodParameter returnType, Class converterType):返回 false 时整条 advice 不参与本次响应
  • beforeBodyWrite(Object body, ...):在序列化前对 body 做最后一次加工,返回修改后的对象

本模块的实现两个方法都用到了。

包装算法

supports 阶段:@RawResponse 早退

java
@Override
public boolean supports(MethodParameter returnType,
                        Class<? extends HttpMessageConverter<?>> converterType) {
    return !returnType.hasMethodAnnotation(RawResponse.class)
            && !returnType.getContainingClass().isAnnotationPresent(RawResponse.class);
}

把 @RawResponse 的检查放在 supports 而不是 beforeBodyWrite 的好处:返回 false 后 Spring 完全不会进入这条 advice,连白名单判断都不走,性能更稳定,也避免后续逻辑被绕过。

白名单 bypass

shouldBypass(path, mediaType) 检查四类条件,命中任一就放行:

类别内容
精确路径/swagger-ui.html/doc.html/favicon.ico
API 文档路径前缀/v3/api-docs/swagger-ui/knife4j/webjars//swagger-resources
静态资源前缀/assets//static//public/
媒体类型text/htmltext/cssapplication/javascript

这部分硬编码是个已知的取舍:好处是开箱即用,业务方不需要任何配置;代价是想加自定义白名单必须改源码或者覆盖 Bean。后续若呼声强,可以抽成 @ConfigurationProperties,让业务方在 application.yml 追加路径。

body 已是 Result 时直接放行

java
if (body instanceof Result) {
    return body;
}

这一行同时承担两个职责:

  1. 业务方手动返回 Result.success(...) 时不被二次包装
  2. GlobalExceptionHandler 的 @ExceptionHandler 方法返回的 Result 不被二次包装

少了这一行,包装链路就会出现 Result<Result<T>> 这种双层结构,前端解析必然出错。

@MyApiResponse 的处理

java
MyApiResponse myApiResponse = method.getAnnotation(MyApiResponse.class);
if (myApiResponse != null) {
    response.setStatusCode(HttpStatus.valueOf(myApiResponse.httpCode()));
    Result<Object> result = Result.success(myApiResponse.code(), myApiResponse.msg(), body);
    return handleStringResponse(result, selectedConverterType);
}

注意三点:

  • response.setStatusCode 直接改 HTTP 状态码,因为 Controller 默认状态码是 200,但用户可能想让创建接口返回 201
  • 注解里的 code 与 msg 写入 Result 的 code / message
  • 业务数据原样放进 data,没有任何变换

String 类型的特殊处理

java
private Object handleStringResponse(Result<Object> result, Class<? extends HttpMessageConverter<?>> converterType) {
    if (StringHttpMessageConverter.class == converterType) {
        try {
            return objectMapper.writeValueAsString(result);
        } catch (JsonProcessingException e) {
            return "{\"success\":false,...}";  // 序列化失败的兜底 JSON
        }
    }
    return result;
}

为什么需要这段?Spring MVC 在 Controller 返回 String 时会选择 StringHttpMessageConverter 而不是 MappingJackson2HttpMessageConverter。前者只接受 String,不接受 Result 对象。如果直接返回 Result,会因为 ClassCastException 在序列化阶段失败。

解决方案是:检测到当前 converter 是 StringHttpMessageConverter 时,主动调用 ObjectMapper 把 Result 先序列化成 JSON 字符串,再交给 converter 写出。

如果 ObjectMapper 自身抛异常(极少见),用一段拼好的兜底 JSON 字符串保底,至少保证 HTTP 响应不破坏 JSON 结构。

设计取舍

选项选择理由
@RawResponse 拦截位置supports 阶段性能更稳,逻辑更短,与 Spring 契约一致
白名单硬编码当前需求够用,后续如有压力再抽配置
body 是 Result 的判断beforeBodyWrite 内supports 阶段拿不到 body,只能在写入前判断
String 返回值处理ObjectMapper 显式序列化Spring 没有标准方案;这是公认的解决套路
状态码覆盖通过 response.setStatusCode比 ResponseEntity 更简单,对业务代码无侵入