fix bug:解决在Spring项目实践中LocalDateTime无法序列化反序列化的问题

Posted 爱叨叨的程序狗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了fix bug:解决在Spring项目实践中LocalDateTime无法序列化反序列化的问题相关的知识,希望对你有一定的参考价值。

概述-本文的意义

JDK 8发行已久,其中不乏一些在实际编码过程中是十分好用的新特性,如JDK 8中的时间特性亦是如此,但是在Spring企业开发中,往往会遇到LocalDateTime无法序列化/反序列化的问题,原因是LocalDateTime类型的值在当前的JSON工具中并没有特定的模式去解析该类型。那么解决该问题最简单的方式是使用@JsonFormat固定一个pattern即可。当时这个注解存在的弊端即为每一个LocalDateTime类型的参数上都需要一个注解,所以当代码量较大时,工作量就会变大,并且容易因疏忽而出现的Bug,那么使用全局就显得简明很多。


两种方式实现全局配置

两种配置方式

  • Jackson配置方式
  • FastJson配置方式

这两者均可实现LocalDateTime类型的序列化/反序列化的目的,使用哪种方式根据读者项目实际情况选择即可。

两种方式的共同原理

最基础的SpringBoot工程中默认集成了Jackson序列化/反序列化工具,那么在当前版本的Jackson亦或是FastJson中默认无法解析LocalDateTime类型的数据,但是这两种工具均支持自定义序列化/反序列化配置,那么我们自定义一个LocalDateTime类型的序列化/反序列化方式,并将其注册为Spring中的一个组件即可。


转换工具

LocalDateTimeGetConverter.java
@Configuration
public class LocalDateTimeGetConverter {
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return new Converter<String, LocalDateTime>() {
            @Override
            public LocalDateTime convert(String source) {
                return LocalDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(source)), ZoneId.systemDefault());
            }
        };
    }
}

实现一:Jackson

JacksonConfig.java
@Configuration
public class JacksonConfig {

    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> {
            builder.locale(Locale.CHINA);
            builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
            builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");

            JavaTimeModule javaTimeModule = new JavaTimeModule();
            javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateEnum.COMMON_DATE_TIME.getDateTimeFormatter()));
            javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateEnum.COMMON_MONTH_DAY.getDateTimeFormatter()));
            javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateEnum.COLON_DELIMITED_TIME.getDateTimeFormatter()));
            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateEnum.COMMON_DATE_TIME.getDateTimeFormatter()));
            javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateEnum.COMMON_MONTH_DAY.getDateTimeFormatter()));
            javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateEnum.COLON_DELIMITED_TIME.getDateTimeFormatter()));

            builder.modules(javaTimeModule);
        };
    }


    @Bean
    @Primary
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder)
    {
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();

        // 通过该方法对mapper对象进行设置,所有序列化的对象都将按改规则进行系列化
        // Include.Include.ALWAYS 默认
        // Include.NON_DEFAULT 属性为默认值不序列化
        // Include.NON_EMPTY 属性为 空("") 或者为 NULL 都不序列化,则返回的json是没有这个字段的
        // Include.NON_NULL 属性为NULL 不序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // 允许出现特殊字符和转义符
        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true);
        // 允许出现单引号
        objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        SimpleModule simpleModule = new SimpleModule();
        /**
         *  将Long,BigInteger序列化的时候,转化为String
         */
        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
        simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
        simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);

        objectMapper.registerModule(simpleModule);

        // 将工具类中的 objectMapper 换为 Spring 中的 objectMapper
        JacksonUtil.objectMapper = objectMapper;
        return objectMapper;
    }
}
DateEnum.java
@Getter
@AllArgsConstructor
public enum DateEnum {

    /**
     * 时间格式
     */
    COMMON_DATE_TIME(0, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault())),
    COMMON_SHORT_DATE(1, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault())),
    COMMON_MONTH_DAY(2, DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault())),
    COMMON_MONTH(3, DateTimeFormatter.ofPattern("yyyy-MM").withZone(ZoneId.systemDefault())),
    YEAR(4, DateTimeFormatter.ofPattern("yyyy").withZone(ZoneId.systemDefault())),
    COLON_DELIMITED_TIME(5, DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault())),
    COLON_DELIMITED_SHORT_TIME(6, DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())),
    CHINESE_DATE_TIME(7, DateTimeFormatter.ofPattern("yyyy年MM月dd日HH时mm分ss秒").withZone(ZoneId.systemDefault())),
    CHINESE_SHORT_DATE(8, DateTimeFormatter.ofPattern("yyyy年MM月dd日HH时mm分").withZone(ZoneId.systemDefault())),
    CHINESE_DATE(9, DateTimeFormatter.ofPattern("yyyy年MM月dd日").withZone(ZoneId.systemDefault())),
    CHINESE_MONTH(10, DateTimeFormatter.ofPattern("yyyy年MM月").withZone(ZoneId.systemDefault())),
    CHINESE_YEAR(11, DateTimeFormatter.ofPattern("yyyy年").withZone(ZoneId.systemDefault())),
    CHINESE_TIME(12, DateTimeFormatter.ofPattern("HH时mm分ss秒").withZone(ZoneId.systemDefault())),
    CHINESE_SHORT_TIME(13, DateTimeFormatter.ofPattern("HH时mm分").withZone(ZoneId.systemDefault()));

    private final Integer code;
    private final DateTimeFormatter dateTimeFormatter;


    public static DateEnum findByCode(int code){
        return Stream.of(values())
                .filter(e -> e.getCode().equals(code))
                .findAny()
                .orElse(null);
    }

}
JacksonUtil.java
@Slf4j
public class JacksonUtil {

    public static ObjectMapper objectMapper = new ObjectMapper();


    /**
     * Java对象转JSON字符串
     *
     * @param object
     * @return
     */
    public static String toJsonString(Object object) {
        try {
            return objectMapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            log.error("The JacksonUtil toJsonString is error : \\n", e);
            throw new RuntimeException();
        }
    }

    /**
     * Java对象转JSON字符串 - 美化输出
     *
     * @param object
     * @return
     */
    public static String toJsonStringWithPretty(Object object) {
        try {
            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
        } catch (JsonProcessingException e) {
            log.error("The JacksonUtil toJsonString is error : \\n", e);
            throw new RuntimeException();
        }
    }


    /**
     * Java对象转byte数组
     *
     * @param object
     * @return
     */
    public static byte[] toJsonBytes(Object object) {
        try {
            return objectMapper.writeValueAsBytes(object);
        } catch (JsonProcessingException e) {
            log.error("The JacksonUtil toJsonBytes is error : \\n", e);
            throw new RuntimeException();
        }
    }


    /**
     * JSON字符串转对象
     *
     * @param json
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T parseObject(String json, Class<T> clazz) {
        try {
            return objectMapper.readValue(json, clazz);
        } catch (Exception e) {
            log.error("The JacksonUtil parseObject is error, json str is {}, class name is {} \\n", json, clazz.getName(), e);
            throw new RuntimeException();
        }
    }

    /**
     * JSON字符串转List集合
     *
     * @param json
     * @param elementClasses
     * @param <T>
     * @return
     */
    @SafeVarargs
    public static <T> List<T> parseList(String json, Class<T>... elementClasses) {
        try {
            return objectMapper.readValue(json, getCollectionType(objectMapper, List.class, elementClasses));
        } catch (Exception e) {
            log.error("The JacksonUtil parseList is error, json str is {}, element class name is {} \\n",
                    json, elementClasses.getClass().getName(), e);
            throw new RuntimeException();
        }
    }


    /**
     * JSON字符串转Set集合
     *
     * @param json
     * @param elementClasses
     * @param <T>
     * @return
     */
    @SafeVarargs
    public static <T> Set<T> parseSet(String json, Class<T>... elementClasses) {
        try {
            return objectMapper.readValue(json, getCollectionType(objectMapper, Set.class, elementClasses));
        } catch (Exception e) {
            log.error("The JacksonUtil parseSet is error, json str is {}, element class name is {} \\n",
                    json, elementClasses.getClass().getName(), e);
            throw new RuntimeException();
        }
    }

    /**
     * JSON字符串转Collection集合
     *
     * @param json
     * @param elementClasses
     * @param <T>
     * @return
     */
    @SafeVarargs
    public static <T> Collection<T> parseCollection(String json, Class<T>... elementClasses) {
        try {
            return objectMapper.readValue(json, getCollectionType(objectMapper, Collection.class, elementClasses));
        } catch (Exception e) {
            log.error("The JacksonUtil parseCollection is error, json str is {}, element class name is {} \\n",
                    json, elementClasses.getClass().getName(), e);
            throw new RuntimeException();
        }
    }

    /**
     * 获取泛型的Collection Type
     *
     * @param collectionClass 泛型的Collection
     * @param elementClasses  元素类
     * @return JavaType Java类型
     * @since 1.0
     */
    public static JavaType getCollectionType(ObjectMapper mapper, Class<?> collectionClass, Class<?>... elementClasses) {
        return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }
}

测试Jackson方式

Get方式请求

Get请求时传入时间戳,后端以Long类型接入,而后使用上文的自定义的转换工具Long类型转换为LocalDateTime即可。

@ApiOperation(value = "用户测试", notes = "用户测试notes")
@GetMapping("localDateTime")
public ResultMessage localDateTimeGet(@RequestParam(value = "localDateTime") Long localDateTime) {
    return ResultMessage.success(dateTime);
}


@ApiOperation(value = "用户测试", notes = "用户测试notes")
@GetMapping("{localDateTime}")
public ResultMessage localDateTimePath(@PathVariable("localDateTime") Long localDateTime) {
    return ResultMessage.success(dateTime);

Post方式请求
@Data
public class LocalDateTimeVO implements Serializable {
    private static final long serialVersionUID = 1L;
    private LocalDateTime localDateTime;
}

@ApiOperation(value = "用户测试", notes = "用户测试notes")
@PostMapping("localDateTime")
public ResultMessage localDateTimePost(@RequestBody LocalDateTimeVO localDateTimeVO) {
    log.info(localDateTimeVO.getLocalDateTime().toString());
    return ResultMessage.success(localDateTimeVO);
}

Jackson方式完结撒花

本文参考简书和耳朵实现方式:

https://juejin.cn/post/6854573211528249357 从LocalDateTime序列化探讨全局一致性序列化

实现二:FastJson

LocalDateTimeSerializer.java
public class LocalDateTimeSerializer implements ObjectSerializer {
    @Override
    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
        if 周记3——解决fixed属性在ios软键盘弹出后失效的bug

web移动端fixed布局和input等表单的爱恨情仇 - 终极BUG,完美解决

各浏览器 position: fixed 造成的bug 通用解决办法,Safari, iOS

[BUG]ios中input不回弹,导致fixed布局错位

fix bug:Redis序列化算法不一致导致乱码问题的原因及自定义序列化解决方案

软件测试JIRA中这几个缺陷的解决状态是啥意思?