在 SpringBoot 项目中, 如何统一 JSON 格式化中的日期格式

问题

现在的关系型数据库例如PostgreSQL/MySQL, 都已经对 JSON 类型提供相当丰富的功能, 项目中对于不需要检索但是又需要结构化的存储, 会在数据库中产生很多 JSON 类型的字段, 与 Jackson 做对象的序列化和反序列化配合非常方便.

如果 JSON 都是类定义的, 这个序列化和反序列化就非常透明 -- 不需要任何干预, 写进去是什么, 读出来就是什么. 但是如果 JSON 在 Java 代码中是定义为一个 Map, 例如 Map<String, Object> 那么就有问题了, 对于 Date 类型的数据, 在存入之前是 Date, 取出来之后就变成 Long 了.

SomePO po = new SomePO();
//...
Map<String, Object> map = new HashMap<>();
map.put("k1", new Date());
po.setProperties(map);
//...
mapper.insert(po);
//...
SomePO dummy = mapper.select(po.id);
// 这里的k1已经变成了 Long 类型
Object k1 = dummy.getProperties().get("k1");

原因

不管是使用原生的 MyBatis 还是包装后的 MyBatis Plus, 在对 JSON 类型字段进行序列化和反序列化时, 都需要借助类型判断, 调用对应的处理逻辑, 大部分情况, 使用的是默认的 Jackson 的 ObjectMapper, 而 ObjectMapper 对 Date 类型默认的序列化方式就是取时间戳, 对于早于1970年之前的日期, 生成的是一个负的长整数, 对于1970年之后的日期, 生成的是一个正的长整数.

查看 ObjectMapper 的源码, 可以看到其对Date格式的序列化和反序列化方式设置于_serializationConfig 和 _deserializationConfig 这两个成员变量中, 可以通过 setDateFormat() 进行修改

public class ObjectMapper extends ObjectCodec implements Versioned, Serializable {
//...
protected SerializationConfig _serializationConfig;
protected DeserializationConfig _deserializationConfig;
//... public ObjectMapper setDateFormat(DateFormat dateFormat) {
this._deserializationConfig = (DeserializationConfig)this._deserializationConfig.with(dateFormat);
this._serializationConfig = this._serializationConfig.with(dateFormat);
return this;
} public DateFormat getDateFormat() {
return this._serializationConfig.getDateFormat();
}
}

默认的序列化反序列化选项, 使用了一个常量 WRITE_DATES_AS_TIMESTAMPS, 在类 SerializationConfig 中进行判断, 未指定时使用的是时间戳

public SerializationConfig with(DateFormat df) {
SerializationConfig cfg = (SerializationConfig)super.with(df);
return df == null ? cfg.with(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) : cfg.without(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}

实际的转换工作在 SerializerProvider 类中, 转换方法为

public final void defaultSerializeDateValue(long timestamp, JsonGenerator gen) throws IOException {
if (this.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) {
gen.writeNumber(timestamp);
} else {
gen.writeString(this._dateFormat().format(new Date(timestamp)));
}
} public final void defaultSerializeDateValue(Date date, JsonGenerator gen) throws IOException {
if (this.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)) {
gen.writeNumber(date.getTime());
} else {
gen.writeString(this._dateFormat().format(date));
}
}

解决

局部方案

1. 字段注解

这种方式可以用在固定的类成员变量上, 不改变整体行为

public class Event {
public String name; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
public Date eventDate;
}

另外还可以自定义序列化反序列化方法, 实现 StdSerializer

public class CustomDateSerializer extends StdSerializer<Date> {
//...
}

就可以在 @JsonSerialize 注解中使用

public class Event {
public String name; @JsonSerialize(using = CustomDateSerializer.class)
public Date eventDate;
}

2. 修改 ObjectMapper

通过 ObjectMapper.setDateFormat() 设置日期格式, 改变默认的日期序列化反序列化行为. 这种方式只对调用此ObjectMapper的场景有效

private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
objectMapper.setDateFormat(df);
return objectMapper;
}

因为 ObjectMapper 一般是当作线程安全使用的, 而 SimpleDateFormat 并非线程安全, 在这里使用是否会有问题? 关于这个疑虑, 可以查看 这个链接

@StaxMan: I am a bit concerned if ObjectMapper is still thread-safe after ObjectMapper#setDateFormat() is called. It is known that SimpleDateFormat is not thread safe, thus ObjectMapper won't be unless it clones e.g. SerializationConfig before each writeValue() (I doubt). Could you debunk my fear? – dma_k Aug 2, 2013 at 12:09

DateFormat is indeed cloned under the hood. Good suspicion there, but you are covered. – StaxMan Aug 2, 2013 at 19:43

3. 修改 SpringBoot 配置

增加配置 spring.jackson.date-format=yyyy-MM-dd HH:mm:ss, 这种配置, 只对 Spring BeanFactory 中创建的 Jackson ObjectMapper有效, 例如 HTTP 请求和响应中对 Date 类型的转换

spring:
...
jackson:
date-format: yyyy-MM-dd HH:mm:ss

整体方案

国内项目, 几乎都会希望落库时日期就是日期的样子(方便看数据库表), 所谓日期的样子就是yyyy-MM-dd HH:mm:ss格式的字符串. 如果怕麻烦, 就通通都用这个格式了.

这样统一存在的隐患是丢失毫秒部分. 这个问题业务人员基本上是不会关心的. 如果需要, 就在格式中加上.

第一是 Spring 配置, 这样所有的请求响应都统一了

spring:
...
jackson:
date-format: yyyy-MM-dd HH:mm:ss

第二是定义一个工具类, 把 ObjectMapper 自定义一下, 这样所有手工转换的地方也统一了, 注意留一个getObjectMapper()方法

public class JacksonUtil {
private static final Logger log = LoggerFactory.getLogger(JacksonUtil.class);
private static final ObjectMapper MAPPER = createObjectMapper();
private JacksonUtil() {} private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
objectMapper.setDateFormat(df);
return objectMapper;
} public static ObjectMapper getObjectMapper() {
return MAPPER;
}
}

第三是启动后修改 MyBatisPlus 的设置, 即下面的 changeObjectMapper() 这个方法, 直接换成了上面工具类中定义的 ObjectMapper, 这样在 MyBatis 读写数据库时的格式也统一了.

@Configuration
@MapperScan(basePackages = {"com.somewhere.commons.impl.mapper"})
public class MybatisPlusConfig { @Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL));
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
} @PostConstruct
public void changeObjectMapper() {
// This will unify the date format with util methods
JacksonTypeHandler.setObjectMapper(JacksonUtil.getObjectMapper());
}
}

参考

SpringBoot+MyBatis Plus对Map中Date格式转换的处理的更多相关文章

  1. Struts2中Date日期转换的问题

      今天跑程序的时候莫名其妙的出现了下面的一个异常: java.lang.NoSuchMethodException:com.ca.agent.model.mybatis.ApprovalInforC ...

  2. JAVA中时间格式转换

    1.将任意日期格式的字符串转换为指定格式的字符串 //默认格式 String s1 = "20190110133236"; //给定格式 String s2 = "201 ...

  3. layui的table中日期格式转换

    我使用的layui的2.4.5版本,在util中已经带了格式转换的函数.这里主要记录如何使用. 未使用前在table中显示如下: 前端代码如下: {field:'createDate', title: ...

  4. Sql与C#中日期格式转换总结

    SQL中的转换方法: 一.将string转换为datetime,主要是使用Convert方法, 方法,Convert(datetime [ ( length ) ] , expression, [st ...

  5. js读取excel中日期格式转换问题

    在使用js-xlsx插件来读取excel时,会将2018/10/16这种数据自动装换成48264.12584511. 所以需要自己手动再转换回来 // excel读取2018/01/01这种时间格式是 ...

  6. Mac中的格式转换如何用读写工具Tuxera NTFS完成

    Tuxera NTFS for Mac是一款专门为Mac用户提供的NTFS驱动软件,它不仅可以进行磁盘文件的访问.编辑.传输和存储,还可以对硬盘进行维修检查以及修复. 今天小编就给大家简单介绍一下Tu ...

  7. SAP BDC 调用中 金额格式转换

    在BDC调用中,由于用户设置不同,导致金额.日期等字段的输入格式不正确.此处给出 自创 金额转换FM 并配有 调用方式. function zgm_conver_cuur. *"------ ...

  8. Sql Server中Float格式转换字符串varchar方法(转)

    1.[Sql Server](70)  SELECT CONVERT(varchar(100), CAST(@testFloat AS decimal(38,2)))SELECT STR(@testF ...

  9. linq中日期格式转换或者比较,程序报错说不支持方法的解决办法

    public void TestMethod1(){using (var _context = new hotelEntities()){var rq = DateTime.Now.Date;var ...

随机推荐

  1. static关键字和代码块

    static关键字 static修饰的变量称为静态变量/共享变量/类变量 用于修饰类的成员,如成员变量.成员方法以及代码块等,内static修饰的成员具备一些特殊性 1.静态变量 在java类中使用s ...

  2. 调和级数为什么是 O(logn) 的

    目录 调和级数 正片 调和级数 调和级数(Harmonic series)定义为 \[H(n)=\sum_{i=1}^n\dfrac 1i \] \(H\) 发散,证明看百度 . 正片 首先我们把 \ ...

  3. MYSQL常见可优化场景

    1.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:select id from t where num is null 可以在num上设置 ...

  4. Python 阿里云盾滑块验证

    本文仅供学习交流使用,如侵立删! 记一次阿里云盾滑块验证分析并通过 操作环境 win10 . mac Python3.9 selenium.pyautogui 分析 最近在做中国庭审公开网数据分析的时 ...

  5. Computational Protein Design with Deep Learning Neural Networks

    本文使用深度神经网络完成计算蛋白质设计去预测20种氨基酸概率. Introduction 针对特定结构和功能的蛋白质进行工程和设计,不仅加深了对蛋白质序列结构关系的理解,而且在化学.生物学和医学等领域 ...

  6. MySQL主从复制之并行复制说明

    传统单线程复制说明 众所周知,MySQL在5.6版本之前,主从复制的从节点上有两个线程,分别是I/O线程和SQL线程. I/O线程负责接收二进制日志的Event写入Relay Log. SQL线程读取 ...

  7. 官宣!DolphinScheduler 毕业成为 Apache 软件基金会顶级项目

    全球最大的开源软件基金会 Apache 软件基金会(以下简称 Apache)于北京时间 2021年4月9日在官方渠道宣布Apache DolphinScheduler 毕业成为Apache顶级项目.这 ...

  8. django中的forms组件

    form介绍 用户需要向后端提交一些数据时,我们常常把这些数据放在一个form表单里,采用form标签,里面包含一些input等标签把用户的数据提交给后端. 在给后端提交数据的时候,我们常常也需要对于 ...

  9. CPU性能优化干货总结

    一.背景 部门成立专项组,对数智平台和站务系统做性能优化,其中目标之一为降低服务端硬件成本,即在32G内存.CPU银牌的配置下,能支撑1万+发客量.要达到此目标,需通过压力测试并配合监控系统,以QPS ...

  10. 【Java】学习路径46-两种创建多线程的方法、以及在匿名内部类创建线程

    两种方法: 1.创建一个继承自Thread的线程类,然后再main(不限)中构造这个线程类对象.方法在之前讲过. 2.创建一个使用Runnable接口的线程类,然后在main(不限)中构造这个Runn ...