1. 为什么我们需要全局配置从踩坑说起我记得刚接触SpringBoot那会儿每次写接口返回数据最头疼的就是时间格式和长整型数字。后端明明定义的是LocalDateTime数据库里存得整整齐齐一到前端页面要么变成了一串看不懂的数组[2024, 12, 31, 23, 59, 59]要么就是带了个烦人的T字母的字符串2024-12-31T23:59:59。产品经理跑过来说“这个时间显示不对啊用户看不懂。” 我只能赔着笑回头在几十个VO视图对象类的字段上一个一个地加上JsonFormat(pattern yyyy-MM-dd HH:mm:ss)注解。这还不算完更坑的是Long类型。当ID或者一些大数字比如雪花算法生成的ID超过JavaScript的Number类型安全范围2^53-1时前端拿到手精度就丢失了1294839203849201234可能就变成了1294839203849201200导致后续的逻辑比对全部出错。那时候的解决方案也是土办法在每一个Long字段上加JsonSerialize(using ToStringSerializer.class)把数字序列化成字符串传给前端。一个中等规模的项目成百上千个实体类和VO光加这些注解就能加到手软而且后期维护起来简直是噩梦改个格式得全局搜索替换还容易漏。这种“打补丁”式的做法效率低下只是其一关键是破坏了代码的整洁性和可维护性。后来我意识到这其实是一个序列化策略的问题。SpringBoot默认使用的Jackson库非常强大它提供了全局配置的能力让我们可以“一劳永逸”地解决这类问题。我们需要的不是给每个字段“治病”而是给整个系统的数据输出“定规矩”。这就是全局配置方案的价值所在一次配置处处生效让我们的代码更干净让我们的开发更高效。2. 理解核心Jackson的序列化与Java 8时间API要搞定全局配置我们得先摸清楚Jackson这个“工具人”的脾气特别是它怎么对待Java 8引入的新时间API。Jackson的默认行为在没有额外配置的情况下Jackson序列化java.util.Date和java.time包下的类如LocalDateTime、LocalDate时行为是不一样的。对于老式的Date你可以通过在application.yml里配置spring.jackson.date-format来指定全局格式。但是这个配置对LocalDateTime和LocalDate是完全无效的。这是因为Java 8时间API是一套全新的体系Jackson需要专门的模块jackson-datatype-jsr310来支持它。为什么Long会精度丢失这其实不是后端的问题而是前后端数据类型的差异。JavaScript的Number类型是双精度浮点数它能安全表示的整数范围是有限的。当Java中的Long值超过这个范围时在JSON序列化成数字的过程中信息就会丢失。解决思路很简单不把它序列化成数字而是序列化成字符串。字符串可以无损地传递任意长度的数字。所以我们的全局配置方案本质上要做两件事为LocalDateTime等类型注册专门的序列化/反序列化器并指定我们想要的格式如yyyy-MM-dd HH:mm:ss。为Long类型注册一个ToStringSerializer让它总是以字符串形式输出。理解了这些我们再来看具体的实现方法就不会觉得是在死记硬背配置了。3. 实战三种全局配置方案详解市面上常见的全局配置方法有好几种我挨个试过各有各的适用场景和坑。下面我结合自己的经验给你详细拆解。3.1 方案一使用application.yml配置仅对Date有效了解即可这个方案最简单但局限性也最大我把它放在这里主要是为了让你明白区别。spring: jackson: date-format: yyyy-MM-dd HH:mm:ss # 指定日期格式 time-zone: GMT8 # 设置时区 serialization: write-dates-as-timestamps: false # 禁止将日期写成时间戳效果与局限有效对象仅对java.util.Date和java.sql.Date/Timestamp生效。无效对象对LocalDateTime、LocalDate、ZonedDateTime等java.time包下的类无效。适用场景如果你的项目还在大量使用老旧的java.util.Date这个配置可以快速统一格式。但对于新项目我强烈建议直接使用Java 8时间API所以这个方案你了解一下就行我们重点看下面的。3.2 方案二使用Jackson2ObjectMapperBuilderCustomizer推荐用于简单场景这是Spring Boot提供的一个非常优雅的定制化接口。它的原理是让你在应用启动时对Jackson的ObjectMapper建造器进行定制。具体配置代码import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; Configuration public class JacksonConfig { private static final String DATE_TIME_PATTERN yyyy-MM-dd HH:mm:ss; private static final String DATE_PATTERN yyyy-MM-dd; Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - { // 1. 注册JavaTimeModule这是支持Java 8时间API的关键 builder.modulesToInstall(new JavaTimeModule()); // 2. 配置LocalDateTime和LocalDate的序列化与反序列化格式 builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))); builder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))); builder.deserializers(new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_PATTERN))); // 3. 配置Long类型序列化为字符串 builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); // 处理基本类型long // 4. 可选全局关闭将日期写为时间戳 builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); }; } }方案优点配置集中代码简洁所有Jackson的定制化逻辑都在一个Bean中完成。非侵入性不需要修改任何现有的WebMvcConfigurer配置。Spring Boot风格充分利用了Spring Boot的自动配置特性是官方推荐的方式之一。需要注意的坑这个方案在绝大多数情况下工作良好。但是如果你的项目中已经通过EnableWebMvc注解或者自定义的WebMvcConfigurer完全接管了Spring MVC的配置那么Spring Boot关于Jackson的某些自动配置可能会失效。此时这个定制器可能不生效。我就在一个老项目迁移时踩过这个坑下面就要说到第三种更“强硬”的方案。3.3 方案三在WebMvcConfigurer中直接配置最彻底兼容性最强当你发现方案二不生效或者你的项目结构已经比较复杂有自定义的MVC配置时直接在WebMvcConfigurer中配置HttpMessageConverter是最直接、最可靠的方法。这相当于你手动打造了一个符合你要求的JSON消息转换器并把它注册到Spring MVC的转换器列表里。完整配置类示例import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; Configuration public class GlobalJsonConfig implements WebMvcConfigurer { private static final String DATE_TIME_PATTERN yyyy-MM-dd HH:mm:ss; private static final String DATE_PATTERN yyyy-MM-dd; Override public void configureMessageConverters(ListHttpMessageConverter? converters) { // 使用Jackson2ObjectMapperBuilder来构建我们定制化的ObjectMapper Jackson2ObjectMapperBuilder builder new Jackson2ObjectMapperBuilder(); // 关键步骤1注册JavaTimeModule builder.modules(new JavaTimeModule()); // 关键步骤2为LocalDateTime和LocalDate指定序列化/反序列化器及格式 builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))); builder.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN))); builder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_PATTERN))); builder.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_PATTERN))); // 关键步骤3创建我们定制好的ObjectMapper并用它构建一个消息转换器 ObjectMapper objectMapper builder.build(); // 关键步骤4处理Long类型-字符串 SimpleModule longModule new SimpleModule(); longModule.addSerializer(Long.class, ToStringSerializer.instance); longModule.addSerializer(Long.TYPE, ToStringSerializer.instance); objectMapper.registerModule(longModule); // 关键步骤5创建转换器并添加到列表最前面确保优先级 MappingJackson2HttpMessageConverter converter new MappingJackson2HttpMessageConverter(objectMapper); // 这里选择清空列表只使用我们自定义的转换器避免冲突。也可以使用converters.add(0, converter)插入到头部。 converters.clear(); converters.add(converter); } }方案优点绝对控制权你完全掌控了ObjectMapper的创建和配置过程不受其他自动配置干扰。兼容性最佳无论你的项目MVC配置多么复杂这种方式都能确保生效。优先级高通过configureMessageConverters方法你可以控制转换器的顺序。一个重要的补充处理Feign调用时的序列化问题这里有一个我踩过的大坑必须单独拿出来说。上面的全局配置管的是HTTP接口返回给前端浏览器时的序列化。但是如果你的服务之间通过Spring Cloud Feign进行调用Feign客户端在反序列化服务提供者Provider返回的JSON时用的是另一套ObjectMapper。现象服务A调用服务B服务B返回的VO里有个LocalDateTime createTime字段。虽然服务B的全局配置让它在返回给前端时格式是yyyy-MM-dd HH:mm:ss但服务A通过Feign拿到手的JSON字符串如果这个字段没有注解Feign默认可能无法正确反序列化比如还是尝试按数组或带T的格式去解析就会报feign.codec.DecodeException。解决方案对于需要被其他服务通过Feign调用的接口其返回的VO类中的LocalDateTime/LocalDate字段仍然需要加上JsonFormat注解。这是因为Feign的反序列化过程不一定会走你为Web MVC配置的那套逻辑。public class SomeFeignResponseVO { private Long id; private String name; JsonFormat(pattern yyyy-MM-dd HH:mm:ss) // Feign调用时需要这个注解 private LocalDateTime createTime; // getters and setters }这看起来像是走回了老路但其实是针对不同通信协议HTTP API vs. RPC的精细化控制。对于纯粹面向前端页面的接口全局配置足以对于同时作为Feign Provider的接口在VO上加点注解是更稳妥的做法。4. 避坑指南与高级技巧配置写好了不代表就万事大吉。在实际开发中还有一些细节需要注意。坑点一ApiParam注解与RequestParam的玄学问题有朋友发现在Controller的方法参数上如果用了ApiParamSwagger注解来标注一个LocalDateTime类型的请求参数即使配置了全局反序列化前端传yyyy-MM-dd HH:mm:ss格式的字符串过来后端也无法正确接收会报400错误。但如果换成RequestParam就能正常解析。// 可能失效的写法 public void query(ApiParam(开始时间) RequestParam LocalDateTime startTime) { // ... } // 稳定工作的写法 public void query(RequestParam ApiParam(开始时间) LocalDateTime startTime) { // ... }其实这不是全局配置的bug而是注解扫描和处理的顺序问题。ApiParam主要是给Swagger生成文档用的某些情况下它可能会干扰Spring MVC对参数的实际解析。最安全的做法是对于需要接收时间参数的RequestParam始终把RequestParam放在前面。或者更推荐的做法是对于复杂的时间范围查询使用一个专门的DTO对象来接收参数DTO里的字段享受全局反序列化配置。坑点二时区问题我们在配置中设置了time-zone: GMT8这确保了时间在序列化和反序列化时都按照东八区来处理。但要注意如果你的数据库存储的是UTC时间或者你的服务器部署在海外这个配置需要相应调整。LocalDateTime本身是不带时区信息的“本地日期时间”时区配置会影响它如何与带时区信息的时间点如Instant进行转换。在跨时区项目中这是一个需要仔细设计的问题。高级技巧更灵活的格式控制上面的例子我们把格式写死了。但在实际项目中不同的场景可能需要不同的格式。比如管理后台需要精确到秒而移动端列表可能只需要显示日期。怎么办你可以创建多个配置类或者通过条件注解ConditionalOnProperty来根据配置动态选择格式。更高级的做法是自定义一个注解比如JsonDateFormat然后在自定义的序列化器中读取这个注解的pattern属性实现按字段级别的格式控制。这样全局配置提供一个默认格式特殊字段可以通过注解覆盖。这需要更深入地定制Jackson但对于大型复杂项目来说这种灵活性是值得的。5. 效果验证与测试配置完成后我们怎么验证它生效了呢光看代码不行得实际跑起来看看。1. 编写测试ControllerRestController RequestMapping(/test) public class TestController { GetMapping(/datetime) public MapString, Object testDateTime() { MapString, Object map new HashMap(); map.put(localDateTime, LocalDateTime.now()); map.put(localDate, LocalDate.now()); map.put(longId, 1294839203849201234L); // 一个超过JS安全范围的Long值 map.put(regularNumber, 100L); // 一个普通范围内的Long值 return map; } }2. 启动应用访问接口如http://localhost:8080/test/datetime在没有全局配置时你可能会得到这样的响应{ localDateTime: [2024, 7, 15, 14, 30, 25, 123456789], localDate: [2024, 7, 15], longId: 1294839203849201200, regularNumber: 100 }或者{ localDateTime: 2024-07-15T14:30:25.123456789, localDate: 2024-07-15, longId: 1294839203849201200, regularNumber: 100 }在正确配置全局方案后响应应该变成{ localDateTime: 2024-07-15 14:30:25, localDate: 2024-07-15, longId: 1294839203849201234, regularNumber: 100 }看到变化了吗LocalDateTime和LocalDate变成了我们熟悉的yyyy-MM-dd HH:mm:ss和yyyy-MM-dd格式去掉了T。所有的Long类型无论大小都变成了字符串前端JavaScript用BigInt或者直接当字符串处理就再也不会精度丢失了。3. 测试反序列化接收参数写一个POST接口接收包含时间字段的JSON。PostMapping(/receive) public String receiveData(RequestBody TestDTO dto) { log.info(接收到时间{}, dto.getSomeTime()); return success; } // TestDTO public class TestDTO { private LocalDateTime someTime; private Long bigId; // getters and setters }使用Postman或curl发送请求Body为{ someTime: 2024-07-15 14:30:25, bigId: 1294839203849201234 }如果后台能正确打印出LocalDateTime对象和Long值说明全局反序列化配置也生效了。走完这个验证流程你的全局序列化配置才算真正落地。这套组合拳打下来以后在项目里再遇到时间格式和Long精度问题你就可以淡定地说“没事全局配置已经处理好了。” 这感觉就像给项目穿上了一件得体的外衣里子面子都照顾到了。