
在 Java 开发中,日期时间的序列化与反序列化一直是令人头疼的问题。当我们在 DTO(数据传输对象)中处理日期时间字段时,总会遇到三个注解:@JsonFormat、@DateTimeFormat 和 @JSONField。它们看似功能相似,实则各有侧重,使用场景也大不相同。
很多开发者在实际项目中常常混淆这三个注解的用法,导致出现诸如 "前端传的日期后端解析报错"、"接口返回的日期格式不符合要求"、"不同框架下日期处理不一致" 等问题。本文将从底层原理到实际应用,全方位解析这三个注解的区别与联系,并通过大量可运行的实例,教会你在 DTO 中如何正确使用它们,彻底解决日期时间处理的痛点。
要真正理解这三个注解的区别,首先需要了解它们各自的 "出身" 和设计目的。不同的注解来自不同的框架,服务于不同的场景,这是它们最本质的区别。
@JsonFormat 注解来自于 Jackson 库,这是一个在 Java 生态中广泛使用的 JSON 处理库。Spring Boot 默认集成了 Jackson 作为 JSON 消息转换器,因此在 Spring 生态中,@JsonFormat 的使用非常普遍。
设计初衷:主要用于控制 JSON 数据与 Java 对象之间的序列化(Java 对象→JSON)和反序列化(JSON→Java 对象)过程中日期时间字段的格式转换。
权威来源:根据 Jackson 官方文档(https://fasterxml.github.io/jackson-annotations/javadoc/2.15/com/fasterxml/jackson/annotation/JsonFormat.html),@JsonFormat 是 Jackson 提供的核心注解之一,用于指定字段的序列化和反序列化格式。
@DateTimeFormat 注解来自于 Spring Framework,是 Spring Web 模块中的一个重要注解。
设计初衷:主要用于将前端(如 HTML 表单、URL 参数)传递的字符串类型的日期时间参数,正确解析为 Java 中的日期时间对象(如 java.util.Date、java.time.LocalDateTime 等)。
权威来源:Spring 官方文档(https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/format/annotation/DateTimeFormat.html)明确指出,@DateTimeFormat 用于指定日期时间类型字段的格式化方式,主要用于 Web 请求参数的绑定。
@JSONField 注解来自于阿里巴巴的 Fastjson 库,这是国内广泛使用的另一个高性能 JSON 处理库。
设计初衷:功能类似于 @JsonFormat,但它是 Fastjson 框架提供的注解,用于控制 Fastjson 在序列化和反序列化过程中对字段的处理,包括日期格式化、字段名称映射等。
权威来源:Fastjson 官方文档(https://github.com/alibaba/fastjson/wiki/JSONField)详细说明了 @JSONField 的用法,它是 Fastjson 进行 JSON 处理时的核心注解之一。
下面的流程图清晰展示了三个注解在请求处理流程中的作用位置:

注:
[客户端]-->1. 发送请求 {请求类型}[JSON响应]-->2. 接收响应 [客户端]
从流程图中可以清晰地看到:
在开始具体示例之前,我们需要先搭建一个标准的 Spring Boot 项目,并引入相关依赖。以下是必要的 Maven 配置:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.jam.example</groupId>
<artifactId>date-annotation-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>date-annotation-demo</name>
<description>Demo project for date annotation comparison</description>
<properties>
<java.version>17</java.version>
<fastjson.version>2.0.32</fastjson.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<mysql.version>8.0.33</mysql.version>
<lombok.version>1.18.30</lombok.version>
<commons-lang3.version>3.14.0</commons-lang3.version>
<springdoc.version>2.1.0</springdoc.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- Commons Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!-- Jackson Datetime -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- Fastjson -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- SpringDoc OpenAPI (Swagger3) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
以上配置包含了我们示例中需要用到的所有依赖,包括 Spring Boot Web、Lombok、Jackson、Fastjson、MyBatis-Plus、MySQL 驱动以及 Swagger3 等,并使用了各组件的最新稳定版本。
@JsonFormat 注解有多个属性,其中最常用的有:
@JsonFormat 主要用于以下两个场景:
首先,我们创建一个使用 @JsonFormat 注解的 DTO 类:
package com.jam.example.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 演示@JsonFormat注解使用的DTO类
*
* @author 果酱
*/
@Data
@Schema(description = "JsonFormat演示DTO")
public class JsonFormatDemoDTO {
@Schema(description = "使用java.util.Date类型的日期时间字段")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date dateField;
@Schema(description = "使用java.time.LocalDateTime类型的日期时间字段")
@JsonFormat(pattern = "yyyy年MM月dd日 HH时mm分ss秒", timezone = "GMT+8")
private LocalDateTime localDateTimeField;
}
接下来,创建一个控制器来测试这个 DTO:
package com.jam.example.controller;
import com.jam.example.dto.JsonFormatDemoDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 演示@JsonFormat注解使用的控制器
*
* @author 果酱
*/
@RestController
@RequestMapping("/json-format")
@Tag(name = "JsonFormat演示接口", description = "用于测试@JsonFormat注解的功能")
@Slf4j
public class JsonFormatDemoController {
/**
* 测试@JsonFormat的反序列化功能
*
* @param dto 包含日期时间字段的DTO对象
* @return 接收到的DTO对象,用于展示序列化效果
*/
@PostMapping("/test")
@Operation(summary = "测试@JsonFormat注解", description = "接收包含日期时间的JSON数据,并返回处理后的结果")
public JsonFormatDemoDTO testJsonFormat(@RequestBody JsonFormatDemoDTO dto) {
log.info("接收到的dateField: {}", dto.getDateField());
log.info("接收到的localDateTimeField: {}", dto.getLocalDateTimeField());
return dto;
}
}
启动应用后,我们可以通过 Swagger UI(访问http://localhost:8080/swagger-ui.html)来测试这个接口。
测试步骤:
{
"dateField":"2023-12-01 12:34:56",
"localDateTimeField":"2023年12月01日 12时34分56秒"
}
接收到的dateField: Fri Dec 01 12:34:56 CST 2023
接收到的localDateTimeField: 2023-12-01T12:34:56
{
"dateField":"2023-12-01 12:34:56",
"localDateTimeField":"2023年12月01日 12时34分56秒"
}
结果分析:
特别注意:
在实际项目中,我们经常需要将 DTO 中的日期时间字段与数据库中的日期时间字段进行映射。MyBatis-Plus 提供了自动映射功能,结合 @JsonFormat 可以很方便地处理这种场景。
首先,创建一个实体类:
package com.jam.example.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 演示日期时间字段的实体类
*
* @author 果酱
*/
@Data
@TableName("date_demo")
public class DateDemoEntity {
@TableId(type = IdType.AUTO)
private Long id;
@TableField("date_column")
private Date dateColumn;
@TableField("local_date_time_column")
private LocalDateTime localDateTimeColumn;
}
然后,创建一个 Mapper 接口:
package com.jam.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.example.entity.DateDemoEntity;
import org.apache.ibatis.annotations.Mapper;
/**
* DateDemoEntity的Mapper接口
*
* @author 果酱
*/
@Mapper
public interface DateDemoMapper extends BaseMapper<DateDemoEntity> {
}
接下来,创建一个 Service 类:
package com.jam.example.service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jam.example.entity.DateDemoEntity;
import com.jam.example.mapper.DateDemoMapper;
import com.jam.example.dto.JsonFormatDemoDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* 日期时间处理的服务类
*
* @author 果酱
*/
@Service
@Slf4j
public class DateDemoService extends ServiceImpl<DateDemoMapper, DateDemoEntity> {
/**
* 将DTO转换为实体并保存
*
* @param dto 包含日期时间的DTO
* @return 保存后的实体ID
*/
public Long saveFromJsonFormatDemoDTO(JsonFormatDemoDTO dto) {
Objects.requireNonNull(dto, "DTO对象不能为空");
DateDemoEntity entity = new DateDemoEntity();
entity.setDateColumn(dto.getDateField());
entity.setLocalDateTimeColumn(dto.getLocalDateTimeField());
baseMapper.insert(entity);
log.info("保存实体成功,ID: {}", entity.getId());
return entity.getId();
}
/**
* 根据ID查询实体并转换为DTO
*
* @param id 实体ID
* @return 转换后的DTO对象
*/
public JsonFormatDemoDTO getJsonFormatDemoDTOById(Long id) {
Objects.requireNonNull(id, "ID不能为空");
DateDemoEntity entity = baseMapper.selectById(id);
if (Objects.isNull(entity)) {
log.warn("未找到ID为{}的实体", id);
return null;
}
JsonFormatDemoDTO dto = new JsonFormatDemoDTO();
dto.setDateField(entity.getDateColumn());
dto.setLocalDateTimeField(entity.getLocalDateTimeColumn());
return dto;
}
}
最后,在控制器中添加相关接口:
@Autowired
private DateDemoService dateDemoService;
/**
* 测试@JsonFormat与MyBatis-Plus结合使用
*
* @param dto 包含日期时间字段的DTO对象
* @return 保存后的DTO对象
*/
@PostMapping("/save")
@Operation(summary = "保存日期时间数据", description = "将DTO数据保存到数据库,并返回结果")
public JsonFormatDemoDTO saveJsonFormatDemoDTO(@RequestBody JsonFormatDemoDTO dto) {
Long id = dateDemoService.saveFromJsonFormatDemoDTO(dto);
return dateDemoService.getJsonFormatDemoDTOById(id);
}
数据库表结构(MySQL):
CREATE TABLE `date_demo` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`date_column` datetime DEFAULT NULL COMMENT '日期时间字段(java.util.Date)',
`local_date_time_column` datetime DEFAULT NULL COMMENT '日期时间字段(java.time.LocalDateTime)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='日期时间处理演示表';
通过这个示例,我们可以看到 @JsonFormat 注解在从 JSON 到 DTO、DTO 到实体、实体到数据库的整个流程中,都能正确处理日期时间的格式转换。
@DateTimeFormat 注解的主要属性有:
@DateTimeFormat 主要用于将前端通过表单提交或 URL 参数传递的日期时间字符串,正确解析为 Java 中的日期时间对象。它通常用于处理application/x-www-form-urlencoded类型的请求数据,而不是 JSON 格式的数据。
权威来源:Spring 官方文档(https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder)指出,@DateTimeFormat 注解需要配合 Spring 的格式化机制使用,通常在控制器方法的参数上使用。
创建一个使用 @DateTimeFormat 注解的 DTO 类:
package com.jam.example.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 演示@DateTimeFormat注解使用的DTO类
*
* @author 果酱
*/
@Data
@Schema(description = "DateTimeFormat演示DTO")
public class DateTimeFormatDemoDTO {
@Schema(description = "使用pattern的Date类型字段")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date datePattern;
@Schema(description = "使用ISO的LocalDate类型字段")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private LocalDate localDateIso;
@Schema(description = "使用pattern的LocalTime类型字段")
@DateTimeFormat(pattern = "HH:mm:ss")
private LocalTime localTimePattern;
@Schema(description = "使用ISO的LocalDateTime类型字段")
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime localDateTimeIso;
@Schema(description = "使用style的LocalDate类型字段")
@DateTimeFormat(style = "S-") // 短日期,忽略时间
private LocalDate localDateStyle;
}
创建对应的控制器:
package com.jam.example.controller;
import com.jam.example.dto.DateTimeFormatDemoDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.ModelAttribute;
import java.time.LocalDate;
/**
* 演示@DateTimeFormat注解使用的控制器
*
* @author 果酱
*/
@RestController
@RequestMapping("/date-time-format")
@Tag(name = "DateTimeFormat演示接口", description = "用于测试@DateTimeFormat注解的功能")
@Slf4j
public class DateTimeFormatDemoController {
/**
* 测试@DateTimeFormat在表单提交中的使用
*
* @param dto 包含日期时间字段的DTO对象
* @return 接收到的DTO对象
*/
@PostMapping("/form")
@Operation(summary = "测试表单提交的日期解析", description = "接收表单格式的日期时间数据")
public DateTimeFormatDemoDTO testFormSubmit(@ModelAttribute DateTimeFormatDemoDTO dto) {
log.info("表单提交的datePattern: {}", dto.getDatePattern());
log.info("表单提交的localDateIso: {}", dto.getLocalDateIso());
log.info("表单提交的localTimePattern: {}", dto.getLocalTimePattern());
log.info("表单提交的localDateTimeIso: {}", dto.getLocalDateTimeIso());
log.info("表单提交的localDateStyle: {}", dto.getLocalDateStyle());
return dto;
}
/**
* 测试@DateTimeFormat在URL参数中的使用
*
* @param date 参数日期
* @return 接收到的日期
*/
@GetMapping("/param")
@Operation(summary = "测试URL参数的日期解析", description = "接收URL参数中的日期时间数据")
public LocalDate testUrlParam(
@Parameter(description = "日期参数,格式为yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
log.info("URL参数中的date: {}", date);
return date;
}
}
测试表单提交:
发送一个application/x-www-form-urlencoded类型的 POST 请求,参数如下:
datePattern=2023-12-01
localDateIso=2023-12-02
localTimePattern=12:34:56
localDateTimeIso=2023-12-03T13:45:22
localDateStyle=23-12-04
观察控制台输出:
表单提交的datePattern: Fri Dec 01 00:00:00 CST 2023
表单提交的localDateIso: 2023-12-02
表单提交的localTimePattern: 12:34:56
表单提交的localDateTimeIso: 2023-12-03T13:45:22
表单提交的localDateStyle: 2023-12-04
测试 URL 参数:
发送一个 GET 请求:http://localhost:8080/date-time-format/param?date=2023-12-05
观察控制台输出:
URL参数中的date: 2023-12-05
结果分析:
特别注意:
MethodArgumentTypeMismatchException异常。在实际项目中,我们应该添加全局异常处理器来处理这种情况。如前所述,当日期格式不匹配时,Spring 会抛出异常。我们可以添加一个全局异常处理器来友好地处理这种情况:
package com.jam.example.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*
* @author 果酱
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理方法参数类型不匹配异常(包括日期格式错误)
*
* @param ex 异常对象
* @return 错误信息
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex) {
log.error("方法参数类型不匹配: {}", ex.getMessage(), ex);
Map<String, String> error = new HashMap<>(2);
error.put("code", "INVALID_PARAM_TYPE");
String message = String.format(
"参数 '%s' 的值 '%s' 格式不正确,期望类型: %s",
ex.getName(), ex.getValue(), ex.getRequiredType().getSimpleName()
);
error.put("message", message);
return error;
}
/**
* 处理请求参数缺失异常
*
* @param ex 异常对象
* @return 错误信息
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleMissingServletRequestParameter(MissingServletRequestParameterException ex) {
log.error("请求参数缺失: {}", ex.getMessage(), ex);
Map<String, String> error = new HashMap<>(2);
error.put("code", "MISSING_PARAM");
error.put("message", String.format("缺少必要参数: %s", ex.getParameterName()));
return error;
}
/**
* 处理方法参数验证异常
*
* @param ex 异常对象
* @return 错误信息
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
log.error("方法参数验证失败: {}", ex.getMessage(), ex);
Map<String, String> error = new HashMap<>(2);
error.put("code", "INVALID_PARAM");
String message = ex.getBindingResult().getFieldError() != null
? ex.getBindingResult().getFieldError().getDefaultMessage()
: "参数验证失败";
error.put("message", message);
return error;
}
/**
* 处理其他未捕获的异常
*
* @param ex 异常对象
* @return 错误信息
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, String> handleOtherExceptions(Exception ex) {
log.error("发生未预期的异常: {}", ex.getMessage(), ex);
Map<String, String> error = new HashMap<>(2);
error.put("code", "SYSTEM_ERROR");
error.put("message", "系统内部错误,请联系管理员");
return error;
}
}
这个全局异常处理器不仅处理了日期格式不匹配的异常,还处理了其他常见的请求参数相关异常,使我们的应用更加健壮。
@JSONField 注解来自 Fastjson,它的常用属性有:
@JSONField 是 Fastjson 提供的注解,用于控制 Fastjson 在序列化和反序列化过程中对字段的处理。它的使用场景与 @JsonFormat 类似,但依赖于 Fastjson 框架。
Spring Boot 默认使用 Jackson 作为 JSON 处理器,要使用 Fastjson,我们需要进行一些配置:
package com.jam.example.config;
import com.alibaba.fastjson2.support.config.FastJsonConfig;
import com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* Web MVC配置类,用于配置Fastjson作为默认JSON处理器
*
* @author 果酱
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 配置Fastjson作为HTTP消息转换器
*
* @param converters 消息转换器列表
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建Fastjson消息转换器
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
// 配置Fastjson
FastJsonConfig config = new FastJsonConfig();
config.setCharset(StandardCharsets.UTF_8);
// 设置支持的媒体类型
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.APPLICATION_JSON);
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
mediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
converter.setSupportedMediaTypes(mediaTypes);
converter.setFastJsonConfig(config);
// 将Fastjson消息转换器添加到列表最前面,使其优先使用
converters.add(0, converter);
}
}
创建一个使用 @JSONField 注解的 DTO 类:
package com.jam.example.dto;
import com.alibaba.fastjson2.annotation.JSONField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Date;
/**
* 演示@JSONField注解使用的DTO类
*
* @author 果酱
*/
@Data
@Schema(description = "JSONField演示DTO")
public class JSONFieldDemoDTO {
@Schema(description = "使用java.util.Date类型的日期时间字段")
@JSONField(format = "yyyy-MM-dd HH:mm:ss", name = "date_field")
private Date dateField;
@Schema(description = "使用java.time.LocalDateTime类型的日期时间字段")
@JSONField(format = "yyyy年MM月dd日 HH时mm分ss秒", ordinal = 1)
private LocalDateTime localDateTimeField;
@Schema(description = "不序列化的字段")
@JSONField(serialize = false)
private String secretInfo;
@Schema(description = "不反序列化的字段")
@JSONField(deserialize = false)
private String readOnlyInfo;
}
创建对应的控制器:
package com.jam.example.controller;
import com.jam.example.dto.JSONFieldDemoDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 演示@JSONField注解使用的控制器
*
* @author 果酱
*/
@RestController
@RequestMapping("/json-field")
@Tag(name = "JSONField演示接口", description = "用于测试@JSONField注解的功能")
@Slf4j
public class JSONFieldDemoController {
/**
* 测试@JSONField的序列化和反序列化功能
*
* @param dto 包含日期时间字段的DTO对象
* @return 处理后的DTO对象
*/
@PostMapping("/test")
@Operation(summary = "测试@JSONField注解", description = "接收包含日期时间的JSON数据,并返回处理后的结果")
public JSONFieldDemoDTO testJSONField(@RequestBody JSONFieldDemoDTO dto) {
log.info("接收到的dateField: {}", dto.getDateField());
log.info("接收到的localDateTimeField: {}", dto.getLocalDateTimeField());
log.info("接收到的secretInfo: {}", dto.getSecretInfo());
log.info("接收到的readOnlyInfo: {}", dto.getReadOnlyInfo());
// 设置一些值,用于测试序列化
dto.setSecretInfo("这是敏感信息,不应该被序列化");
dto.setReadOnlyInfo("这是只读信息,应该被序列化但不应该被反序列化");
return dto;
}
}
发送如下 JSON 请求体:
{
"date_field": "2023-12-01 12:34:56",
"localDateTimeField": "2023年12月01日 12时34分56秒",
"secretInfo": "客户端发送的敏感信息",
"readOnlyInfo": "客户端尝试修改的只读信息"
}
观察控制台输出:
接收到的dateField: Fri Dec 01 12:34:56 CST 2023
接收到的localDateTimeField: 2023-12-01T12:34:56
接收到的secretInfo: 客户端发送的敏感信息
接收到的readOnlyInfo: null
观察接口返回的 JSON 数据:
{
"localDateTimeField":"2023年12月01日 12时34分56秒",
"date_field":"2023-12-01 12:34:56",
"readOnlyInfo":"这是只读信息,应该被序列化但不应该被反序列化"
}
结果分析:
特别注意:
经过前面的详细介绍和实战示例,我们已经对 @JsonFormat、@DateTimeFormat 和 @JSONField 有了深入的了解。现在,我们来对这三个注解进行全面对比,并给出在实际项目中如何选择的指南。
特性 | @JsonFormat | @DateTimeFormat | @JSONField |
|---|---|---|---|
所属框架 | Jackson | Spring Framework | Fastjson |
主要用途 | JSON 序列化 / 反序列化 | 表单 / URL 参数解析 | JSON 序列化 / 反序列化 |
日期格式化 | 支持(pattern) | 支持(pattern、iso、style) | 支持(format) |
时区设置 | 支持(timezone) | 不支持 | 支持(timezone) |
字段名称映射 | 不支持 | 不支持 | 支持(name) |
序列化控制 | 支持 | 不支持 | 支持(serialize) |
反序列化控制 | 支持 | 不支持 | 支持(deserialize) |
字段顺序控制 | 不支持 | 不支持 | 支持(ordinal) |
适用数据格式 | JSON | 表单 / URL 参数 | JSON |
Java 8 日期支持 | 需要 jackson-datatype-jsr310 | 原生支持 | 原生支持 |

在实际项目中,我们经常需要组合使用这些注解。例如,一个 DTO 可能既需要接收表单提交的数据,又需要接收 JSON 数据,这时就需要同时使用 @DateTimeFormat 和 @JsonFormat(或 @JSONField)。
下面是一个组合使用的示例:
package com.jam.example.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.alibaba.fastjson2.annotation.JSONField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
/**
* 演示多个注解组合使用的DTO类
*
* @author 果酱
*/
@Data
@Schema(description = "多注解组合使用演示DTO")
public class CombinedAnnotationDTO {
@Schema(description = "同时使用三个注解的日期时间字段")
// 处理表单/URL参数
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
// 处理Jackson的JSON序列化/反序列化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
// 处理Fastjson的JSON序列化/反序列化
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime dateTime;
}这个 DTO 中的 dateTime 字段可以同时处理三种情况:
虽然使用注解可以灵活地控制日期时间的格式化,但在实际项目中,我们通常会采用全局配置的方式来统一处理日期时间格式,减少重复代码。下面介绍几种常用的全局配置方案。
package com.jam.example.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.format.DateTimeFormatter;
/**
* Jackson全局配置,统一日期时间格式化
*
* @author 果酱
*/
@Configuration
public class JacksonConfig {
// 日期时间格式
public static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
// 日期格式
public static final String DATE_FORMAT = "yyyy-MM-dd";
// 时间格式
public static final String TIME_FORMAT = "HH:mm:ss";
/**
* 配置Jackson的ObjectMapper
*
* @param builder Jackson2ObjectMapperBuilder
* @return 配置好的ObjectMapper
*/
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
// 配置Java 8日期时间类型的序列化器和反序列化器
// LocalDateTime
objectMapper.registerModule(new com.fasterxml.jackson.datatype.jsr310.JavaTimeModule()
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)))
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)))
// LocalDate
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)))
// LocalTime
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMAT))));
return objectMapper;
}
}
如果使用 Fastjson,可以通过修改前面的 WebMvcConfig 来进行全局配置:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 创建Fastjson消息转换器
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
// 配置Fastjson
FastJsonConfig config = new FastJsonConfig();
config.setCharset(StandardCharsets.UTF_8);
// 设置全局日期格式化
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
// 配置序列化特性
config.setSerializerFeatures(
SerializerFeature.PrettyFormat,
SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullStringAsEmpty,
SerializerFeature.WriteNullNumberAsZero,
SerializerFeature.WriteNullListAsEmpty,
SerializerFeature.WriteNullBooleanAsFalse,
SerializerFeature.DisableCircularReferenceDetect
);
// 设置支持的媒体类型
List<MediaType> mediaTypes = new ArrayList<>();
mediaTypes.add(MediaType.APPLICATION_JSON);
mediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
mediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
converter.setSupportedMediaTypes(mediaTypes);
converter.setFastJsonConfig(config);
// 将Fastjson消息转换器添加到列表最前面,使其优先使用
converters.add(0, converter);
}
可以通过注册 Formatter 来全局配置 @DateTimeFormat 的默认格式:
package com.jam.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.time.format.DateTimeFormatter;
/**
* Spring日期时间格式化全局配置
*
* @author 果酱
*/
@Configuration
public class DateTimeFormatConfig implements WebMvcConfigurer {
/**
* 注册全局日期时间格式化器
*
* @param registry FormatterRegistry
*/
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
// 配置LocalDateTime格式
registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// 配置LocalDate格式
registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
// 配置LocalTime格式
registrar.setTimeFormatter(DateTimeFormatter.ofPattern("HH:mm:ss"));
registrar.registerFormatters(registry);
}
}
全局配置和注解可以结合使用,它们的优先级关系是:注解配置 > 全局配置。
这意味着:
这种机制既保证了项目中日期格式的统一性(通过全局配置),又保留了灵活性(通过注解进行特殊处理)。
在使用这三个注解的过程中,开发者经常会遇到一些问题。下面列举一些常见问题及解决方案。
问题描述:前端传递的日期时间字符串无法被正确解析,抛出转换异常。
可能原因:
解决方案:
问题描述:解析后的时间比实际时间少 8 小时,或返回给前端的时间比实际时间少 8 小时。
可能原因:
解决方案:
问题描述:添加了注解,但日期格式没有按照预期进行转换。
可能原因:
解决方案:
问题描述:使用 LocalDateTime 等 Java 8 日期类型时,序列化后得到的是一个包含年月日等字段的对象,而不是期望的字符串。
可能原因:
解决方案:
本文详细介绍了 @JsonFormat、@DateTimeFormat 和 @JSONField 三个注解的区别与使用方法,核心知识点包括:
在实际项目中,建议按照以下步骤处理日期时间: