搜索 K
Appearance
Appearance
GlobalResponseAdvice 通过 Spring MVC 的 ResponseBodyAdvice<Object> 扩展点,在 HttpMessageConverter 序列化之前介入返回体。
ResponseBodyAdvice<T> 是 Spring MVC 的标准扩展接口,配合 @ControllerAdvice / @RestControllerAdvice 生效,两个方法:
supports(MethodParameter returnType, Class converterType):返回 false 时整条 advice 不参与本次响应beforeBodyWrite(Object body, ...):在序列化前对 body 做最后一次加工,返回修改后的对象本模块的实现两个方法都用到了。
@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,连白名单判断都不走,性能更稳定,也避免后续逻辑被绕过。
shouldBypass(path, mediaType) 检查四类条件,命中任一就放行:
| 类别 | 内容 |
|---|---|
| 精确路径 | /swagger-ui.html、/doc.html、/favicon.ico |
| API 文档路径前缀 | /v3/api-docs、/swagger-ui、/knife4j、/webjars/、/swagger-resources |
| 静态资源前缀 | /assets/、/static/、/public/ |
| 媒体类型 | text/html、text/css、application/javascript |
这部分硬编码是个已知的取舍:好处是开箱即用,业务方不需要任何配置;代价是想加自定义白名单必须改源码或者覆盖 Bean。后续若呼声强,可以抽成 @ConfigurationProperties,让业务方在 application.yml 追加路径。
if (body instanceof Result) {
return body;
}这一行同时承担两个职责:
Result.success(...) 时不被二次包装GlobalExceptionHandler 的 @ExceptionHandler 方法返回的 Result 不被二次包装少了这一行,包装链路就会出现 Result<Result<T>> 这种双层结构,前端解析必然出错。
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,但用户可能想让创建接口返回 201private 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 更简单,对业务代码无侵入 |