之前有写过一个关于上游接口不按规范导致的 Jackson 序列化问题(见https://blog.csdn.net/Remember_Z/article/details/119737998 或 https://www.yangguirong.com/archives/ji-yi-ci-fan-xu-lie-hua-yi-chang),大概就是接口文档中标明了某个字段是数组格式,但是当数组中只有一个对象时,这个属性会直接以对象的形式的返回,如下两种返回形式
// 当数组长度大于1时,返回值与文档是一致的
{
"users": [
{
"name": "Jack",
"age": 18
},
{
"name": "John",
"age": 20
}
]
}
// 而当数组长度等于1时,返回格式就是下面的形式了
{
"users": {
"name": "Jack",
"age": 18
}
}
这还不是全部的问题,还有一种情况,假设 user 有一个属性 contact,是一个对象,存放的是用户的联系信息,正常情况下返回的 user 信息如下
{
"name": "Jack",
"age": 18,
"contact": {
"phone": "158****8888",
"email": "[email protected]"
}
}
而如果某个用户联系信息为空,则接口有可能会返回 null,这是最理想的情况,也是一般做法,但是哈,他们的接口不一定会真的返回一个 null 给你,它有可能返回的是一个空串,如下
{
"name": "Jack",
"age": 18,
"contact": ""
}
这种情况下,我正常的用 DTO
对象接收就有问题了。我之前的处理是通过自定义反序列器的形式进行手动转换,在字段比较少的时候还好,可以逐个加一下(见之前博客的说明)。但是如果字段较多,总不能一个个写吧,这种情况下,可以定义一个通用的泛型数组转换器,至于泛型擦除,可以通过额外定义一个辅助注解来进行解决
反序列化器
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.h3c.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ReflectionUtils;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 对象转list反序列化器
*
* 避免泛型擦除,借助{@link ObjListDeserializerType}注解来取类型
*
* @author yang guirong
*/
@Slf4j
public class ObjListDeserializer extends JsonDeserializer<List<?>> {
private final String emptyJsonObjStr = "{}";
private final String[] baseDataType = {"Byte", "Short", "Integer", "Long", "Float", "Double", "Boolean", "Character", "String"};
public ObjListDeserializer() {
}
@Override
public List<?> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
List<Object> list = new ArrayList<>();
Class<?> aClass = jsonParser.getParsingContext().getCurrentValue().getClass();
String fieldName = jsonParser.getParsingContext().getCurrentName();
Field field = ReflectionUtils.findField(aClass, fieldName);
if (field == null) {
Field[] fields = aClass.getDeclaredFields();
field = Arrays.stream(fields).filter(f -> {
JsonProperty property = f.getDeclaredAnnotation(JsonProperty.class);
return property != null && fieldName.equals(property.value());
}).findFirst().orElse(null);
if (field == null) {
log.error("[deserialize]错误,类<{}>不存在名称为<{}>属性!", aClass.getName(), fieldName);
return list;
}
}
ObjListDeserializerType objListDeserializerType = field.getDeclaredAnnotation(ObjListDeserializerType.class);
if (objListDeserializerType == null) {
log.error("[deserialize]错误,类<{}>中名称为<{}>属性不存在ObjListDeserializerType注解!", aClass.getName(), fieldName);
return list;
}
Class<?> type = objListDeserializerType.value();
try {
if (node instanceof TextNode && StrUtil.isBlank(node.textValue())) {
return list;
} else if (node.isArray()) {
return JsonUtil.toList(node.toString(), type);
} else if (node.isObject()) {
String str = node.toString();
if (isBaseDataType(type) && emptyJsonObjStr.equals(str)) {
return list;
}
Object dto = JsonUtil.toObj(str, type);
list.add(dto);
return list;
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
@Override
public List<?> getNullValue(DeserializationContext ctx) {
return new ArrayList<>();
}
private boolean isBaseDataType(Class<?> type) {
for (String str : baseDataType) {
if (str.equals(type.getSimpleName())) {
return true;
}
}
return false;
}
}
泛型辅助注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface ObjListDeserializerType {
Class<?> value();
}
然后上面的例子中就可以通过如下的方式进行处理
@Data
public class UsersDTO {
@ObjListDeserializerType(UserItem.class)
@JsonDeserialize(using = ObjListDeserializer.class)
private List<UserItem> users;
@Data
public static class UserItem {
private String name;
private Integer age;
private ContactDTO contact;
@Data
public static class ContactDTO {
private String phone;
private String email;
}
}
}
这样就解决了第一种情况,但是第二种情况,也就是空串的问题还没有解决。网上有人说是用 @JsonInclude
,但实际上是不行的,这个注解是用于序列化的,不是用于反序列化的。当然这种情况也可以使用自定义反序列器的方式进行解决,但是字段多了之后,还得一个个加,稍不留意就漏了某个字段。所以还得从全局的反序列策略来进行修改。
首先,我在对接上游接口的时候,选择是通过 RestTemplate
直接调用的接口,所以肯定是要从这个类进行入手的。
我的 RestTemplate
配置类如下
@Configuration
public class RestTemplateConfig {
@Bean
public ClientHttpRequestFactory clientHttpRequestFactory() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(5000);
factory.setReadTimeout(10000);
return factory;
}
@Bean
@ConditionalOnClass(RestTemplate.class)
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
RestTemplate restTemplate = restTemplateBuilder.build();
restTemplate.setRequestFactory(clientHttpRequestFactory());
return restTemplate;
}
}
使用的是 RestTemplateBuilder
来进行构建(关于这个 Bean 的配置可以看配置类 RestTemplateAutoConfiguration
),其中 build
方法如下
public RestTemplate build() {
return configure(new RestTemplate());
}
可以看到实例化 RestTemplate
用的是无参构造方法,其实这个构造方法可以不用看,因为其中的 messageConverters
即便在构造方法中设置了,RestTemplateBuilder
的 configure
也会将之再次覆盖。
public <T extends RestTemplate> T configure(T restTemplate) {
...
if (!CollectionUtils.isEmpty(this.messageConverters)) {
restTemplate.setMessageConverters(new ArrayList<>(this.messageConverters));
}
...
return restTemplate;
}
而在项目运行中最终处理消息的是 MappingJackson2HttpMessageConverter
,因为 spring-boot-starter-web
中包含了 spring-boot-starter-json
,而后者就包含 jackson-databind
等依赖,所以 MappingJackson2HttpMessageConverter
是一定存在的,但也存在其他的消息转换器,至于为何最终反序列化的时候会使用它,打断点看一下 RestTemplate
的 excute
方法就可以得出结果,最终将消息由 json
转换为实体类的是 HttpMessageConverterExtractor
的 extractData
方法,代码就不贴出来了,感兴趣的可以自行打断点看一下。
而 MappingJackson2HttpMessageConverter
转换消息,使用的是 ObjectMapper
,ObjectMapper
是可以自定义某些特性的,其中的反序列特性有这么两个 ACCEPT_EMPTY_STRING_AS_NULL_OBJECT
和 ACCEPT_SINGLE_VALUE_AS_ARRAY
刚好可以满足之前提到的两个问题。前者将空串视作空对象(null
),刚好可以解决第二个问题,而后者,对于类型声明是数组,但是只有一个值的时候可以自动转换为数组,刚好可以解决第一个问题,关键就在于如何让它生效。
前面提到了,因为使用的是 RestTemplateBuilder
来进行配置,所以不管 RestTemplate
的构造方法中如何进行定义 messageConverters
,最后都会被 RestTemplateBuilder
的 configure
方法给覆盖,所以要弄清楚 RestTemplateBuilder
中 messageConverters
中的 MappingJackson2HttpMessageConverter
是怎么定义的。
首先,RestTemplateBuilder
的 Bean
定义如下
@Bean
@Lazy
@ConditionalOnMissingBean
public RestTemplateBuilder restTemplateBuilder(RestTemplateBuilderConfigurer restTemplateBuilderConfigurer) {
RestTemplateBuilder builder = new RestTemplateBuilder();
return restTemplateBuilderConfigurer.configure(builder);
}
它的构造方法如下,messageConverters
是空的,不是我想要的结果
public RestTemplateBuilder(RestTemplateCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
this.requestFactoryCustomizer = new RequestFactoryCustomizer();
this.detectRequestFactory = true;
this.rootUri = null;
this.messageConverters = null;
this.interceptors = Collections.emptySet();
this.requestFactory = null;
this.uriTemplateHandler = null;
this.errorHandler = null;
this.basicAuthentication = null;
this.defaultHeaders = Collections.emptyMap();
this.customizers = copiedSetOf(customizers);
this.requestCustomizers = Collections.emptySet();
}
而 configure
如下,可以看到, RestTemplateBuilder
的 messageConverters
是来自 RestTemplateBuilderConfigurer
public RestTemplateBuilder configure(RestTemplateBuilder builder) {
if (this.httpMessageConverters != null) {
builder = builder.messageConverters(this.httpMessageConverters.getConverters());
}
builder = addCustomizers(builder, this.restTemplateCustomizers, RestTemplateBuilder::customizers);
builder = addCustomizers(builder, this.restTemplateRequestCustomizers, RestTemplateBuilder::requestCustomizers);
return builder;
}
再找到 RestTemplateBuilderConfigurer
的 Bean
定义
@Bean
@Lazy
@ConditionalOnMissingBean
public RestTemplateBuilderConfigurer restTemplateBuilderConfigurer(
ObjectProvider<HttpMessageConverters> messageConverters,
ObjectProvider<RestTemplateCustomizer> restTemplateCustomizers,
ObjectProvider<RestTemplateRequestCustomizer<?>> restTemplateRequestCustomizers) {
RestTemplateBuilderConfigurer configurer = new RestTemplateBuilderConfigurer();
configurer.setHttpMessageConverters(messageConverters.getIfUnique());
configurer.setRestTemplateCustomizers(restTemplateCustomizers.orderedStream().collect(Collectors.toList()));
configurer.setRestTemplateRequestCustomizers(
restTemplateRequestCustomizers.orderedStream().collect(Collectors.toList()));
return configurer;
}
得,接着找 HttpMessageConverters
,这个类相当于是 HttpMessageConverter
的一个集合,同时又提供了一些额外的操作
public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>>
再接着找这个 Bean
的定义,来到 HttpMessageConvertersAutoConfiguration
配置类,部分代码如下
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(HttpMessageConverter.class)
@Conditional(NotReactiveWebApplicationCondition.class)
@AutoConfigureAfter({ GsonAutoConfiguration.class, JacksonAutoConfiguration.class, JsonbAutoConfiguration.class })
@Import({ JacksonHttpMessageConvertersConfiguration.class, GsonHttpMessageConvertersConfiguration.class,
JsonbHttpMessageConvertersConfiguration.class })
public class HttpMessageConvertersAutoConfiguration {
static final String PREFERRED_MAPPER_PROPERTY = "spring.mvc.converters.preferred-json-mapper";
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
...
}
可以看到 @Import
导入了 JacksonHttpMessageConvertersConfiguration
,这个就是最终答案。点进去,部分代码如下
@Bean
@ConditionalOnMissingBean(value = MappingJackson2HttpMessageConverter.class,
ignoredType = { "org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter", "org.springframework.data.rest.webmvc.alps.AlpsJsonHttpMessageConverter" })
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
return new MappingJackson2HttpMessageConverter(objectMapper);
}
再来看注入的这个 ObjectMapper
的 Bean
定义,配置类是 JacksonAutoConfiguration
,具体的 Bean
定义是在其内部配置类中
@Bean
@Primary
@ConditionalOnMissingBean
ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
再看 Jackson2ObjectMapperBuilder
,依然是在 JacksonAutoConfiguration
中。
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder(ApplicationContext applicationContext,
List<Jackson2ObjectMapperBuilderCustomizer> customizers) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.applicationContext(applicationContext);
customize(builder, customizers);
return builder;
}
通过 customize
来设置 builder
的属性,Jackson2ObjectMapperBuilderCustomizer
是一个接口,其实现类为 StandardJackson2ObjectMapperBuilderCustomizer
,其 Bean
定义如下
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Jackson2ObjectMapperBuilder.class)
@EnableConfigurationProperties(JacksonProperties.class)
static class Jackson2ObjectMapperBuilderCustomizerConfiguration {
@Bean
StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer(
ApplicationContext applicationContext, JacksonProperties jacksonProperties) {
return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, jacksonProperties);
}
..
}
可以看到,注入了属性配置类 JacksonProperties
,而上面提到的 customize
方法具体实现如下
@Override
public void customize(Jackson2ObjectMapperBuilder builder) {
if (this.jacksonProperties.getDefaultPropertyInclusion() != null) {
builder.serializationInclusion(this.jacksonProperties.getDefaultPropertyInclusion());
}
if (this.jacksonProperties.getTimeZone() != null) {
builder.timeZone(this.jacksonProperties.getTimeZone());
}
configureFeatures(builder, FEATURE_DEFAULTS);
configureVisibility(builder, this.jacksonProperties.getVisibility());
configureFeatures(builder, this.jacksonProperties.getDeserialization());
configureFeatures(builder, this.jacksonProperties.getSerialization());
configureFeatures(builder, this.jacksonProperties.getMapper());
configureFeatures(builder, this.jacksonProperties.getParser());
configureFeatures(builder, this.jacksonProperties.getGenerator());
configureDateFormat(builder);
configurePropertyNamingStrategy(builder);
configureModules(builder);
configureLocale(builder);
configureDefaultLeniency(builder);
configureConstructorDetector(builder);
}
看到这里,一切明了了,最终的答案就在 JacksonProperties
这个属性配置类上,其定义如下,可以看到是 spring.jackson
前缀的配置,很熟悉有木有,这下终于到底了。。。
@ConfigurationProperties(prefix = "spring.jackson")
public class JacksonProperties
看了这么多代码,原来只需要在配置文件里加一下就行了,不得不感叹,spring
封装的真的是太多了,找起来真费劲。
接下来动手环节,在 application.yml
配置文件中添加如下配置
spring:
jackson:
deserialization:
accept-empty-string-as-null-object: true
accept-single-value-as-array: true
启动,打断点可以看到,这两个属性设置进来了
RestTemplate
中多了一个,不用管,最终解析 Json
的时候使用的是第一个。
使用IEDA
的Evaluate
查看一下,可以看到,第7个的反序列化特性是生效的,如果看第8个,那就是默认的false了,不过不要紧,按顺序来,只要前面的没问题就行。
接下来再来测试之前提到的两个问题,就可以发现问题都解决了,perfect。
不过我转念一想,这样修改的话,也相当于修改了标注了 @Primary
注解的 ObjectMapper
这个 Bean
,或许也会对其他方面造成影响,而我这里明显只需要修改我这一个 RestTemplate
的配置就行了。
既然我只需要改 RestTemplate
中的反序列方式,那之前的方法就不用了,用下面的第二种方法,将影响范围降到最低。
@Bean
@ConditionalOnClass(RestTemplate.class)
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
RestTemplate restTemplate = restTemplateBuilder.build();
restTemplate.setRequestFactory(clientHttpRequestFactory());
for (HttpMessageConverter<?> messageConverter : restTemplate.getMessageConverters()) {
if (messageConverter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = (MappingJackson2HttpMessageConverter) messageConverter;
ObjectMapper mapper = mappingJackson2HttpMessageConverter.getObjectMapper();
mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
}
}
return restTemplate;
}
如上,通过一个循环找出所有 MappingJackson2HttpMessageConverter
(之前的截图提到了,会有两个,但是最终执行的是第一个,第二个不影响),将其属性拷贝给一个新的对象,对新的对象进行修改,这样改就可以将影响范围降低。这里如果直接对原始的对象进行修改的话,与第一种方式是一样的,所以不推荐。
不过最终我其实还是使用的第一种方法,毕竟这是个小项目,ObjectMapper
用到的地方也不太多,仅仅修改这两个反序列属性也不会对其他地方造成影响。
总结:自己动手调试一遍的收获是远远高于百度搜索的。