前言
最近在将 fastjson
升级到最新版本(1.2.35)时发现官方推荐使用 FastJsonHttpMessageConverter
来集成 spring,于是便将 FastJsonHttpMessageConverter4
换成了 FastJsonHttpMessageConverter
其它设置没有改变,配置如下所示:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(SerializerFeature.WriteMapNullValue, // 空字段保留
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullNumberAsZero);
converter.setFastJsonConfig(config);
converter.setDefaultCharset(Charset.forName("UTF-8"));
converters.add(converter);
}
启动后却发生了乱码
于是便查看了下浏览器的 response hearders 信息
从这可以看出后台返回的就是最普通的 text/html 格式,连编码都没有指定,结果显而易见会乱码。可以确定问题是出在 content-type
这里了。
探寻
为了查出问题所在,我们就需要查看 FastJsonHttpMessageConverter
的源码了,如果只想看解决方案的朋友可以点这里。
首先,直接点到顶层父类接口.
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> var1, MediaType var2);
boolean canWrite(Class<?> var1, MediaType var2);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;
void write(T var1, MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}
可以看到其中有个 write(...)
方法是指定 mediaType
(即 content-type
) 的,进入到 FastJsonHttpMessageConverter
中查看,发现有复写 write(...)
方法,如下:
public void write(Object t, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
HttpHeaders headers = outputMessage.getHeaders();
if(headers.getContentType() == null) {
if(contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentType = this.getDefaultContentType(t);
}
if(contentType != null) {
headers.setContentType(contentType);
}
}
if(headers.getContentLength() == -1L) {
Long contentLength = this.getContentLength(t, headers.getContentType());
if(contentLength != null) {
headers.setContentLength(contentLength.longValue());
}
}
this.writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
可以很明显的看出在这里进行了 content-type 的编码操作,而且这里传入了一个 contentType ,值是多少呢?打个断点跑起来
没有意外,传入的就是 text/html ,而且我们也可以看到 headers 的 size 为 0,也就是说会进入下面这个语句中
if(headers.getContentType() == null) {
if(contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentType = this.getDefaultContentType(t);
}
if(contentType != null) {
headers.setContentType(contentType);
}
}
这里先判断 contentType 是否为 null,如果不为 null 的话就直接进行 headers.setContentType(contentType)
的操作,也就造成了乱码。
知道了问题所在,那么解决起来就很快了,我们要做的便是改变这个 contentType,第一件事便是要知道它从何而来,这就还是要进入 FastJsonHttpMessageConverter
的顶层父接口 HttpMessageConverter
中,在这里查看 write(...)
方法在何地被引用,由于需要进入源码查询,因此需要导入源码包,具体导入过程可以百度查找,我用的 idea ,直接点击反编译类文件的右上角的 Download Sources
便可以下载和关联源文件,下载完后双击选中 write(...)
方法,按 CTRL + ALT + H
便可以出现如图所示引用链
第一个便是我们要找的目标,进入到里面,直接定位关键代码:
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
if (((GenericHttpMessageConverter) messageConverter).canWrite(
declaredType, valueType, selectedMediaType)) {
outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),inputMessage, outputMessage);
if (outputValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
((GenericHttpMessageConverter) messageConverter).write(
outputValue, declaredType, selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType +
"\" using [" + messageConverter + "]");
}
}
return;
}
}
这里先从 messageConverters 中取出我们自定义的 FastJsonHttpMessageConverter ,然后调用 write () 方法,可以看到这里给 mediaType 赋的值是一个叫做 selectedMediaType 的变量,这个变量又是什么呢?继续搜索,发现下面这段代码:
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
if (outputValue != null && producibleMediaTypes.isEmpty()) {
throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
}
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
for (MediaType producibleType : producibleMediaTypes) {
if (requestedType.isCompatibleWith(producibleType)) {
compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (compatibleMediaTypes.isEmpty()) {
if (outputValue != null) {
throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
}
return;
}
List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
MediaType.sortBySpecificityAndQuality(mediaTypes);
MediaType selectedMediaType = null;
for (MediaType mediaType : mediaTypes) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
从这段代码我们可以清晰的看到 selectedMediaType 就是从 producibleMediaTypes 中获取的第一个可以与请求类型 requestedMediaTypes 中某个类型所相兼容的类型,而所谓的 producibleMediaTypes 就是在 FastJsonHttpMessageConverter 中空参构造方法中所设置的 SupportedMediaTypes
/**
* Returns the media types that can be produced:
* <ul>
* <li>The producible media types specified in the request mappings, or
* <li>Media types of configured converters that can write the specific return value, or
* <li>{@link MediaType#ALL}
* </ul>
* @since 4.2
*/
@SuppressWarnings("unchecked")
protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> valueClass, Type declaredType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
}
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter instanceof GenericHttpMessageConverter && declaredType != null) {
if (((GenericHttpMessageConverter<?>) converter).canWrite(declaredType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
return result;
}
else {
return Collections.singletonList(MediaType.ALL);
}
}
// FastJsonHttpMessageConverter 空参构造
public FastJsonHttpMessageConverter() {
super(MediaType.ALL);
}
千回万转,最终又回到了原点,这里设置的参数是 ALL ,点进 MediaType 中可以发现 ALL 的类型是 "*/*" ,也就是说匹配所有类型,因此 selectedMediaType 默认就为 requestedMediaTypes 中的第一个类型,即为 "text/html"
到这里,差不多一切都明了了,FastJsonHttpMessageConverter
既没有在指定 contentType 时设置 defaultCharset ,也没有在 supportContentTypes 中设置 contentType 的具体类型和编码,会乱码也就不足为奇了。
解决
通过对源码的一番探寻,我们可以很容易的找出解决方案出来,这里提供两种方法,可以根据个人爱好采用。
-
方案一,自定义 supportedMediaTypes
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter(); FastJsonConfig config = new FastJsonConfig(); config.setSerializerFeatures(SerializerFeature.WriteMapNullValue, // 空字段保留 SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.WriteNullNumberAsZero); converter.setFastJsonConfig(config); List<MediaType> types = new ArrayList<MediaType>(); types.add(MediaType.APPLICATION_JSON_UTF8); converter.setSupportedMediaTypes(types); converter.setDefaultCharset(Charset.forName("UTF-8")); converters.add(converter); }
方案二(针对 springboot ),在 application.properties 中添加
spring.http.encoding.force=true
这一行配置,表示强制使用 defaultCharset(因此也还是需要设置 defaultCharset)。
思考
两种解决方案,相比之下,第一种更明了也更灵活一点,毕竟 springboot 的思想便是零配置。springmvc 中默认的 AbstractJackson2HttpMessageConverter
便是采用了这种配置。
/**
* Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
* You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
* @see Jackson2ObjectMapperBuilder#json()
*/
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
}
配置都是一样的,为什么 FastJsonHttpMessageConverter
需要额外的配置,而 FastJsonHttpMessageConverter4
就不需要呢?通过继承关系我们就可以明白,FastJsonHttpMessageConverter
直接继承了 AbstractHttpMessageConverter
,而 FastJsonHttpMessageConverter4
则是继承了 AbstractHttpMessageConverter
的直接子类AbstractGenericHttpMessageConverter
,因此并没有重写 write 方法,也就是说 contentType 是由其父类 AbstractGenericHttpMessageConverter
配置的,代码如下:
/**
* This implementation sets the default headers by calling {@link #addDefaultHeaders},
* and then calls {@link #writeInternal}.
*/
public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
addDefaultHeaders(headers, t, contentType);
这句代码便是进行了 contentType 的设置,它是其父类 AbstractHttpMessageConverter
中的方法,如下
/**
* Add default headers to the output message.
* <p>This implementation delegates to {@link #getDefaultContentType(Object)} if a
* content type was not provided, set if necessary the default character set, calls
* {@link #getContentLength}, and sets the corresponding headers.
* @since 4.2
*/
protected void addDefaultHeaders(HttpHeaders headers, T t, MediaType contentType) throws IOException{
if (headers.getContentType() == null) {
MediaType contentTypeToUse = contentType;
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentTypeToUse = getDefaultContentType(t);
}
else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) {
MediaType mediaType = getDefaultContentType(t);
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
}
}
headers.setContentType(contentTypeToUse);
}
}
if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
}
同 FastJsonHttpMessageConverter
中的大体意思差不多,都是在进行 contentType 和 contentLenth 的设置,但在 contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
这句代码中,它给 contentType 指定了其编码类型,因此即使它的类型是 "text/html" ,但也能正常显示。
尾巴
虽然解决方案百度一下很快就能出来,但很多人都只是给了方案,而没有给原理,写这篇文章的目的不单单是为了解决问题,也顺便是为了探寻一下 springmvc 的执行流程,了解其内部对各个部件的调用流程,虽然花了点时间,不过所幸学到了不少的东西。
关于这个不知道算不算 bug 的 bug,我也在 github 上提了一个 issue ,希望能够有所改善吧。
---完---