Spring Boot项目里Jackson的@JsonFormat注解突然不灵了?排查后发现是Gson在‘搞鬼’
2026/4/24 15:51:19 网站建设 项目流程

Spring Boot项目中Jackson的@JsonFormat注解失效:Gson冲突的深度排查指南

问题现象:当日期格式化突然"罢工"

上周三凌晨两点,我被一通紧急电话吵醒。团队里的小王在电话那头焦急地说:"线上订单系统的创建时间全部变成了时间戳!明明上周还正常的!"我揉了揉眼睛,打开IDE检查代码——所有日期字段都规规矩矩地标注着@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss"),但返回给前端的JSON却变成了毫秒数。更诡异的是,本地测试环境一切正常,只有生产环境出现这个问题。

这种看似简单的配置失效背后,往往隐藏着Spring Boot生态中JSON库的"权力斗争"。当你的@JsonFormat突然失灵时,不要急着怀疑人生——很可能是项目中混入了其他JSON库,比如Gson,悄悄篡夺了Jackson的序列化权杖。

排查思路:从症状到根源的侦探游戏

1. 基础检查:排除低级错误

首先进行常规检查,就像医生问诊一样:

// 确认注解使用正确示例 public class OrderDTO { @JsonFormat( shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai" ) private Date createTime; // getters & setters }

常见低级错误包括:

  • 忘记引入jackson-databind依赖(虽然Spring Boot starter已经包含)
  • 注解拼写错误(如@JsonFormt
  • 错误的pattern格式(如月份用MM而不是mm
  • 时区未指定导致显示时间偏差

2. 依赖检查:谁在悄悄搞破坏

当基础检查无果后,就该查看依赖树了。运行以下Maven命令:

mvn dependency:tree -Dincludes=com.fasterxml.jackson,com.google.gson

典型输出可能显示:

[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.12.3:compile [INFO] +- com.google.code.gson:gson:jar:2.8.6:compile [INFO] \- org.springframework.boot:spring-boot-starter-web:jar:2.5.0:compile

危险信号:

  • Gson与Jackson共存
  • 存在GsonHttpMessageConverter配置
  • 引入了Swagger等可能自带Gson的组件

3. 配置检查:消息转换器的暗战

Spring MVC使用HttpMessageConverter处理请求响应转换。关键检查点:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // 这里可能藏着替换Jackson的代码 } }

典型问题场景:

  • 某处配置移除了MappingJackson2HttpMessageConverter
  • 添加了GsonHttpMessageConverter且优先级更高
  • 自定义了ObjectMapper但配置被覆盖

根因定位:Gson的"政变"现场

1. 转换器链的真相

通过调试模式查看当前生效的HttpMessageConverter

@GetMapping("/debug/converters") public List<String> listConverters() { return request.getServletContext() .getAttribute(WebMvcConfigurer.MESSAGE_CONVERTERS) .toString(); }

正常Spring Boot输出应包含:

[ByteArrayHttpMessageConverter, StringHttpMessageConverter, MappingJackson2HttpMessageConverter, ...]

但如果发现GsonHttpMessageConverter出现在MappingJackson2HttpMessageConverter之前,问题就找到了。

2. 配置冲突的常见来源

案例一:Swagger配置覆盖

@Configuration public class SwaggerConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter); converters.add(new GsonHttpMessageConverter()); // 这就是凶手! } }

案例二:依赖传递引入Gson

某些库会悄悄引入Gson依赖:

<dependency> <groupId>com.some.library</groupId> <artifactId>some-core</artifactId> <version>1.2.3</version> </dependency>

通过mvn dependency:tree发现后,可用<exclusions>排除:

<exclusions> <exclusion> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> </exclusion> </exclusions>

解决方案:恢复Jackson的王座

方案1:彻底移除Gson(推荐)

如果项目不需要Gson:

  1. 从pom.xml移除Gson依赖
  2. 删除所有GsonHttpMessageConverter配置
  3. 确保没有其他配置移除MappingJackson2HttpMessageConverter

方案2:和平共处策略

当必须使用Gson时:

@Configuration public class WebConfig implements WebMvcConfigurer { @Bean public MappingJackson2HttpMessageConverter jacksonConverter() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); return new MappingJackson2HttpMessageConverter(mapper); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // 确保Jackson在Gson之前 converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter); converters.add(0, jacksonConverter()); } }

关键点:

  • 显式创建ObjectMapper并配置日期处理
  • 通过converters.add(0, ...)确保Jackson优先级最高
  • 保留其他转换器以满足不同需求

方案3:条件化配置

更优雅的方式是使用@Conditional

@Configuration @ConditionalOnClass(name = "com.fasterxml.jackson.databind.ObjectMapper") public class JacksonConfig { // Jackson特定配置 } @Configuration @ConditionalOnClass(name = "com.google.gson.Gson") public class GsonConfig { // Gson特定配置 }

防御性编程:避免再次踩坑

  1. 依赖管理

    • 使用dependencyManagement统一版本
    • 定期检查依赖树(mvn dependency:tree
  2. 配置检查

    @SpringBootTest public class ConverterTest { @Autowired private List<HttpMessageConverter<?>> converters; @Test void shouldContainJacksonConverter() { assertTrue(converters.stream() .anyMatch(c -> c instanceof MappingJackson2HttpMessageConverter)); } }
  3. 日志监控

    logging.level.org.springframework.web.servlet.mvc.method.annotation=DEBUG
  4. 文档记录

    • 在项目README中明确JSON库选择
    • 记录所有自定义的消息转换器配置

深入理解:Spring的消息转换机制

Spring MVC处理JSON响应的核心流程:

  1. 控制器方法返回对象
  2. DispatcherServlet查找匹配的HttpMessageConverter
  3. 按照canWrite()getSupportedMediaTypes()筛选
  4. Converter注册顺序选择第一个匹配的
  5. 调用write()方法生成响应体

优先级规则:

  • 手动添加的转换器优先于自动配置
  • 相同类型转换器按添加顺序决定
  • Spring Boot默认注册顺序:
    1. ByteArrayHttpMessageConverter
    2. StringHttpMessageConverter
    3. MappingJackson2HttpMessageConverter
    4. 其他...

替代方案:当必须使用Gson时

如果项目确实需要Gson作为主要JSON库:

@Configuration public class GsonConfig { @Bean public Gson gson() { return new GsonBuilder() .setDateFormat("yyyy-MM-dd HH:mm:ss") .create(); } @Bean public GsonHttpMessageConverter gsonConverter(Gson gson) { GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); converter.setGson(gson); return converter; } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter); converters.add(gsonConverter(gson())); } }

Gson的日期格式化方式:

// 实体类中不再使用@JsonFormat public class Order { private Date createTime; @JsonAdapter(DateTypeAdapter.class) public Date getCreateTime() { return createTime; } } public class DateTypeAdapter extends TypeAdapter<Date> { private final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Override public void write(JsonWriter out, Date value) throws IOException { out.value(value != null ? format.format(value) : null); } @Override public Date read(JsonReader in) throws IOException { try { return in.hasNext() ? format.parse(in.nextString()) : null; } catch (ParseException e) { throw new JsonParseException(e); } } }

性能考量:Jackson vs Gson

在决定使用哪个库时,可以参考以下对比:

特性JacksonGson
默认日期格式时间戳ISO8601
注解支持丰富(@JsonFormat等)有限(@SerializedName等)
性能更高稍低
配置灵活性高(Module系统)中等
Spring Boot整合默认支持需要显式配置
处理复杂对象更稳定可能循环引用问题

基准测试建议:

  • 使用JMH进行序列化/反序列化测试
  • 特别注意大对象和深度嵌套场景
  • 测试日期处理等特殊场景性能

最佳实践总结

  1. 单一JSON库原则

    • 新项目优先使用Jackson(Spring Boot默认)
    • 旧项目迁移时逐步替换,避免混用
  2. 显式配置优于隐式

    @Bean public ObjectMapper objectMapper() { return new ObjectMapper() .registerModule(new JavaTimeModule()) .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); }
  3. 测试覆盖关键场景

    @Test void shouldSerializeDateWithCorrectFormat() { Order order = new Order(); order.setCreateTime(new Date()); String json = objectMapper.writeValueAsString(order); assertTrue(json.contains("2023-08-01")); // 示例 }
  4. 监控与告警

    • 对API响应进行格式校验
    • 设置日期格式异常的监控规则
  5. 团队规范

    • 在项目文档中明确JSON处理规范
    • 代码审查时检查消息转换器配置

那次生产环境事故最终发现是因为某次紧急热修复时,有人添加了一个Swagger配置类,里面无意中移除了Jackson转换器。我们花了四小时回滚部署,但学到了宝贵的一课:在Spring Boot的生态中,看似简单的配置变动可能引发连锁反应。现在,我们团队所有涉及消息转换器的修改都需要经过双重审查,并且在CI流程中添加了Converter的自动化测试。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询