前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >fix bug:解决在Spring项目实践中LocalDateTime无法序列化反序列化的问题

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

作者头像
关忆北.
发布2021-12-07 16:54:33
2.3K0
发布2021-12-07 16:54:33
举报
文章被收录于专栏:关忆北.关忆北.

概述-本文的意义

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

两种方式实现全局配置

两种配置方式
  • Jackson配置方式
  • FastJson配置方式

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

两种方式的共同原理

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

转换工具

LocalDateTimeGetConverter.java
代码语言:javascript
复制
@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
代码语言:javascript
复制
@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
代码语言:javascript
复制
@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
代码语言:javascript
复制
@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即可。

代码语言:javascript
复制
@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);
image-20210801111924684
image-20210801111924684
image-20210801111941035
image-20210801111941035
Post方式请求
代码语言:javascript
复制
@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);
}
image-20210801113324780
image-20210801113324780
Jackson方式完结撒花

本文参考简书和耳朵实现方式: https://juejin.cn/post/6854573211528249357 从LocalDateTime序列化探讨全局一致性序列化

实现二:FastJson

LocalDateTimeSerializer.java
代码语言:javascript
复制
public class LocalDateTimeSerializer implements ObjectSerializer {
    @Override
    public void write(JSONSerializer serializer, Object object, Object fieldName, Type fieldType, int features) throws IOException {
        if (object != null) {
            LocalDateTime localDateTime = (LocalDateTime) object;
            //将localDateTime转换为中国区(+8)时间戳。
            serializer.write(localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli());
        } else {
            serializer.write(null);
        }
    }
}
LocalDateTimeDeserializer.java
代码语言:javascript
复制
public class LocalDateTimeDeserializer implements ObjectDeserializer {
    @Override
    @SuppressWarnings("unchecked")
    public LocalDateTime deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        String timestampStr = parser.getLexer().numberString();

        if (timestampStr == null || "".equals(timestampStr)) {
            return null;
        }

        timestampStr = timestampStr.replaceAll("\"", "");

        long timestamp = Long.parseLong(timestampStr);
        if (timestamp == 0) {
            return null;
        }
        return Instant.ofEpochMilli(timestamp).atZone(ZoneOffset.ofHours(8)).toLocalDateTime();
    }

    @Override
    public int getFastMatchToken() {
        return 0;
    }
}

请求代码同Jackson,篇幅原因,不再重复。

Get方式请求
image-20210801121126108
image-20210801121126108
image-20210801121137797
image-20210801121137797
Post方式请求
image-20210801121230251
image-20210801121230251

本方案参考: []: https://blog.csdn.net/chimmhuang/article/details/104830430 “LocalDateTime在项目中的使用(LocalDateTime对接前端通过时间戳互转、LocalDateTime对接数据库)”

完结撒花

代码已开源至Gitee: 点我看源码

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-08-01 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概述-本文的意义
  • 两种方式实现全局配置
    • 两种配置方式
      • 两种方式的共同原理
      • 转换工具
      • 实现一:Jackson
      • 测试Jackson方式
      • 实现二:FastJson
      • 完结撒花
      相关产品与服务
      文件存储
      文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档