从LocalDateTime序列化来看全局一致性序列化

点击上方“Java基基”,选择“设为星标”

做积极的人,而不是积极废人!

每天 14:00 更新文章,每天掉亿点点头发...

源码精品专栏

 

来源:juejin.cn/post/

6854573211528249357

7371f61a1dde5246ff9f0cff069ceac8.jpeg


楔子

前两天,项目组中的一个老系统,被同事改动了代码。导致一些接口在返回LocalDateTime类型数据的时候,中间多了一个T时。我让他处理一下,他搞了好久,没搞定,也没整明白。

于是就有了今天要写的这篇:从LocalDateTime序列化来看全局一致性序列化体验

这个标题看起来蛮不像人话的,有种挺官方的感觉,我先给大家翻译翻译我们的主题是什么:通过讲解LocalDateTime的序列化从而引出整个项目中的所有序列化处理,并让他们保持一致。

在我们项目中一般存在着两种序列化,

一个呢是SpringMVC官方的序列化,也就是Spring帮你做的序列化,比如你在一个接口上面打了一个ResponseBody注解,SpringMVC中的消息转换器会帮你做序列化。

另一个就是我们项目内的序列化,自己定义的JsonUtil也好,还是你引入的第三方JSON处理工具(比如FastJson)也好,都可以说做是我们项目内部的序列化。

这两者如果不一样,有时候序列化出来的数据可能会出现结果不大一样的结果,为了防止这种情况,今天我们就来探讨一下项目中的序列化。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

💡举个例子

我们先来举个例子,来看看如果序列化不一致会出现啥样的效果。

@GetMapping("/api/anon")
public ApiResult test01() {
    return ApiResult.ok("匿名访问成功");
}

这是一段很普通的访问接口,返回的结果如下:

{
    "code": 200,
    "msg": "请求成功",
    "data": {
        "请求成功": "匿名访问成功"
    },
    "timestamp": "2020-07-19T23:07:07.738",
    "fail": false,
    "success": true
}

这里大家只需要注意一下「timestamp」 的序列化结果,「timestamp」 是一个LocalDateTime类型,在SpringMVC中的消息转换器对LocalDateTime做序列化的时候没有特殊处理,直接调用了LocalDateTimetoString() 方法,所以这个序列化结果中间有个T

但是如果这里的序列化用了其他方案,可能这个序列化结果会是不一样的体验,在我的项目中我也采用了Jackson来做序列化(Spring中也用的它),我们可以看看我们自己定义的一个JsonUtil对LocalDateTime做序列化会是什么结果。

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

我们序列化工具类长这样,和上面一样,我们序列化一个ApiResult看看会是什么结果:

{
    "code": 400,
    "msg": "请求失败",
    "timestamp": {
        "month": "JULY",
        "year": 2020,
        "dayOfMonth": 19,
        "hour": 23,
        "minute": 25,
        "monthValue": 7,
        "nano": 596000000,
        "second": 2,
        "dayOfYear": 201,
        "dayOfWeek": "SUNDAY",
        "chronology": {
            "id": "ISO",
            "calendarType": "iso8601"
        }
    },
    "fail": true,
    "success": false
}

Jackson默认的ObjectMapper下序列化出来的结果就是这个鬼样子,因为是序列化最后倒是转化成字符串了,那这样的数据前端如果拿到了肯定是不能正常转成时间类型的,

LocalDateTime只是一个缩影,哪怕对于字符串,不同的序列化配置也是有着不同的影响,字符串里面可能会有转义字符,有引号,不同的方案出来的结果可能是不一样的,

在实际项目中对第三方接口进行HTTP对接一般来说都是需要的,其中传输过去的数据一般会经过我们项目中JSON工具类的序列化为字符串之后再传输过去,如果序列化方案不同可能会在序列化过程中传过去的数据不是我们想要的。

还有些接口是我们直接往HttpServeletResponse里面写数据,这种时候一般也是写JSON数据,比如:

public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
    response.setHeader("Cache-Control", "no-cache");
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json");
    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
    response.getWriter().println(JacksonUtil.toJsonString(ApiResult.fail(authException.getMessage())));
    response.getWriter().flush();
}

这里我用工具类直接去序列化这个ApiResult,传给前台的数据就会也出现上面例子中的情况,LocalDateTime序列化结果不是我们想要的。

所以在项目中的序列化和Spring中的序列化保持一致还是很有必要的。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

📃实操方案

上面说过了项目中保持序列化的一致性的必要性(我认为是必要的哈哈)。

那我们下面就可以说说如果去做这个一致性。

我们知道,如果你想要在Spring的序列化中将你返回的那个对象某个LocalDateTime类型变量进行序列化的话,很简单,可以这样:

public class ApiResult implements Serializable {

    private static final Map<String, String> map = new HashMap<>(1);
    private int code;
    private String msg;
    private Object data;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime timestamp;

就很简单的在这个变量上面加一个JsonFormat注解就ok了,但这样不是全局的,哪个变量加哪个变量就生效。

想做到全局生效,我们需要在Spring的配置去修改Spring中使用的ObjectMapper,了解Jackson的小伙伴应该都知道,序列化的各种配置都在配置在这个ObjectMapper中的,不知道也没关系,你现在知道了。

那么我们可以通过去配置Spring中的ObjectMapper做到全局生效:

@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(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
            javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
            javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
            javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss")));

            builder.modules(javaTimeModule);
        };
    }
}

通过在Jackson2ObjectMapperBuilderCustomizer之中加入一些序列化方案就可以达到这个效果,上文的代码就是做了这些操作,这样之后我们再次访问最开始那个接口,就会出现如下效果:

{
    "code": 200,
    "msg": "请求成功",
    "data": {
        "请求成功": "匿名访问成功"
    },
    "timestamp": "2020-07-20 00:06:12",
    "fail": false,
    "success": true
}

timestamp中间那个T不存在了,因为我们已经加入了LocalDateTime的序列化方案了。

但是仅仅如此还不行,这只是做了LocalDateTime的全局序列化,我们还需要让自己的工具类也和Spring的保持一致:

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

    /**
     *  将Long,BigInteger序列化的时候,转化为String
     */
//  SimpleModule simpleModule = new SimpleModule();
//
//  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;
}

这段代码是紧跟上一步,对Jackson2ObjectMapperBuilderbuilder出来的ObjectMapper做一些操作,设置一系列自己想要的属性。

代码中注释那一块也是做一个序列化转换,如果你的项目中用到了比较长的LONG类型数字,可能会导致JS拿不到完全的数字,因为java中的long类型要比JS的number类型长一点,这个时候你必须要转换成String给前台,它才能拿到正确的数字,如果你有需要可以打开这一段。

最后一句就是我们比较关键的了,把builder出来的ObjectMapper赋值给我们工具类中的ObjectMapper,这样的话它俩其实指向一个地址,也就是使用同一个对象进行序列化,所得出的结果当然就是相同的了。

后记

今天的从LocalDateTime序列化探讨全局一致性序列化就到这里了,希望对大家有所帮助。

你们的每个点赞收藏与评论都是对我知识输出的莫大肯定,如果有文中有什么错误或者疑点或者对我的指教都可以在评论区下方留言,一起讨论。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢

62b73eb1f730285e729a8e9956dbce6b.png

已在知识星球更新源码解析如下:

1bbe67e8c670abb6c4d258326040ae95.jpeg

54e972a2c5d18ce7591d16e98a103985.jpeg

7264d6a1ff4af74a380d105dca3ad0ad.jpeg

74937e763d1e4f32d2bbc4d2df3668e4.jpeg

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 101 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 6W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值