场景
同样的两个子项目一个controller响应string返回的是正常的json,另一个则是被转义后的加了斜杠的json
public String getHSCounterDataByDate(@RequestParam(value = "startdate", required = false) Integer startdate,
@RequestParam(value = "enddate", required = false) Integer enddate) {
// 略......
return JSON.toJSONString(result, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty);
}
上面个controller返回的是被转移的json 类似 :== "{“code”:200
排查
起初怀疑是springmvc底层的json转换类将普通的string进行了再次的json化操作
然后查看了其他子项目发现响应同样是string的情况下并没有被转义
然后我就想从spring的HttpMessageConverter来下手看看到底是怎么回事
通过断点我发现
spring的源码里面这个地方会填充spring默认的json配置
我打了断点看到列表里面是一个 StringHttpMessageConverter
和 GsonHttpMessageConverter 这时我感觉不太对劲
立马去看了其他子项目的情况发现并没有gson这个转换类而是jackson,这样才是符合预期的,spring默认应该是jackson
然后我就通过断点找了半天…心累
最终我发现了这句话
AnyNestedCondition 1 matched 1 did not; NestedCondition on GsonHttpMessageConvertersConfiguration.PreferGsonOrJacksonAndJsonbUnavailableCondition.JacksonJsonbUnavailable NoneNestedConditions 0 matched 2 did not; NestedCondition on GsonHttpMessageConvertersConfiguration.JacksonAndJsonbUnavailableCondition.JsonbPreferred @ConditionalOnProperty (spring.mvc.converters.preferred-json-mapper=jsonb) found different value in property ‘spring.mvc.converters.preferred-json-mapper’; NestedCondition on GsonHttpMessageConvertersConfiguration.JacksonAndJsonbUnavailableCondition.JacksonAvailable @ConditionalOnBean (types: org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; SearchStrategy: all) did not find any beans of type org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; NestedCondition on GsonHttpMessageConvertersConfiguration.PreferGsonOrJacksonAndJsonbUnavailableCondition.GsonPreferred @ConditionalOnProperty (spring.mvc.converters.preferred-json-mapper=gson) found different value in property ‘spring.mvc.converters.preferred-json-mapper’
本人英语不好自行翻译吧
我猜想大致就是并没有配置jackson和jsonb,而是配了一个不可识别的配置所以导致spring最终选用了gson转换类
这时我赶紧去看配置文件发现
spring.mvc.converters.preferred-json-mapper=fastjson
这不纯纯捣乱吗
因为我看了源码并不支持这种配置
spring官方只支持jackson,gson,jsonb三种
其他的需要实现WebMvcConfigurer接口将对应的转换类add进总的列表
如果像上面一样preferred-json-mapper配置了fastjson这个配置,那么最终注入的其实是gson
重点
那为什么gson就会进行转义呢,这个时候就要说到一个header属性了
accept 就是请求头里面告诉服务端你想要接收的类型是什么
一般情况下我们都不会特殊指定这个字段
但是spring的服务端,在响应请求的时候会根据这个数据去获取满足条件的转换类
我们默认传"/*"的时候那么服务端就拿到了所有的满足return类型的转换类
并且根据他的类型进行排序如下图,范围小的在前面
这张是fastjson的时候spring对accept的处理顺序, gson应该也一样,会把application/json放在前面
spring会根据这个进行顺序遍历, 拿到当前服务所有的转换类(注: 转换类按顺序进行遍历),我们的gson会被放在最前面,具体看源码
所以gson对应的application/json满足,那么直接使用gson进行转换,转换后返回,那么结果就是被转义了
而如果使用jackson的话顺序是这样的
这个排序是由spring提供规则,各自的转换器实现自己的顺序,
不得不说一句,有多大能力干多大事,fastjson和gson处理string会转义就不要把自己放在前面,现实是揽的活多却干不好
转换器列表jackson的情况,可以看到jackson在后面
其他的自己看一下 AbstractMessageConverterMethodProcessor类
这样的话accept 就命中了text/plain那么就会从转换器列表中按顺序取出StringHttpMessageConverter进行转换数据,那么string就不会被再次转义了
所以最终就是
jsckson的时候响应string并不会再次转义而gson或者fastjson会
所以我想最好不要去改动spring默认的json转换类jsckson
可能平常大家用fastjson比较多,但是即使代码中使用就算了,在mvc这边不建议修改,不然可能会出一些想不到的问题
疑问
可能有的人会问 – 那么如果每次都是text/plain的话如果我直接返回对象不返回string岂不是最终会出问题,
结果是不会的
getProducibleMediaTypes(request, valueType, targetType)
spring的这行代码决定了mediaTypesToUse的列表数据, 这个方法会根据你的controller的实际return类型和你定义的返回类型双重决定最终可以返回的类型列表
如果你定义的返回是对象的情况mediaTypesToUse根本不会存在text/plain
看到这里不得不感叹一句,spring牛逼
最终我删掉了preferred-json-mapper这个配置,响应的结果就正常了,此时spring就走了默认的jackson
要说为什么随便配一个东西spring的json框架就变成了gson
这个我想可能是历史原因吧, 毕竟最开始spring好像支持的是gosn如果我没记错的话
同样的不建议在controller中直接返回string,类型的json,这不是脱裤子放屁吗
但是由于是历史原因,这些代码已经写了很久了,我并不能去修改它,
而上个版本进行了框架结构的大升级,另一位同事配了preferred-json-mapper=fastjson 最终屎盆子要扣到我的网关中台头上,我气不过于是就翻了翻看看
如果非要选择fastjson或者gson, 并且要保重string类型正常返回
先上代码
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd");
config.setCharset(Charset.forName("UTF-8"));
config.setSerializerFeatures(
//输出类名
SerializerFeature.WriteClassName,
//输出map中value为null的数据
SerializerFeature.WriteMapNullValue,
//json格式化
SerializerFeature.PrettyFormat,
//输出空list为[],而不是null
SerializerFeature.WriteNullListAsEmpty,
//输出空string为"",而不是null
SerializerFeature.WriteNullStringAsEmpty
);
converter.setFastJsonConfig(config);
ArrayList<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.APPLICATION_JSON);
converter.setSupportedMediaTypes(mediaTypes);
for (int i = 0; i < converters.size(); i++) {
HttpMessageConverter<?> httpMessageConverter = converters.get(i);
if (httpMessageConverter instanceof MappingJackson2HttpMessageConverter) {
converters.remove(i);
converters.add(i,converter);
}
}
}
}
必须通过实现的这种方式来添加fastjson, 另一种注入的方式由于spring不支持fastjson最终会导致在排序的时候fastjson总是在前边,这样方式是追加在列表后面,提换jackson
具体参见源码MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
会涉及到排序,因为string和fastjson的q是一样的(理解为参与排序的值)
那么谁原本在前面,排序之后还是在前边,
所以我们上面就是为了让fastjson排在string后面
上面说的spring不支持fastjson才这么操作,gson可以用规范的方式
那么spring支持gson这么操作不够规范
1 实际上需要排除jackson的相关包
com.fasterxml.jackson.databind.ObjectMapper
2 添加对应配置
spring.mvc.converters.preferred-json-mapper=gson
这两步即可,这个我没有实践,理论上是可以的,
我尝试了一下,由于引用jackson的jar的地方太多了,不止spring相关的,所以我最后放弃了,这样排除jar下去最终可能引起其他问题,上面这种方式比较靠谱影响面较小
最后说一下传统mvc项目的处理办法供参考,可以对应boot项目
bean顺序很重要
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<list>
<value>text/plain;charset=utf-8</value>
<value>application/json;charset=UTF-8</value>
<value>application/soap+xml; charset=utf-8</value>
<value>text/html;charset=UTF-8</value>
<value>application/x-www-form-urlencoded;charset=UTF-8</value>
</list>
</property>
</bean>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</list>
</property>
</bean>
Respect !!!