浅析RestTemplate的Jackson消息转换器

一、前因后果

之前有写过一个关于上游接口不按规范导致的 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 即便在构造方法中设置了,RestTemplateBuilderconfigure 也会将之再次覆盖。

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 是一定存在的,但也存在其他的消息转换器,至于为何最终反序列化的时候会使用它,打断点看一下 RestTemplateexcute 方法就可以得出结果,最终将消息由 json 转换为实体类的是 HttpMessageConverterExtractorextractData 方法,代码就不贴出来了,感兴趣的可以自行打断点看一下。

MappingJackson2HttpMessageConverter 转换消息,使用的是 ObjectMapperObjectMapper 是可以自定义某些特性的,其中的反序列特性有这么两个 ACCEPT_EMPTY_STRING_AS_NULL_OBJECTACCEPT_SINGLE_VALUE_AS_ARRAY 刚好可以满足之前提到的两个问题。前者将空串视作空对象(null),刚好可以解决第二个问题,而后者,对于类型声明是数组,但是只有一个值的时候可以自动转换为数组,刚好可以解决第一个问题,关键就在于如何让它生效。

前面提到了,因为使用的是 RestTemplateBuilder 来进行配置,所以不管 RestTemplate 的构造方法中如何进行定义 messageConverters,最后都会被 RestTemplateBuilderconfigure 方法给覆盖,所以要弄清楚 RestTemplateBuildermessageConverters 中的 MappingJackson2HttpMessageConverter 是怎么定义的。

首先,RestTemplateBuilderBean 定义如下

@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 如下,可以看到, RestTemplateBuildermessageConverters 是来自 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;
}

再找到 RestTemplateBuilderConfigurerBean 定义

@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);
}

再来看注入的这个 ObjectMapperBean 定义,配置类是 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的Jackson消息转换器_第1张图片

RestTemplate 中多了一个,不用管,最终解析 Json 的时候使用的是第一个。

浅析RestTemplate的Jackson消息转换器_第2张图片

使用IEDAEvaluate查看一下,可以看到,第7个的反序列化特性是生效的,如果看第8个,那就是默认的false了,不过不要紧,按顺序来,只要前面的没问题就行。

浅析RestTemplate的Jackson消息转换器_第3张图片

接下来再来测试之前提到的两个问题,就可以发现问题都解决了,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 用到的地方也不太多,仅仅修改这两个反序列属性也不会对其他地方造成影响。

总结:自己动手调试一遍的收获是远远高于百度搜索的。

你可能感兴趣的:(后端技术,spring,boot)