深入理解 Spring Boot 中日期时间格式化:@DateTimeFormat 与 @JsonFormat 完整实践

在开发 Spring Boot 应用时,我们经常会遇到一个问题:如何在与前端交互时,既能正确解析前端传来的日期字符串,又能以指定格式把日期数据返回给前端。其实,要解决这个问题并不难,关键在于两个非常有用的注解------ @DateTimeFormat@JsonFormat。今天就让我们以博客的形式,深入聊聊这两个注解的来龙去脉,并通过详细的代码案例,从浅入深讲解它们的用法和注意细节。


为什么需要格式化日期时间?

在前后端开发中,日期和时间一直都是一个容易出错的点。前端可能会以不同的格式(比如 2025-04-08 14:30:152025/04/08 或者 ISO 8601 标准的 2025-04-08T14:30:15)发送数据,而后端在反序列化时必须清楚如何解析才能转换成 Java 的日期类型(如 LocalDateTimeLocalDateDate)。同样,当我们的后端需要返回数据时,为了让前端能够方便地展示也需要确定特定的日期格式。Spring Boot 刚好为我们提供了两个注解来分别应对这两个环节:

  • @DateTimeFormat 用于【数据绑定】阶段,即接收前端传来的字符串,并将其转换为 Java 时间对象。
  • @JsonFormat 用于【序列化】阶段,即当对象返回给前端时,把 Java 时间对象转换为指定格式的字符串。

一、@DateTimeFormat:精准解析前端传入的日期数据

@DateTimeFormat 可以应用在 Controller 方法参数、表单对象字段或者实体类的属性上。当前端传来的日期字符串与我们定义的 pattern 匹配时,Spring 会自动将其转换成对应的日期类型。

示例代码

假如我们有一个用户注册的请求,前端传来用户的生日信息。我们可以构建如下的 DTO 类:

java 复制代码
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;

/**
 * 用户数据传输对象,用于接收前端传来的参数
 */
public class UserDTO {

    // 用户姓名
    private String name;

    /**
     * 用户生日
     * 说明: 当前端传过来的字符串类似 "2025-04-08 14:30:15" 时,
     * 使用 @DateTimeFormat 注解指定的 pattern 将会被用来解析成 LocalDateTime 对象
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;

    // 省略 getter 和 setter 方法
}

在 Controller 中,我们可以直接使用这个 DTO 对象接收前端数据:

java 复制代码
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/register")
    public String registerUser(@RequestBody UserDTO userDTO) {
        // userDTO.getBirthday() 已经自动解析成 LocalDateTime 类型
        // 这里可以添加你的业务逻辑处理,比如保存到数据库
        return "用户注册成功!";
    }
}

注意

  • @DateTimeFormat 只能影响反序列化,即把字符串转换为 Java 时间对象,而不会影响返回给前端的格式化。
  • 如果前端发送的时间格式与 pattern 不一致,会导致解析失败并报 400 错误。

二、@JsonFormat:定义返回给前端的日期格式

当你准备好将数据返回给前端时,希望看到的日期格式与前端的要求或者 UI 设计一致。这时就需要利用 @JsonFormat 注解来对日期进行格式化。通常,我们会同时在同一字段上添加两个注解,从而分别控制数据输入和输出。

示例代码

我们修改上述 DTO 类,新增一个 createTime 字段,并同时使用 @DateTimeFormat@JsonFormat 注解:

java 复制代码
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;

/**
 * 用户视图对象,用于数据交互
 */
public class UserVO {

    // 用户姓名
    private String name;

    /**
     * 用户生日:只参与数据绑定(输入)时的解析
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;

    /**
     * 用户创建时间:接收时使用 @DateTimeFormat 解析,返回时通过 @JsonFormat 格式化
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy/MM/dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;

    // getter 与 setter 方法
}

在这个例子中:

  • 当我们接收前端传来的 "birthday": "2025-04-08 14:30:15" 字符串时,会使用 @DateTimeFormat 转化为 LocalDateTime 对象。
  • 当系统返回 createTime 字段时,通过 @JsonFormat 指定的 "yyyy/MM/dd HH:mm:ss" 格式将其序列化,比如返回 "2025/04/08 15:22:10"
  • timezone = "GMT+8" 确保了时区正确,避免因默认时区导致的时间偏差问题。

三、完整示例:从前端数据接收、业务处理到返回结果

为了让你直观地理解整个流程,我们把 Controller、Service 和 DTO 结合起来,构建一个完整的示例。这里我们设计一个新建用户的功能,既包含接收前端传来的日期字符串,也包含生成并返回格式化后的创建时间。

1. 数据传输对象 UserDTO

java 复制代码
import org.springframework.format.annotation.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;

/**
 * 用户数据传输对象,用于接收前端数据与返回结果的格式化
 */
public class UserDTO {

    // 用户姓名
    private String name;

    /**
     * 用户生日,前端通过 JSON 字符串传递,
     * 使用 @DateTimeFormat 进行解析
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;

    /**
     * 用户创建时间,后台生成,并且在返回时格式化展示
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;

    // Constructors、getter 与 setter 方法
}

2. Service 层处理用户数据

java 复制代码
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;

@Service
public class UserService {

    /**
     * 模拟创建用户的业务逻辑
     * @param userDTO 接收前端传来的用户数据
     * @return 返回处理后的用户数据,其中包含创建时间
     */
    public UserDTO createUser(UserDTO userDTO) {
        // 模拟保存到数据库操作,此处只做简单处理
        // 手动设置创建时间为当前系统时间
        userDTO.setCreateTime(LocalDateTime.now());

        // 可能还会做一些校验、赋值、日志记录等操作

        return userDTO;
    }
}

3. Controller 层暴露 REST 接口

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 新建用户接口
     * 接收 JSON 数据,并返回格式化后的用户信息
     * 前端请求例子:
     * POST /api/users
     * {
     *   "name": "张三",
     *   "birthday": "2025-05-08 10:10:10"
     * }
     */
    @PostMapping("/users")
    public UserDTO createUser(@RequestBody UserDTO userDTO) {
        // userDTO.getBirthday() 会使用 @DateTimeFormat 解析成 LocalDateTime 对象
        // 在 Service 层中,生成 createTime 字段
        UserDTO savedUser = userService.createUser(userDTO);
        // 返回的 savedUser 中,createTime 将通过 @JsonFormat 格式化为指定格式
        return savedUser;
    }
}

在这个完整示例中:

  1. 前端以以下 JSON 数据请求用户创建接口:

    java 复制代码
    {
        "name": "张三",
        "birthday": "2025-05-08 10:10:10"
    }
  2. Spring Boot 根据 @DateTimeFormat 解析 birthday 并封装到 UserDTO 中。

  3. Service 层自动设置 createTime 为当前时间,传回 Controller。

  4. Controller 返回时,Jackson 处理 createTime 字段时会应用 @JsonFormat 指定的格式,确保前端看到正确的时间格式。


四、常见问题与实践经验

在实际开发中,你可能会遇到以下一些问题:

  1. 返回格式不对:
    如果你在实体上只使用了 @DateTimeFormat 而没有添加 @JsonFormat,那么返回的 JSON 可能仍然使用 ISO-8601 格式(例如 "2025-04-08T14:30:15")。解决方法是确保需要格式化的字段上同时加上 @JsonFormat 注解。
  2. 解析失败:
    当前端传入的字符串格式和 @DateTimeFormat 定义的 pattern 不一致时,会出现解析错误。务必确保前端传输的日期格式和后端预期格式完全匹配;或者使用 ISO 枚举例如 @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) 以提高容错率。
  3. 时区问题:
    由于不同环境的默认时区可能不同,建议在 @JsonFormat 中明确指定 timezone,比如 timezone = "GMT+8",以防服务器默认时区导致时间显示偏差。
  4. 全局配置 vs 局部注解:
    为了避免每个实体类都写重复的注解,部分团队会在全局配置 Jackson 序列化/反序列化的日期格式。但有时某些字段需要特殊格式时,直接在字段上添加注解即可。两者可以灵活搭配使用。

五、总结

这篇文章详细讲解了如何使用 @DateTimeFormat@JsonFormat 来解决 Spring Boot 应用中日期时间格式化的常见问题。

  • @DateTimeFormat 主要用于【数据绑定】阶段,将前端传来的字符串转换为 Java 时间对象。
  • @JsonFormat 则用于【序列化】阶段,将 Java 时间对象以特定格式输出给前端。

两者合用可确保前后端无缝对接,既方便了数据输入,也优化了数据输出的格式。希望这篇博客能帮助你在项目中更游刃有余地处理日期时间相关的需求,避免踩坑。

相关推荐
angushine2 小时前
Gateway获取下游最终响应码
java·开发语言·gateway
爱的叹息2 小时前
关于 JDK 中的 jce.jar 的详解,以及与之功能类似的主流加解密工具的详细对比分析
java·python·jar
一一Null2 小时前
Token安全存储的几种方式
android·java·安全·android studio
来自星星的坤2 小时前
SpringBoot 与 Vue3 实现前后端互联全解析
后端·ajax·前端框架·vue·springboot
AUGENSTERN_dc2 小时前
RaabitMQ 快速入门
java·后端·rabbitmq
晓纪同学3 小时前
C++ Primer (第五版)-第十三章 拷贝控制
java·开发语言·c++
小样vvv3 小时前
【源码】SpringMvc源码分析
java
nzwen6663 小时前
Redis学习笔记及总结
java·redis·学习笔记
烛阴3 小时前
零基础必看!Express 项目 .env 配置,开发、测试、生产环境轻松搞定!
javascript·后端·express