记一次 MyBatis 与 JdbcTemplate 日期类型混用导致的 Jackson 格式化“失效”问题背景最近在项目里遇到了一个让人有些头疼的问题同一个接口返回的 JSON 数据中日期字段的格式居然不一样。排查之后发现原因是项目中同时使用了 MyBatis 和 JdbcTemplate 两种方式查询数据库而两者返回的日期类型不一致加上 Jackson 在处理不同日期类型时的默认行为不同最终导致了格式化“部分生效”的现象。我把整个过程记录下来希望能帮助遇到同样问题的朋友。现象描述我们的项目在序列化对象时会统一配置 Jackson 的日期格式大致如下ObjectMapper mapper new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss));使用MyBatis的 Mapper 接口查询数据实体类中时间字段用的是java.util.Date。序列化后日期能够成功按照yyyy-MM-dd HH:mm:ss格式输出。使用JdbcTemplate的queryForList方法查询数据结果用MapString, Object接收。此时发现Map 中的日期字段输出格式变成了类似2025-12-17的纯日期字符串秒和时分部分完全不见了并没有按照我们设置的yyyy-MM-dd HH:mm:ss来展示。明明都是同一个ObjectMapper实例为什么只有java.util.Date类型被正常格式化了而 JdbcTemplate 返回的日期却“不听话”呢原因分析1. 两种 Date 类型本身就有差异首先需要明确java.util.Date和java.sql.Date虽然看着很像但用途和设计有本质区别。java.util.Date通用的日期时间类同时包含日期和时间信息精确到毫秒大多数业务实体都使用它。java.sql.Date继承自java.util.Date是 JDBC 专门用来映射数据库DATE类型的类。为了符合 SQL 标准的 DATE只包含年、月、日它的时间部分会被规整为零即 00:00:00并且其toString()方法也只输出yyyy-MM-dd格式。当使用JdbcTemplate.queryForList时JDBC 驱动会按照标准映射关系将数据库的DATE类型字段自动映射为java.sql.Date对象放入 Map 中。而 MyBatis 默认会将日期映射为java.util.Date我们也可以在实体类中显式使用java.util.Date这就导致两种方式产出的日期对象类型不完全相同。2. Jackson 对不同 Date 类型的处理并不一致Jackson 在序列化时对不同日期类型使用不同的内置序列化器。关键点在于对java.util.Date类型Jackson 使用DateSerializer。通过ObjectMapper.setDateFormat()设置的SimpleDateFormat会被该序列化器采纳所以我们可以很容易地实现全局格式化。对java.sql.Date类型Jackson 则使用专门的SqlDateSerializer。这个序列化器为了准确表达 SQL 日期的含义即只包含日期部分会直接忽略全局设置的DateFormat而是调用java.sql.Date的toString()方法或仅输出日期部分因此结果总是yyyy-MM-dd这样的格式。这就是为什么我们明明配置了全局日期格式java.util.Date可以生效而java.sql.Date却纹丝不动的原因——根本不是“失效”而是 Jackson 压根就没打算对java.sql.Date使用我们设置的SimpleDateFormat。解决方案自定义序列化器并全局注册既然默认的SqlDateSerializer不买账我们就只能自己动手通过自定义序列化器来接管java.sql.Date的格式化逻辑并将它全局注册到ObjectMapper中。这样无论java.sql.Date出现在实体类、Map 还是任何地方都能按照我们指定的格式输出。思路很直接创建一个继承JsonSerializerjava.sql.Date的序列化类在serialize方法中用SimpleDateFormat格式化日期然后通过SimpleModule注册。自定义序列化器代码如下importcom.fasterxml.jackson.core.JsonGenerator;importcom.fasterxml.jackson.databind.JsonSerializer;importcom.fasterxml.jackson.databind.SerializerProvider;importjava.io.IOException;importjava.sql.Date;importjava.text.SimpleDateFormat;publicclassCustomSqlDateSerializerextendsJsonSerializerDate{privatestaticfinalSimpleDateFormatFORMATnewSimpleDateFormat(yyyy-MM-dd HH:mm:ss);Overridepublicvoidserialize(Datevalue,JsonGeneratorgen,SerializerProviderserializers)throwsIOException{if(valuenull){gen.writeNull();}else{gen.writeString(FORMAT.format(value));}}}注册到ObjectMapper的方式也很简单importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.module.SimpleModule;importjava.sql.Date;ObjectMappermappernewObjectMapper();// 如果还有针对 java.util.Date 的全局格式可继续保留// mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss));SimpleModulemodulenewSimpleModule();module.addSerializer(Date.class,newCustomSqlDateSerializer());mapper.registerModule(module);这样一来所有java.sql.Date类型字段的序列化都会走我们自定义的逻辑输出yyyy-MM-dd HH:mm:ss格式的字符串与 MyBatis 实体中java.util.Date的表现达成一致。问题顺利解决。补充说明因为JdbcTemplate.queryForList返回的是Map我们无法像处理实体类那样在字段上加注解所以全局注册才是真正有效的方案。这也正是题目中所说的“只能自定义序列化类指定 java.sql.Date 类型字段的格式化方式才生效”的含义。总结这次问题的根因可以归纳为两点JDBC 规范导致类型差异JdbcTemplate 返回的Map中日期字段默认是java.sql.Date而 MyBatis 实体常使用java.util.Date。Jackson 对 SQL 日期类型的特殊照顾SqlDateSerializer有自己独立的格式输出逻辑无视全局DateFormat设置。通过自定义JsonSerializer并全局注册到ObjectMapper我们成功让java.sql.Date也听从了统一的日期格式配置。这件事也提醒我在混用多种数据访问方式时要格外注意框架的默认行为小小的类型差异就可能引发令人困惑的序列化问题。希望这篇记录对你有用。如果你也有类似的踩坑经历欢迎交流。