解决 Spring Framework 常见的序列化和反序列化问题

解决 Spring Framework 常见的序列化和反序列化问题

前提条件

框架使用 Jackson 作为主要的序列化工具,spring-boot-starter-web 依赖中默认集成了 Jackson 的依赖包;以下示例均针对 Java8 新加入的 Local 时间类型,其他类型请参考例子自行修改;

全局 JSON 数据的序列化和反序列化

修改 Jackson 的 ObjectMapper 配置参数以及指定对应类型的编码和解码方式即可;编码和解码方式可自定义;

@Bean
public ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    // 禁用默认的时间格式序列化方式
    objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
    // LocalDateTime 系列序列化和反序列化模块 
    JavaTimeModule javaTimeModule = new JavaTimeModule();
    javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
    javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
    javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
    javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN)));
    javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN)));
    javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN)));
    // Date 序列化 
    javaTimeModule.addSerializer(Date.class, new JsonSerializer<Date>() {
        @Override
        public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
            jsonGenerator.writeString(DateUtil.format(date, DatePattern.NORM_DATETIME_FORMAT));
        }
    });
    // Date 反序列化 
    javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<Date>() {
        @SneakyThrows
        @Override
        public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) {
            return DateUtil.parse(jsonParser.getText());
        }
    });
    objectMapper.registerModule(javaTimeModule);
    return objectMapper;
}

说明:ObjectMapper 主要是针对请求体 body 部分数据的编码和解码

WebMVC GET/PUT/DELETE 请求参数序列化
@Bean
public Converter<String, LocalDate> localDateConverter() {
    return new Converter<String, LocalDate>() {
        @SneakyThrows
        @Override
        public LocalDate convert(@NonNull String source) {
            return LocalDate.parse(source, DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN));
        }
    };
}

@Bean
public Converter<String, LocalDateTime> localDateTimeConverter() {
    return new Converter<String, LocalDateTime>() {
        @SneakyThrows
        @Override
        public LocalDateTime convert(@NonNull String source) {
            return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN));
        }
    };
}

@Bean
public Converter<String, LocalTime> localTimeConverter() {
    return new Converter<String, LocalTime>() {
        @SneakyThrows
        @Override
        public LocalTime convert(@NonNull String source) {
            return LocalTime.parse(source, DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN));
        }
    };
}

@Bean
public Converter<String, Date> dateConverter() {
    return new Converter<String, Date>() {
        @SneakyThrows
        @Override
        public Date convert(@NonNull String source) {
            return DateUtil.parse(source);
        }
    };
}

说明:主要针对 web 请求中非 body 部分的参数的解码,可以发现转换类型都是 String -> ;此时可用 Parser 代替 Converter;
为什么只有反序列化没有序列呢?—— 因为后端返回数据一般使用 json 形式传输,json 的序列化已经在上一步全局配置

Feign 的 GET 请求传输对象

Feign 的底层实现基于 Http 通讯,针对 GET 请求的接口会被替换成 POST 的请求方式,可以引入以下依赖解决请求方式变化的报错,还需要配置 feign.httpclient.enabled=true

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

以上依赖只是替换了 java 中默认的 http 通讯,但是对应数据的封装依旧封装在 body 中,所以需要在 feign 的服务端接口参数上添加 @RequestBody,否则映射不到请求参数的数据;

Feign 的 GET/PUT/DELETE 传输路径参数

feign 接口服务端在接收参数时会使用 webMVC 的转换器反序列化参数,但是客户端在发送请求的时候并不会使用 webMVC 的转换器序列化参数;所以如果序列化和反序列化方法不一致就会导致 feign 接口报错;上述针对Local 时间类型的反序列化是将 “yyyy-MM-dd HH:mm:ss” 格式的字符串解码成 Local 时间类型,那么序列化肯定是将 Local 时间类型转换成 “yyyy-MM-dd HH:mm:ss” 格式的字符串,然而默认的 TemporalAccessorPrinter 并没有如期望的格式去序列化时间;所以这一部分比较复杂,除需要注入以上 webMVC 的转换器外,还需要加入以下 bean 保证序列化和反序列化一致

@Bean
public Contract feignContract(ConversionService feignConversionService) {
    return new SpringMvcContract(CollUtil.newArrayList(), feignConversionService);
}

@Bean
public FormattingConversionService feignConversionService() {
    DateTimeFormatter df = DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN);
    DateTimeFormatter tf = DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN);
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN);
    FormattingConversionService conversionService = new DefaultFormattingConversionService();
    conversionService.addFormatterForFieldType(LocalDate.class, new TemporalAccessorPrinter(df), new TemporalAccessorParser(LocalDate.class, df));
    conversionService.addFormatterForFieldType(LocalTime.class, new TemporalAccessorPrinter(tf), new TemporalAccessorParser(LocalTime.class, tf));
    conversionService.addFormatterForFieldType(LocalDateTime.class, new TemporalAccessorPrinter(dtf), new TemporalAccessorParser(LocalDateTime.class, dtf));
    return conversionService;
}

注意网上说的 feign 的编码器和解码器是针对 feign 传输 json 对象时才生效的配置,这里的情况不属于 json 对象的传输;附带重写 feign 针对 json 数据的编码和解码,其中 objectMapper 即本篇第一步的 bean 对象

/** * feign 接口的编码器 */
@Bean
public Encoder encoder() {
    HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
    ObjectFactory messageConverters = () -> new HttpMessageConverters(converter);
    return new SpringEncoder(messageConverters);
}

/** * feign 接口的解码器 */
@Bean
public Decoder decoder() {
    HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
    ObjectFactory messageConverters = () -> new HttpMessageConverters(converter);
    return new SpringDecoder(messageConverters);
}
RedisTemplate 的序列化和反序列化

redisTemplate 之所以需要设置指定的序列化方式是因为默认的序列化方法保存数据时是以 byte[] 的形式保存的,可视化比较差;之所以设置反序列化是为了保证和序列化方式一致,以防止数据编码后解码错误;

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(connectionFactory);
    Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
    ObjectMapper objectMapper = new ObjectMapper();
    // 指定java对象可序列化的内容范围;PropertyAccessor.ALL表示当前指定为所有(字段,get\set 方法,构造器等);JsonAutoDetect.Visibility.ANY表示修饰符为所有(public\private\protected) 
    objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
    // key采用String的序列化方式 
    template.setKeySerializer(stringRedisSerializer);
    // hash的key也采用String的序列化方式 
    template.setHashKeySerializer(stringRedisSerializer);
    // value序列化方式采用jackson 
    template.setValueSerializer(jackson2JsonRedisSerializer);
    // hash的value序列化方式采用jackson 
    template.setHashValueSerializer(jackson2JsonRedisSerializer);
    // 其他默认 
    template.afterPropertiesSet();
    return template;
}

说明:其中有一行代码 objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY) 比较重要,如果你redis直接保存的整个对象,那么取出数据后可以使用强转类型直接转换;否则会报错 ” LinkedHashMap can not convert to <T> “类似的错误