在 Spring Boot 开发中,处理日期格式是个绕不开的日常任务。前端传过来的日期字符串解析失败、后端返回的日期要么是时间戳要么是乱码的格式、跨时区返回的日期差8小时……这些问题,恐怕每位后端开发者都或多或少踩过坑。
为了解决这类问题,很多人会习惯性地把 @DateTimeFormat 和 @JsonFormat 注解堆在字段上,甚至混合使用,以为这样就能一劳永逸。结果上线后,要么入参解析报错,要么返回格式不对,排查许久才发现:这两个注解的核心作用截然不同,用错了等于白加。
本文旨在彻底讲透这两个注解的区别、正确用法以及常见的“避坑”要点。如果你想在 云栈社区 与更多开发者交流此类实战经验,这里有丰富的技术讨论氛围。
一句话分清两个注解的核心区别
很多人容易混淆这两个注解,本质上是没搞清它们的“作用阶段”——一个管“进”,一个管“出”,分工明确:
| 注解 |
核心作用 |
生效阶段 |
依赖框架 |
核心场景 |
@DateTimeFormat |
格式化入参 |
前端 → 后端(接收参数) |
Spring MVC |
接收前端传的日期字符串(如 2024-05-20),转成 Date / LocalDateTime |
@JsonFormat |
格式化出参 |
后端 → 前端(返回数据) |
Jackson |
把后端的 Date / LocalDateTime 转成指定格式的字符串返回给前端 |
简单来说:
- 想让前端传的日期字符串能被后端正确接收,用
@DateTimeFormat;
- 想让后端返回的日期不是时间戳或乱码,而是指定格式(如
yyyy-MM-dd HH:mm:ss),用 @JsonFormat;
- 既想收参正常,又想返参格式正确,两个注解可以一起用(但需注意细节配置)。
最容易踩的4个坑
这是最高频的错误!很多人看到日期解析报错,就给入参字段加上 @JsonFormat,结果无论怎么配置,前端传的 “2024-05-20” 还是无法解析成 Date。
例如下面这段错误代码,添加 @JsonFormat 后入参依然报错:
// 错误写法:想用@JsonFormat接收前端入参
@PostMapping("/save")
public Result save(@RequestBody UserForm form) {
// 前端传{"createTime":"2024-05-20 12:00:00"},解析失败
return Result.success();
}
@Data
class UserForm {
// @JsonFormat管出参,对入参无效!
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
原因很简单:@JsonFormat 是 Jackson 库的注解,它只负责将后端对象序列化为 JSON 返回,根本管不了前端传参的解析。想解决入参问题,必须使用 @DateTimeFormat。
与坑1相反,有人想让返回的日期显示成 “2024-05-20”,于是给字段加了 @DateTimeFormat,结果返回的仍然是时间戳或默认格式:
// 错误写法:想用@DateTimeFormat控制返回格式
@GetMapping("/get")
public Result<User> getUser() {
User user = new User();
user.setCreateTime(new Date());
// 返回的createTime还是时间戳,@DateTimeFormat完全没作用
return Result.success(user);
}
@Data
class User {
// @DateTimeFormat管入参,对出参无效!
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@DateTimeFormat 是 Spring MVC 的注解,它只处理前端传过来的参数解析,并不控制返回格式。想控制出参,必须使用 @JsonFormat。
坑3:时区没配,返回的日期差8小时
这是第二高频的坑!使用 @JsonFormat 时只配置了 pattern 格式,没有配置时区,结果返回的日期比实际时间少8小时:
// 错误写法:缺省时区,返回日期差8小时
@Data
class User {
// 前端收到的时间会是 GMT 时间,比北京时间少8小时
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
原因是 Jackson 默认使用 GMT 时区,而我们通常使用的是东八区(GMT+8),因此必须手动指定时区,否则必然出现问题。
坑4:LocalDateTime / Date 混用注解,解析失败
Java 8 的 LocalDateTime 和传统的 Date 在使用注解配置上有所不同,混着配置会导致解析直接失败:
Date 类型:可以直接使用 @DateTimeFormat / @JsonFormat;
LocalDateTime 类型:使用 @DateTimeFormat 时需要指定 iso 属性,@JsonFormat 的格式也需要适配。
例如这段错误代码,LocalDateTime 使用了 Date 的配置方式,导致前端传参解析失败:
@Data
class UserForm {
// 错误:LocalDateTime用这种配置,前端传参解析失败
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
正确用法:分场景配置(直接复制能用)
无论是 GET 请求的查询参数,还是 POST 表单提交的参数,都应该使用 @DateTimeFormat:
// 1. GET请求(路径参数/请求参数)
@GetMapping("/getByTime")
public Result getByTime(
// 前端传 ?createTime=2024-05-20 12:00:00,能正确解析
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date createTime) {
return Result.success(createTime);
}
// 2. POST表单请求
@PostMapping("/saveForm")
public Result saveForm(
// 表单传createTime=2024-05-20,解析成LocalDate
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate createTime) {
return Result.success(createTime);
}
// 3. JSON请求体(LocalDateTime类型)
@PostMapping("/saveJson")
public Result saveJson(@RequestBody UserForm form) {
return Result.success(form.getCreateTime());
}
@Data
class UserForm {
// LocalDateTime类型的入参,必须加iso属性
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime createTime;
}
这里的核心是必须配置时区 timezone = "GMT+8",否则会差8小时:
@GetMapping("/getUser")
public Result<User> getUser() {
User user = new User();
user.setCreateTime(new Date());
user.setUpdateTime(LocalDateTime.now());
return Result.success(user);
}
@Data
class User {
// Date类型出参:指定格式+时区
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
// LocalDateTime类型出参:同样要指定时区
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}
场景3:既收参又返参(两个注解一起用)
这是最常见的业务场景,入参用 @DateTimeFormat,出参用 @JsonFormat,一起加在字段上即可:
@Data
class User {
// 入参解析:@DateTimeFormat 生效
// 出参格式化:@JsonFormat 生效
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
// LocalDateTime类型的既收又返
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss", iso = DateTimeFormat.ISO.DATE_TIME)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime updateTime;
}
场景4:全局配置(不用每个字段加注解)
如果项目内所有日期字段都需要统一的格式,不必为每个字段单独添加注解,直接在配置文件中进行全局设置会更省心,也能有效避免遗漏。这对于遵循统一规范的 Java 项目尤其有用。
# application.yml
spring:
# 1. 全局配置入参日期格式(对应@DateTimeFormat)
mvc:
format:
date: yyyy-MM-dd
date-time: yyyy-MM-dd HH:mm:ss
# 2. 全局配置出参日期格式(对应@JsonFormat)
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 适配LocalDateTime/LocalDate(Java8时间类型)
deserialization:
adjust-dates-to-context-time-zone: true
serialization:
write-dates-as-timestamps: false
配置完成后,所有的 Date / LocalDateTime 类型字段:
- 入参会自动按照配置的格式解析,无需再加
@DateTimeFormat;
- 出参会自动按照配置的格式返回,无需再加
@JsonFormat;
- 时区统一为东八区,不会再出现8小时的时差问题。
线上真实踩坑案例:日期格式错导致订单时间混乱
某电商项目中,用户提交订单时,前端传入 “2024-05-20 18:00:00”,后端错误地使用了 @JsonFormat 来接收,导致解析失败,订单创建时间被默认设置为当前系统时间。后续财务对账时,发现了大量订单时间错乱,引发严重问题。
修复过程如下:
- 将入参字段上的
@JsonFormat 替换为正确的 @DateTimeFormat;
- 针对
LocalDateTime 类型字段,补充了 iso 属性配置;
- 在项目中增加了全局的日期格式和时区配置,避免后续新增字段时遗漏注解;
- 上线后,订单时间解析准确率达到100%,对账问题得到彻底解决。
总结
@DateTimeFormat 和 @JsonFormat 引发的各种问题,根源往往不在于注解本身复杂,而在于没有理解它们明确的分工:一个负责“进”,一个负责“出”。
记住下面3个核心要点,基本就能避开所有常见坑:
- 收参用
@DateTimeFormat,返参用 @JsonFormat,角色不能互换。
- 使用
@JsonFormat 必须配置 timezone = "GMT+8",这是解决时差问题的关键。
LocalDateTime 类型使用 @DateTimeFormat 时要加 iso 属性,不能和 Date 类型的配置混淆。
如果你的项目对日期格式有统一要求,强烈建议优先采用全局配置方案,这样既省去了逐个字段添加注解的繁琐,也降低了因疏忽导致配置遗漏的风险。本质上,处理好日期格式问题,核心就是做到“阶段对齐、时区对齐、类型对齐”,把握住这三点,相关的困扰便能迎刃而解。更多关于此类配置的细节和最佳实践,可以参考 技术文档 专区中的相关指南。