前言
在现代 Spring Boot 应用中,我们通常会定义一个统一的响应结构,如 Result<T>,来封装所有的 API 返回值。为了避免在每个 Controller 方法中手动包装,我们(正确地)转向了 ResponseBodyAdvice。
// 我们的统一响应体
public class Result<T> {
private Integer code;
private String message;
private T data;
// ... 构造函数和 success/fail 方法
}
当我们兴高采烈地实现 GlobalResponseAdvice 后,奇迹发生了:
getUser()返回User对象 →\rightarrow→ 正常包装为Result<User>(JSON)getMap()返回Map→\rightarrow→ 正常包装为Result<Map>(JSON)getInt()返回Integer→\rightarrow→ 正常包装为Result<Integer>(JSON)getString()返回String→\rightarrow→ 灾难发生!
前端没有收到预期的 {"code":0, "message":"success", "data":"hello"},而是收到了一个 Content-Type 为 text/plain 的、奇形怪状的字符串:Result(code=0, message=success, data=Hello)。
为什么?为什么 String 如此特殊?
问题的根源:HttpMessageConverter 的“抢答”
这个问题的核心不在于 String 和 Integer 的类型差异,而在于 Spring MVC 用来处理返回值的 HttpMessageConverter(消息转换器)链。
Spring 在决定如何将你的 Controller 返回值写入 HTTP 响应时,会遍历一个转换器列表。
1. “快乐路径”:当您返回 User、Integer 或 List
- Controller 返回: 一个
User对象。 - Spring 选择转换器: Spring 遍历列表,
StringHttpMessageConverter拒绝了它(“我只处理 String”)。最终,MappingJackson2HttpMessageConverter(JSON 转换器)“中标”了(“我能处理 POJO!”)。 ResponseBodyAdvice执行: 你的beforeBodyWrite方法被调用。body是User对象。selectedConverterType是MappingJackson2HttpMessageConverter.class。
- 你的逻辑: 你的代码返回
Result.success(user)。 - Spring 最终处理: Spring 拿到这个新的
Result<User>对象,并继续使用它最初选择的MappingJackson2HttpMessageConverter将其序列化为 JSON。 - 结果: 完美!
Content-Type: application/json,响应体是Result<User>的 JSON。
2. “灾难路径”:当您返回 String
- Controller 返回: 一个
String对象。 - Spring 选择转换器: Spring 遍历列表。
StringHttpMessageConverter立即“抢答”:“这是一个 String,归我了!”。- 关键点:
StringHttpMessageConverter的优先级非常高,并且它的默认Content-Type是text/plain。
- 关键点:
ResponseBodyAdvice执行: 你的beforeBodyWrite方法被调用。body是String对象。selectedConverterType是StringHttpMessageConverter.class。
- 你的逻辑(如果未处理): 假设你没有特殊处理,你同样返回
Result.success("Hello")。 - Spring 最终处理(灾难!): Spring 拿到了这个新的
Result<String>对象,但它被告知必须使用最初选择的StringHttpMessageConverter来处理它。 StringHttpMessageConverter的工作很简单:在它拿到的任何对象上调用.toString(),然后以text/plain格式输出。- 结果: Spring 在
Result<String>对象上调用了toString(),导致你看到了那个丑陋的Result(code=0, message=success, data=Hello)字符串,并且Content-Type是text/plain。
解决方案:识别“陷阱”并“手动接管”
要解决这个“弯弯绕”,我们必须在 beforeBodyWrite 中识别出这个“陷阱”(即 StringHttpMessageConverter 被选中时),并手动完成本该由 JSON 转换器做的工作。
这就是你的 GlobalResponseAdvice 中那段“神奇”代码的真正含义:
// 你的 GlobalResponseAdvice.java (已是正确实现)
@Override
@Nullable
public Object beforeBodyWrite(@Nullable Object body, @NonNull MethodParameter returnType,
@NonNull MediaType selectedContentType,
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
@NonNull ServerHttpRequest request, @NonNull ServerHttpResponse response) {
// 步骤 1: 识别陷阱
// 检查 Spring 是否错误地选择了 StringHttpMessageConverter
if (StringHttpMessageConverter.class.isAssignableFrom(selectedConverterType)) {
// 步骤 2: 手动“纠正” Content-Type
// 强制将 Content-Type 从 text/plain 改回 application/json
HttpHeaders headers = response.getHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
try {
// 步骤 3: 手动“纠正” 响应体
// 1. 将 body 包装成 Result.success(body)
// 2. 手动使用 ObjectMapper 将其序列化为 JSON 字符串
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
// 处理序列化异常
String declaringClass = returnType.getContainingClass().getName();
String returnTypeName = returnType.getParameterType().getName();
// 抛出平台异常,让全局异常处理器接管
throw PlatformException.of("platform.log.response.serialization", e,
declaringClass,
returnTypeName
);
}
}
// 如果不是 StringHttpMessageConverter(即快乐路径),正常包装
return Result.success(body);
}
解决方案如何工作
通过上述代码,当 Controller 返回 String 时,流程被修正为:
beforeBodyWrite捕获到StringHttpMessageConverter。- 它将
Content-Type强制改回application/json。 - 它手动将
Result<String>序列化为 JSON 字符串(例如:"{\"code\":0,\"data\":\"Hello\"}")。 - 它将这个新的 JSON 字符串返回给 Spring。
- Spring 将这个新字符串交给
StringHttpMessageConverter。 StringHttpMessageConverter看到自己拿到的还是一个String,非常高兴,它原封不动地将这个(已经是 JSON 格式的)字符串写入响应体。
我们“欺骗”了 StringHttpMessageConverter,让它在不知不觉中帮我们完成了 JSON 的传输。
总结
ResponseBodyAdvice 中对 String 类型的特殊处理,不是 Bug,而是应对 Spring MVC 消息转换器链高优先级机制的必要手段。Integer 和 Boolean 之所以不需要,只是因为它们足够幸运,默认被 MappingJackson2HttpMessageConverter(JSON 转换器)处理了。
理解了 HttpMessageConverter 的选择机制,这个“弯弯绕”也就不再神奇了。


3161

被折叠的 条评论
为什么被折叠?



