Java 中时间对象的序列化
在 Java 应用程序中,时间对象是使用地比较频繁的对象,比如,记录某一条数据的修改时间,用户的登录时间等应用场景。在传统的 Java 编程中,大部分的程序员都会选择使用 java.uti.Date 这个类型的类来表示时间(这个类可不是什么善类)。
在现代化互联网的使用场景中,由于前后端分离的原因,在前后端之间进行数据的交互都会默认采用 JSON(JavaScript Object Notion 即 JavaScript 对象表示法)来完成前后端的数据交互。在对时间对象的 JSON 序列化处理的过程中,可能或多或少都会遇到一些坑,本文将结合笔者自身遇到的一些问题,提供我个人认为比较合理的解决方案。
Date 对象的序列化
如果你正在使用 java.util.Date 或者它的一些子类,那么请尽快放弃使用这一系列类,这个类可能是 Java 中为数不多令人觉得恶心的类。这个类存在以下几点显而易见的缺陷:
这个类是表示时间的,但是它表示的时间并没有时区的概念,只是单纯地存储了一个
long类型的时间戳来表示时间,而这个时间戳则是基于系统默认的时区尽管大部分的
getter和setter方法已经被弃用了,但是如果去翻看这个类的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 在没有配置相关的属性的情况下,会调用 Date 的 toString 方法来填充属性的 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 都是很清楚的、易懂的,和
Date的get系列方法形成鲜明对比和
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 中时间对象的序列化的更多相关文章
- 【译】Java中的对象序列化
前言 好久没翻译simple java了,睡前来一篇. 译文链接: http://www.programcreek.com/2014/01/java-serialization/ 什么是对象序列化 在 ...
- java中可定制的序列化过程 writeObject与readObject
来源于:[http://bluepopopo.iteye.com/blog/486548] 什么是writeObject 和readObject?可定制的序列化过程 这篇文章很直接,简单易懂.尝试着翻 ...
- JAVA中JavaBean对象之间属性拷贝的方法
JAVA中JavaBean对象之间的拷贝通常是用get/set方法,但如果你有两个属性相同的JavaBean或有大部分属性相同的JavaBean,对于这种情况,可以采用以下几个简便方法处理. 下面对这 ...
- Java——IO流 对象的序列化和反序列化流ObjectOutputStream和ObjectInputStream
对象的输入输出流 : 主要的作用是用于写入对象信息与读取对象信息. 对象信息一旦写到文件上那么对象的信息就可以做到持久化了 对象的输出流: ObjectOutputStream 对象的输入流: Ob ...
- Java中的对象池技术
java中的对象池技术,是为了方便快捷地创建某些对象而出现的,当需要一个对象时,就可以从池中取一个出来(如果池中没有则创建一个),则在需要重复重复创建相等变量时节省了很多时间.对象池其实也就是一个内存 ...
- 【学习笔记】Java中生成对象的5中方法
概述:本文介绍以下java五种创建对象的方式: 1.用new语句创建对象,这是最常用的创建对象的方式. 2.使用Class类的newInstance方法 3.运用反射手段,调用java.lang.re ...
- 浅谈Java中的对象和引用
浅谈Java中的对象和对象引用 在Java中,有一组名词经常一起出现,它们就是“对象和对象引用”,很多朋友在初学Java的时候可能经常会混淆这2个概念,觉得它们是一回事,事实上则不然.今天我们就来一起 ...
- java中直接打印对象
java中直接打印对象,会调用对象.toString()方法.如果没有重写toString()方法会输出"类名+@+hasCode"值,hasCode是一个十六进制数 //没有重写 ...
- java中时间的获取(二)
java中时间的获取2 /** * 获取数据库操作记录时间 */ public static String getOpreateDbTime() { Calendar c = Calendar.get ...
- 如何使用java中的对象
使用java中的对象,分2步: 1.创建一个对象: 2.使用对象的属性和方法. 见下面的示例代码: package com.imooc; //1.定义一个类 public class Telphone ...
随机推荐
- destoon上做纯js实现html指定页面导出word
因为最近做了范文网站需要,所以要下载为word文档,如果php进行处理,很吃后台服务器,所以想用前端进行实现.查询github发现,确实有这方面的插件. js导出word文档所需要的两个插件: 1 2 ...
- 物理standby database的日常维护【转】
1.停止Standby select process, status from v$managed_standby; --查看备库是否在应用日志进行恢复 alter database recover ...
- ABC319 A-E 题解
A 用 map <string, int> 将名字对应的值存下来即可. 赛时代码 B 按照题意暴力模拟,注意细节. 赛时代码 C 答辩题,卡了我半个小时. 枚举 \(1\sim 9\) 的 ...
- 16.2 ARP 主机探测技术
ARP (Address Resolution Protocol,地址解析协议),是一种用于将 IP 地址转换为物理地址(MAC地址)的协议.它在 TCP/IP 协议栈中处于链路层,为了在局域网中能够 ...
- 如何在 Vue.js 中引入原子设计?
本文为翻译文章,原文链接: https://medium.com/@9haroon_dev/introducing-atomic-design-in-vue-js-a9e873637a3e 前言 原子 ...
- keycloak~为keycloak-services项目添加第三方模块(首创)
我们在对keycloak框架中的核心项目keycloak-services进行二次开发过程中,发现了一个问题,当时有这种需求,在keycloak-services中需要使用infinispan缓存,我 ...
- 牛客多校第五场 K King of Range
题意: 给定一个\(n\)个数得序列\(a_i\),给定\(m\)个询问,每次给出一个\(k\),寻找有多少个区间\([l, r]\)中最大值与最小值之差严格大于\(k\). 思路: 可以发现,如果已 ...
- C/C++ 开发SCM服务管理组件
SCM(Service Control Manager)服务管理器是 Windows 操作系统中的一个关键组件,负责管理系统服务的启动.停止和配置.服务是一种在后台运行的应用程序,可以在系统启动时自动 ...
- GUI界面实现小学生口算题卡功能(一)| 简要了解GUI
上课没认真听,下课不好好写. 关于GUI,首先了解了一下什么是GUI: GUI(Graphical User Interface),图形用户界面.采用图形方式显示的计算机操作用户接口.与早期计算机使用 ...
- 【uniapp】【微信小程序】【外包杯】如何创建分包
意义:分包可以减少小程序数次启动时的加载时间 1.创建分包的根目录 2.在page.json中,和pages节点平级的位置声明节点,用来定义分包的相关结构 3.在subpkg目录上新建页面 4.完成了