背景
- Spring Boot 升级 2.2.4 后,有一个变化在于当返回 json 时
content-type由application/json;charset=UTF-8变为了application/json;
- 而这个则会造成默认编码不为 UTF-8 的应用出现乱码问题
原因
- 那么是为什么呢?根因在于
Jackson2CodecSupport 的 DEFAULT_MIME_TYPES 去除了 Charset 选项,所以输出就没有 Charset 了

- 那么 Spring 是怎么确定
Content-Type 的呢?能不能在中途加上呢?
- 分析调用链路,可以发现是在
EncoderHttpMessageWriter 做了 updateContentType

- 那么怎么 update 的呢?

- 可见,是首先从 MessageHeader 里拿了下,没拿到,则用默认的,如果默认没有,则用
AbstractMessageWriterResultHandler 选择的最好的一个 MediaType

- 那么默认的是什么呢?

- 就是 Encoder 的第一个 MimeType ,对于 Json 默认的 Encode 即为
Jackson2JsonEncoder 因此 Jackson2CodecSupport 去除的 charset 会影响到真实请求输出
解法
- 由上可见,解法很简单,有两种,一为添加自定义 Encoder,自定义 Encoder 的 default mime Type 中加上 charset 即可。
- 不过这种方法成本太高,自定义 Encoder 还要完善测试什么,比较麻烦
- 因此第二种,在 Response Header 中加上
Content-Type 成为首选,这个也比较简单,加一个 WebFilter 即可
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class ContentTypeFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
return chain.filter(exchange);
}
}
- 加完果真,正常的输出就有了 Charset,但测试一跑,发现异常链路还是有问题,没有 charset,这又是为什么呢?
- 追踪源码发现,原来是 RequestMappingHandlerAdapter 异常处理时会清除所有 Content 相关头,而此时已走完 filter 链,要想改写,只能复写 HandlerResultHandler

- 复写 HandlerResultHandler 过于麻烦,而且对于每种输出都要复写,成本很高

- 所以没想到什么好方法,就采用了一个 Hack 的方法,通过反射改写
Jackson2CodecSupport 的 DEFAULT_MIME_TYPES
static {
addCharsetToJsonContentType();
}
public static void addCharsetToJsonContentType() {
Field defaultMimeTypeField = null;
try {
defaultMimeTypeField = Jackson2CodecSupport.class.getDeclaredField("DEFAULT_MIME_TYPES");
defaultMimeTypeField.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(defaultMimeTypeField, defaultMimeTypeField.getModifiers() & ~Modifier.FINAL);
defaultMimeTypeField.set(null,
Collections.unmodifiableList(Arrays.asList(new MimeType("application", "json", Charsets.UTF_8),
new MimeType("application", "*+json", Charsets.UTF_8))));
defaultMimeTypeField.setAccessible(false);
modifiersField.setInt(defaultMimeTypeField, defaultMimeTypeField.getModifiers() | Modifier.FINAL);
System.out.println("Already Add Charset To Jackson");
}
catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
关联知识 - WebFlux 下请求处理链路
- 由 Spring 文档可知,WebFLux 下最核心的为
HttpHandler和WebHandler,网络框架(netty,tomcat或者其他)收到请求先交由HttpHandler再由其调用最终的WebHandler

- 那么 HttpHandelr 是如何构建的呢?

- 可见,其首先拿到最终的 webHandler 和 filter 链构造了一个decorated,然后又包装了exceptionHandlers,最终变为一个HttpWebHandlerAdapter直接对接网络框架
- 当网络框架响应 channel 状态变化时,则会调用 HttpWebHandlerAdapter 的 handler 方法,然后依次责任链调用

- 具体如上图,可见当走到
RequestMappingHandlerAdapter的handleException时,早已走完了 filter 链,因此我们的 ContentTypeFilter 就失效了
参考资料