在 Java 应用程序中,时间对象是使用地比较频繁的对象,比如,记录某一条数据的修改时间,用户的登录时间等应用场景。在传统的 Java 编程中,大部分的程序员都会选择使用 java.uti.Date 这个类型的类来表示时间(这个类可不是什么善类)。

在现代化互联网的使用场景中,由于前后端分离的原因,在前后端之间进行数据的交互都会默认采用 JSON(JavaScript Object Notion 即 JavaScript 对象表示法)来完成前后端的数据交互。在对时间对象的 JSON 序列化处理的过程中,可能或多或少都会遇到一些坑,本文将结合笔者自身遇到的一些问题,提供我个人认为比较合理的解决方案。

Date 对象的序列化

如果你正在使用 java.util.Date 或者它的一些子类,那么请尽快放弃使用这一系列类,这个类可能是 Java 中为数不多令人觉得恶心的类。这个类存在以下几点显而易见的缺陷:

  • 这个类是表示时间的,但是它表示的时间并没有时区的概念,只是单纯地存储了一个 long 类型的时间戳来表示时间,而这个时间戳则是基于系统默认的时区

  • 尽管大部分的 gettersetter 方法已经被弃用了,但是如果去翻看这个类的 getYear() 等方法绝对会让你大吃一惊。它的 year 是基于 1900 年为起始年,month 则是以 \(0\) 为开始月份

  • 这个类是一个可变类,这意味着在记录了一个时间之后,依旧可以修改这个时间对象,这从设计上来讲是不合理的

尽管自 JDK 1.1 开始着手设计了 java.util.Calendar 准备修复这个类存在的一些问题,但是结果不是很明显,java.util.Calendar 依旧是可变的

出于以上的一些原因,建议不要使用 java.util 包下的时间类,如果必要,可以考虑使用 java.time.Instant 来替换 java.util.Date

但是总会有意外,如果现有的系统中存在大量的使用 java.util.Date 的场景,那么也只能试着和这个类友好的相处。对于没有配置任何序列化规则的 JSON 序列化工具类,会默认将类中的所有实例属性递归地进行处理方法来转换成对应的 JSON 内容。对于下面定义的类:

import java.util.Date;

public class Person {
private String name;
private Date createdTime; // 省略部分 Getter 和 Setter 方法
}

使用下面的方法来设置相关的属性:

Person person = new Person();
person.setName("xhliu");
person.setCreatedTime(new Date());

Jackson 的序列化

当使用 Jackson 将这个对象进行序列化时(此时没有设置 Jackson ),会得到类似下面的输出结果:

{"name":"xhliu","createdTime":1655642583437}

由于 Date 默认情况下只有一个存储时间戳的非空属性,因此会将其进行序列化。显然,实际使用时肯定不希望是这样的格式。如果希望 Jackson 能够序列化成指定的格式,可以在这个 Date 类型的属性上加上 @JsonFormat 注解使得 Jackson 序列化成对应的格式,一般都会采用如下的格式:

import com.fasterxml.jackson.annotation.JsonFormat;
class Person {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createdTime;
}

Jackson 中 @JsonFormat 注解的目的是格式化属性的序列化形式,在这里格式化 Date 的输出格式,具体的 Date 的格式以及各个字段的含义可以参考:ISO 8601

此时,再使用 Jackson 进行序列化可以看到类似下面的效果:

{"name":"xhliu","createdTime":"2022-06-19 13:08:51"}

除了在预先的字段上加上 @JsonFormt 的注解来显式地格式化时间,也可以通过配置 Jackson 的全局日期格式来配置日期的输出格式,如下所示:

ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

需要注意的是,@JsonFormat 的优先级会高于全局的配置,因此,如果遇到某些需要进行特定格式化的场景,使用 @JsonFormat 是一个比较好的选择

Jackson 的反序列化

由于已经使用了相关的序列化格式,因此在进行反序列化时也需要按照相同的格式才能完成JSON 的反解析。大部分的 REST 请求在接受参数时,对于 Date 的解析出现异常都是由于格式不匹配导致的,为了解决这个问题,可以使用 @JsonFormat 的注解来规定时间的格式,使得它能够正常解析对应的时间属性`

Gson 的序列化

和 Jackson 的序列化不同,Gson 在没有配置相关的属性的情况下,会调用 DatetoString 方法来填充属性的 JSON 值,对于上面的例子,如果使用没有进行任何配置的 Gson 来进行 JSON 的序列化,输出的结果可能如下所示:

{"name":"xhliu","createdTime":"Jun 19, 2022, 9:20:41 PM"}

和默认的 Jackson 的输出相比,只能说是一个五八,一个四十了

如果想要格式化 Date,需要为 Gson 注册一个序列化适配器,注册到 Gson 中来实现 Date 的序列化。可以手动实现序列化适配器,只需要定义一个类实现相关的序列化和反序列化操作即可,类似的适配器如下所示:

import com.google.gson.*;
import lombok.SneakyThrows; import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date; public class NormalDateSerializerAdapter
implements JsonSerializer<Date>, JsonDeserializer<Date> {
@Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
/*
* 尽管 SimpleDateFormat 是线程不安全的(网上铺天盖地都有的八股文),但是在使用的过程中
* 中是一个 ”栈封闭“ 的状态,明显这项操作是线程安全的
*
* Tips: SimpleDateFormat 不是是线程安全的类,因为它在格式化日期的过程中修改了私有属性状态,
* 并且没有使用任何同步手段来保证操作的有序性
*/
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return new JsonPrimitive(format.format(src));
}     @SneakyThrows
@Override
public Date deserialize(
JsonElement json, Type typeOfT,
JsonDeserializationContext context
) throws JsonParseException {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return format.parse(json.getAsString());
}
}

在 Gson 的构造过程中注入这个类型适配器:

Gson gson = new GsonBuilder()
.registerTypeAdapter(Date.class, new NormalDateSerializerAdapter())
.create();

此时再进行序列化的操作,可以看到 Date 已经符合一般的表现形式了:

{"name":"xhliu","createdTime":"2022-06-20 09:31:43"}

有时由于恶性的需求的原因,这种全局的时间格式可能并不能满足要求。有些需求并不需要时间精确到时分秒。针对这种情况,在 Gson 中注册类型适配器无法满足要求。和 Jackson 类似同,Gson 也支持通过注解的方式来设置字段的序列化格式,这样就能够使得序列化的领域精确到某一个字段属性,Gson 通过 @JsonAdapter 的注解来定义属性的对象序列化格式,如下所示:

import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import lombok.SneakyThrows; import java.lang.reflect.Type;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date; public class Person {
private String name; @JsonAdapter(value = YearDateSerializerAdapter.class)
private Date createdTime; // 自定义的日期序列化格式
public static class YearDateSerializerAdapter
implements JsonDeserializer<Date>, JsonSerializer<Date> { @Override
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return new JsonPrimitive(format.format(src));
} @SneakyThrows
@Override
public Date deserialize(
JsonElement json, Type typeOfT,
JsonDeserializationContext context
) throws JsonParseException {
DateFormat format = new SimpleDateFormat("yyyy-MM-dd");
return format.parse(json.getAsString());
}
}
}

这样,在序列化这个对象时将会按照 @JsonAdapter 注解中定义的类型适配器进行序列化和反序列化

Gson 的反序列化

由于已经配置了相关的类型适配器,因此反序列化时需要符合预期能够解析的格式。对于上面的适配器来讲,只要是符合 "yyy-MM-dd HH:mm:ss" 的格式便能够进行解析。具体处理时需要注意和自己的反序列化格式相匹配

java.time 包下时间对象的序列化

java.time 包下的的时间类自 JDK 1.8 引入,它解决了原有 java.util 包下有关时间类中存在的问题。这些类相比较于旧有的时间类,存在以下的优势:

  • API 都是很清楚的、易懂的,和 Dateget 系列方法形成鲜明对比

  • Date 直接保存时间戳不同,java.time 包下表示时间的类是可伸缩的,比如:Instant 就表示时间戳、LocalDate 表示日期(年、月、日)、ZonedDateTime 表示带有时区的时间

  • java.time 包下的所有类都是不可变类,这也就意味着它们一定是线程安全的

  • 这个包下的类提供了链式的 API 调用,使得代码意图更加清晰

Jackson 的序列化

对于 java.time 包下的时间类,Jackson 已经提供了相应的时间模块来处理这些类的序列化和反序列化操作。对于一般的 Maven 项目,需要加入相关的依赖项目:

<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson-version}</version>
</dependency>

当使用时,注册对应的时间模块即可:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());

现在,对相关的时间属性加上 @JsonFormat 注解格式化时间即可:

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDate;
import java.time.LocalDateTime; public class Order {
private int id;
private String orderName;
private String orderDesc; @JsonFormat(pattern = "yyyy-MM-dd")
private LocalDate orderCreatedDate; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime orderCreatedDateTime;
}

相关的序列化结果如下:

{"id":1,"orderName":null,"orderDesc":null,"orderCreatedDate":"2022-06-24","orderCreatedDateTime":"2022-06-24 21:51:58"}

Gson 的序列化

Gson 并没有提供相关的时间模块组件,因此需要自定义相关的序列化和反序列化实现,以 LocalDateTime 的序列化和反序列化为例,可以定义相关的序列化和反序列化实现:

private static class LocalDateTimeAdapter
implements JsonSerializer<LocalDateTime>, JsonDeserializer<LocalDateTime> {
// 自定义的时间格式
private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Override
public LocalDateTime deserialize(
JsonElement json, Type typeOfT,
JsonDeserializationContext context
) throws JsonParseException {
return LocalDateTime.parse(
json.getAsString(),
dateTimeFormatter.withZone(ZoneId.systemDefault())
);
} @Override
public JsonElement serialize(
LocalDateTime src, Type typeOfSrc,
JsonSerializationContext context
) {
return new JsonPrimitive(dateTimeFormatter.format(src));
}
}

然后,将这个类型适配器注册到 Gson 中,使得其能够处理时间的序列化:

Gson gson = new GsonBuilder()
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
.create();

值得注意的一点是,Gson 是通过反射的方式来访问相关的属性的,而这一方式在 JDK 9 开始就已经被禁用了,因此在序列化时可能会看到类似下面的异常:

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final int java.time.LocalDate.year accessible: module java.base does not "opens java.time" to unnamed module @2d9d4f9d
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)
at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)
at com.google.gson.internal.reflect.UnsafeReflectionAccessor.makeAccessible(UnsafeReflectionAccessor.java:44)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:159)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:102)
at com.google.gson.Gson.getAdapter(Gson.java:489)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.createBoundField(ReflectiveTypeAdapterFactory.java:117)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:166)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:102)
at com.google.gson.Gson.getAdapter(Gson.java:489)
at com.google.gson.Gson.toJson(Gson.java:727)
at com.google.gson.Gson.toJson(Gson.java:714)
at com.google.gson.Gson.toJson(Gson.java:669)
at com.google.gson.Gson.toJson(Gson.java:649)
at com.example.demo.config.GsonConfig.main(GsonConfig.java:85)

为了解决这个问题,可以在运行时添加 --add-opens java.base/java.time=ALL-UNNAMED 虚拟机选项(VM Options)来使得反射功能能够正常使用

java -cp xxx --add-opens java.base/java.time=ALL-UNNAMED

如果是 IDEA 的话,可以在 Edit Configuration ——> Modify Options 中找到 VM Options

序列化的结果类似下面所示:

{"id":1,"orderCreatedDate":{"year":2022,"month":6,"day":24},"orderCreatedDateTime":"2022-06-24 22:20:26"}

对于其它 java.time 包下的时间类,也可以使用类似的方式来定义相关的序列化行为

参考:

[1] https://iogogogo.github.io/2020/06/23/gson-java8-datetime/

Java 中时间对象的序列化的更多相关文章

  1. 【译】Java中的对象序列化

    前言 好久没翻译simple java了,睡前来一篇. 译文链接: http://www.programcreek.com/2014/01/java-serialization/ 什么是对象序列化 在 ...

  2. java中可定制的序列化过程 writeObject与readObject

    来源于:[http://bluepopopo.iteye.com/blog/486548] 什么是writeObject 和readObject?可定制的序列化过程 这篇文章很直接,简单易懂.尝试着翻 ...

  3. JAVA中JavaBean对象之间属性拷贝的方法

    JAVA中JavaBean对象之间的拷贝通常是用get/set方法,但如果你有两个属性相同的JavaBean或有大部分属性相同的JavaBean,对于这种情况,可以采用以下几个简便方法处理. 下面对这 ...

  4. Java——IO流 对象的序列化和反序列化流ObjectOutputStream和ObjectInputStream

    对象的输入输出流 : 主要的作用是用于写入对象信息与读取对象信息. 对象信息一旦写到文件上那么对象的信息就可以做到持久化了 对象的输出流: ObjectOutputStream 对象的输入流:  Ob ...

  5. Java中的对象池技术

    java中的对象池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复重复创建相等变量时节省了很多时间.对象池其实也就是一个内存 ...

  6. 【学习笔记】Java中生成对象的5中方法

    概述:本文介绍以下java五种创建对象的方式: 1.用new语句创建对象,这是最常用的创建对象的方式. 2.使用Class类的newInstance方法 3.运用反射手段,调用java.lang.re ...

  7. 浅谈Java中的对象和引用

    浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...

  8. java中直接打印对象

    java中直接打印对象,会调用对象.toString()方法.如果没有重写toString()方法会输出"类名+@+hasCode"值,hasCode是一个十六进制数 //没有重写 ...

  9. java中时间的获取(二)

    java中时间的获取2 /** * 获取数据库操作记录时间 */ public static String getOpreateDbTime() { Calendar c = Calendar.get ...

  10. 如何使用java中的对象

    使用java中的对象,分2步: 1.创建一个对象: 2.使用对象的属性和方法. 见下面的示例代码: package com.imooc; //1.定义一个类 public class Telphone ...

随机推荐

  1. destoon上做纯js实现html指定页面导出word

    因为最近做了范文网站需要,所以要下载为word文档,如果php进行处理,很吃后台服务器,所以想用前端进行实现.查询github发现,确实有这方面的插件. js导出word文档所需要的两个插件: 1 2 ...

  2. 物理standby database的日常维护【转】

    1.停止Standby select process, status from v$managed_standby; --查看备库是否在应用日志进行恢复 alter database recover ...

  3. ABC319 A-E 题解

    A 用 map <string, int> 将名字对应的值存下来即可. 赛时代码 B 按照题意暴力模拟,注意细节. 赛时代码 C 答辩题,卡了我半个小时. 枚举 \(1\sim 9\) 的 ...

  4. 16.2 ARP 主机探测技术

    ARP (Address Resolution Protocol,地址解析协议),是一种用于将 IP 地址转换为物理地址(MAC地址)的协议.它在 TCP/IP 协议栈中处于链路层,为了在局域网中能够 ...

  5. 如何在 Vue.js 中引入原子设计?

    本文为翻译文章,原文链接: https://medium.com/@9haroon_dev/introducing-atomic-design-in-vue-js-a9e873637a3e 前言 原子 ...

  6. keycloak~为keycloak-services项目添加第三方模块(首创)

    我们在对keycloak框架中的核心项目keycloak-services进行二次开发过程中,发现了一个问题,当时有这种需求,在keycloak-services中需要使用infinispan缓存,我 ...

  7. 牛客多校第五场 K King of Range

    题意: 给定一个\(n\)个数得序列\(a_i\),给定\(m\)个询问,每次给出一个\(k\),寻找有多少个区间\([l, r]\)中最大值与最小值之差严格大于\(k\). 思路: 可以发现,如果已 ...

  8. C/C++ 开发SCM服务管理组件

    SCM(Service Control Manager)服务管理器是 Windows 操作系统中的一个关键组件,负责管理系统服务的启动.停止和配置.服务是一种在后台运行的应用程序,可以在系统启动时自动 ...

  9. GUI界面实现小学生口算题卡功能(一)| 简要了解GUI

    上课没认真听,下课不好好写. 关于GUI,首先了解了一下什么是GUI: GUI(Graphical User Interface),图形用户界面.采用图形方式显示的计算机操作用户接口.与早期计算机使用 ...

  10. 【uniapp】【微信小程序】【外包杯】如何创建分包

    意义:分包可以减少小程序数次启动时的加载时间 1.创建分包的根目录 2.在page.json中,和pages节点平级的位置声明节点,用来定义分包的相关结构 3.在subpkg目录上新建页面 4.完成了