ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Fastjson2时间戳序列化实战:从ISO字符串到毫秒数字的性能优化

Fastjson2时间戳序列化实战:从ISO字符串到毫秒数字的性能优化 1. 从一次线上告警说起时间戳序列化的“坑”那天晚上手机突然弹出一条告警提示某个核心接口的响应时间飙升。排查日志发现一个奇怪的现象一个返回用户列表的接口响应体大小比平时大了近一倍。用工具抓包一看问题出在时间字段上。原本期望返回的是一个简洁的1689054661000这样的长整型时间戳但实际返回的却是2023-07-12T08:51:01.00000:00这种完整的 ISO 8601 字符串。对于包含上千条用户记录的列表每个对象多出几十个字符累积起来就是巨大的网络传输开销和客户端解析压力。我们当时用的正是 Fastjson2。这个库以其极致的性能著称但在默认配置下它对java.util.Date或java.time类型的序列化行为可能并不符合所有场景的预期。尤其是在前后端分离、移动端优先的架构下时间戳Timestamp因其紧凑、无歧义、易于跨平台处理的特性成为时间信息交换的首选格式。那么如何让 Fastjson2 乖乖地把时间序列化成时间戳而不是一长串日期字符串呢这不仅仅是改个配置那么简单背后涉及到序列化器的定制、全局与局部策略的权衡以及对 Fastjson2 新特性的深入理解。如果你也在为类似问题头疼或者想提前规避这个性能与协作的“坑”下面的内容就是为你准备的实战指南。2. 理解 Fastjson2 的默认时间序列化行为在动手配置之前我们必须先搞清楚 Fastjson2 默认是怎么处理时间对象的。知其然更要知其所以然这样才能在遇到复杂场景时游刃有余。2.1 默认行为基于JSONField注解与DateFormat的序列化Fastjson2 对时间的序列化策略有一个清晰的优先级链条。在没有进行任何全局配置的情况下它会按照以下顺序决定如何输出一个时间字段字段级别的JSONField注解这是最高优先级的配置。如果你在实体类的某个Date字段上使用了JSONField(format “yyyy-MM-dd HH:mm:ss”)那么无论全局配置如何这个字段都会按照指定的格式进行序列化。类级别的JSONType注解如果在类上使用了JSONType注解并指定了format那么该类中所有未单独配置JSONField的日期字段会继承这个格式。全局默认日期格式如果以上两级都没有配置Fastjson2 会查看全局默认的日期格式。在 Fastjson2 中默认的全局日期格式是null。这是一个关键点。内置默认行为当全局格式为null时Fastjson2 对java.util.Date和java.time.temporal.TemporalAccessor如LocalDateTime 的默认序列化行为是将其转换为ISO-8601 扩展格式的字符串。例如2023-07-12T08:51:01.00008:00。你可以通过一个简单的测试来验证import com.alibaba.fastjson2.JSON; import java.util.Date; public class DefaultBehaviorTest { public static void main(String[] args) { Date now new Date(); System.out.println(JSON.toJSONString(now)); // 输出: 2023-07-12T16:51:01.00008:00 } }这种 ISO 格式虽然标准、无歧义但正如开篇提到的它在网络传输和解析效率上不如纯数字的时间戳。特别是在高并发、大数据量的微服务间调用场景下这种差异会被放大。2.2 为什么需要时间戳场景驱动的格式选择选择时间戳而非日期字符串通常是基于以下几个实际工程考量传输效率一个long类型的时间戳如1689054661000在 JSON 中通常占用 13-16 个字符包括可能的引号。而一个完整的 ISO 日期字符串轻松超过 30 个字符。数据量越大节省的带宽和序列化/反序列化时间就越可观。解析便利性与一致性几乎所有编程语言和平台JavaScript, Python, Java, Go等都原生支持从时间戳毫秒或秒构建日期对象且行为一致。而解析形如“yyyy-MM-dd HH:mm:ss”的字符串则需要考虑时区、Locale 等问题容易出错。排序与比较时间戳本身是数字在数据库索引、内存排序、范围查询等操作上比字符串有天然的效率优势。前端友好性现代前端框架如 Vue、React和图表库如 ECharts在处理时间轴数据时往往更倾向于接收时间戳数组这能极大简化前端数据处理逻辑。因此将 Fastjson2 的默认时间输出改为时间戳是很多追求性能和简洁架构的团队的共同选择。3. 核心方案配置全局序列化器为时间戳要让 Fastjson2 将所有日期类型默认序列化为时间戳最直接有效的方法是配置一个全局的ObjectWriter。ObjectWriter是 Fastjson2 中负责将 Java 对象写入 JSON 的核心组件我们可以通过自定义它来改变特定类型的序列化行为。3.1 方案一使用JSON.config进行全局配置推荐这是 Fastjson2 推荐的方式简洁且功能强大。我们可以在应用启动时如 Spring Boot 的PostConstruct或配置类中进行一次性配置。import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONWriter; import com.alibaba.fastjson2.writer.ObjectWriter; import java.lang.reflect.Type; import java.util.Date; public class Fastjson2GlobalConfig { public static void init() { // 创建一个针对 Date 类型的自定义序列化器 ObjectWriterDate dateToTimestampWriter new ObjectWriterDate() { Override public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) { Date date (Date) object; if (date null) { jsonWriter.writeNull(); } else { // 核心将 Date 的 getTime() 值即毫秒时间戳直接写入 JSON jsonWriter.writeInt64(date.getTime()); } } }; // 将自定义序列化器注册为 Date 类型的全局默认序列化器 JSON.getDefaultObjectWriterProvider().register(Date.class, dateToTimestampWriter); // 如果你也使用了 java.time API比如 LocalDateTime也需要类似处理 // 这里以 LocalDateTime 为例将其转换为毫秒时间戳需先转为 Instant ObjectWriterLocalDateTime localDateTimeToTimestampWriter new ObjectWriterLocalDateTime() { Override public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) { LocalDateTime localDateTime (LocalDateTime) object; if (localDateTime null) { jsonWriter.writeNull(); } else { // 将 LocalDateTime 转换为 UTC 时间的毫秒时间戳 long epochMilli localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); jsonWriter.writeInt64(epochMilli); } } }; JSON.getDefaultObjectWriterProvider().register(LocalDateTime.class, localDateTimeToTimestampWriter); } }配置解析与注意事项writeInt64方法这是关键。它直接将long值写入 JSON 流输出的是一个不带引号的数字。如果你错误地使用了writeString(String.valueOf(date.getTime()))那么输出的将是带引号的字符串“1689054661000”虽然对人类阅读影响不大但严格来说它不再是 JSON Number 类型可能在某些严格的解析器中引发问题。时区处理对于java.util.Date它本质上存储的就是自1970-01-01T00:00:00Z(UTC) 以来的毫秒数所以getTime()直接就是 UTC 时间戳。对于LocalDateTime它是不带时区信息的“本地日期时间”必须通过atZone指定一个时区通常是系统默认时区或 UTC才能转换为Instant并获取时间戳。这里是一个潜在的坑如果你的系统跨时区部署必须统一时区标准通常是 UTC否则序列化出来的时间戳会因服务器时区不同而不同。注册时机这个配置需要在任何 JSON 序列化操作发生之前执行。在 Spring Boot 中可以放在一个带有Configuration注解的类的PostConstruct方法里或者使用ApplicationRunner/CommandLineRunner。提示在实际项目中你可能会遇到多种时间类型Date,LocalDateTime,LocalDate,Instant等。建议为它们分别创建并注册对应的ObjectWriter以确保全局行为一致。可以封装一个工具类来完成所有这些类型的注册。3.2 方案二使用JSONWriter.Feature进行特性配置Fastjson2 提供了一些内置的Feature特性来影响序列化行为。虽然目前2.0.64版本没有直接提供WriteDateAsTimestamp这样的特性但我们可以组合使用其他特性来接近目标不过这种方法有局限性。// 这种方法无法直接将 Date 序列化为纯数字时间戳。 // 以下配置尝试使用 ISO8601 优化格式但结果仍是字符串。 JSONWriter.Context context new JSONWriter.Context(JSONWriter.Feature.WriteDateUseDateFormat); context.setDateFormat(“yyyy-MM-dd HH:mm:ss”); // 无效因为最终仍是字符串格式 // 更接近目标的一种尝试使用 WriteClassName 特性不这完全偏离了方向。结论对于“序列化为时间戳”这个明确需求使用自定义ObjectWriter是唯一可靠且直接的全局解决方案。内置的Feature主要服务于日期格式字符串的变换。3.3 方案三局部注解覆盖全局配置即使配置了全局时间戳序列化你仍然可以在特定的字段上使用JSONField注解来覆盖全局行为以满足特殊需求。这是 Fastjson2 灵活性的一种体现。import com.alibaba.fastjson2.annotation.JSONField; import java.util.Date; import java.time.LocalDateTime; public class UserDTO { private Long id; private String name; // 这个字段遵循全局配置将被序列化为时间戳 (如 1689054661000) private Date createTime; // 使用注解覆盖全局配置该字段将被序列化为指定格式的字符串 JSONField(format “yyyy-MM-dd”) private Date birthday; // 对于 LocalDateTime 同样适用 JSONField(format “yyyy/MM/dd HH:mm:ss”) private LocalDateTime updateTime; // 甚至可以直接序列化为秒级时间戳注意单位是秒 JSONField(format “unix”) private Date loginTime; // 输出如 1689054661 // getters and setters... }JSONField(format “unix”)的妙用这个格式值非常实用它告诉 Fastjson2 将日期序列化为自 1970-01-01 00:00:00 UTC 以来的秒数Unix 时间戳。这在一些 API 设计如某些社交媒体 API中很常见。需要注意的是unix格式输出的是秒而我们的自定义ObjectWriter通常输出的是毫秒。务必根据你的接口契约谨慎选择。4. 实战中的进阶问题与解决方案配置好全局序列化器只是第一步。在真实的复杂项目中你可能会遇到一些边界情况和进阶问题。4.1 处理null日期值与空集合当日期字段为null时我们的自定义ObjectWriter已经通过判断处理了。但有时我们还需要控制整个对象或集合的序列化行为。全局忽略null值可以通过JSONWriter.Feature.WriteMapNullValue来控制。默认情况下Fastjson2不序列化值为null的字段。如果你需要包含null的日期字段值为null需要在序列化时传入这个特性但通常不推荐因为这会增大数据体积。String json JSON.toJSONString(obj, JSONWriter.Feature.WriteMapNullValue);处理包含日期的空集合这不是问题。集合为空时序列化为[]自定义的ObjectWriter不会被执行。4.2 与 Spring Boot 的HttpMessageConverter集成在 Spring Boot Web 应用中我们通常使用HttpMessageConverter来转换 HTTP 请求和响应的 JSON 数据。为了让 Spring MVC 使用我们配置好的 Fastjson2 实例需要替换默认的 Jackson 转换器。添加 Fastjson2 Spring Boot Starter 依赖如果可用或直接引入核心库。创建一个配置类来配置HttpMessageConverterimport com.alibaba.fastjson2.JSON; 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.Collections; import java.util.List; Configuration public class Fastjson2WebConfig implements WebMvcConfigurer { Override public void configureMessageConverters(ListHttpMessageConverter? converters) { // 1. 调用我们之前写的全局配置初始化方法 Fastjson2GlobalConfig.init(); // 2. 创建 FastJsonHttpMessageConverter FastJsonHttpMessageConverter converter new FastJsonHttpMessageConverter(); FastJsonConfig config new FastJsonConfig(); // 3. 设置 FastJsonConfig这里可以配置序列化特性、日期格式等 // 注意由于我们已经通过 JSON.config() 全局注册了自定义的 ObjectWriter // 所以 FastJsonConfig 中的日期格式设置可能不会生效因为 ObjectWriter 优先级更高。 // config.setDateFormat(“yyyy-MM-dd HH:mm:ss”); // 可能被覆盖 // 设置字符集和支持的 MediaType config.setCharset(StandardCharsets.UTF_8); converter.setFastJsonConfig(config); converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON)); converter.setDefaultCharset(StandardCharsets.UTF_8); // 4. 将 Fastjson2 的转换器添加到 converters 列表的最前面优先使用 converters.add(0, converter); } }关键点确保Fastjson2GlobalConfig.init()在 Converter 被使用之前执行。这样所有通过 Spring MVCResponseBody或RestController返回的对象其日期字段都会按照我们的全局配置序列化为时间戳。4.3 反序列化如何将时间戳读回 Date 对象序列化是“出去”反序列化是“回来”。当我们把时间戳传给后端后端需要将其解析回Date对象。Fastjson2 在反序列化时对数字类型的时间戳有很好的原生支持。import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONReader; import java.util.Date; public class DeserializeTest { public static void main(String[] args) { // 场景1JSON 中的时间戳是数字 String jsonWithNumber “{\”createTime\”: 1689054661000}”; User user1 JSON.parseObject(jsonWithNumber, User.class); System.out.println(user1.getCreateTime()); // 正确输出 Date 对象 // 场景2JSON 中的时间戳是数字字符串带引号 String jsonWithStringNumber “{\”createTime\”: \”1689054661000\”}”; // 默认情况下Fastjson2 也能处理这种格式因为它会尝试将字符串转换为 long User user2 JSON.parseObject(jsonWithStringNumber, User.class); System.out.println(user2.getCreateTime()); // 同样能正确输出 Date 对象 // 场景3使用 ISO 日期字符串反序列化 String jsonWithISOString “{\”createTime\”: \”2023-07-12T08:51:01Z\”}”; User user3 JSON.parseObject(jsonWithISOString, User.class); System.out.println(user3.getCreateTime()); // 也能正确解析 } } class User { private Date createTime; // getter and setter }反序列化的智能之处Fastjson2 的JSONReader在解析日期字段时非常灵活。它会依次尝试如果是数字long或int直接将其作为毫秒时间戳构造Date。如果是字符串先尝试解析为数字处理场景2如果失败再尝试用 ISO 8601 等格式解析为日期处理场景3。这意味着即使我们全局将日期序列化为时间戳也完全不影响后端接收前端传来的各种格式的时间数据兼容性很好。当然为了规范建议前后端约定统一使用数字时间戳。4.4 性能考量与线程安全性能自定义ObjectWriter的write方法实现非常简洁直接调用getTime()和writeInt64其性能与 Fastjson2 内置的默认序列化器处于同一量级几乎没有额外开销。选择时间戳本身就是为了提升性能。线程安全通过JSON.getDefaultObjectWriterProvider().register(...)注册的ObjectWriter是全局的。Fastjson2 的ObjectWriterProvider内部使用ConcurrentHashMap来存储这些Writer因此注册操作和查找操作都是线程安全的。自定义的ObjectWriter实现也应该是无状态的就像我们上面写的那样以确保线程安全。5. 避坑指南从 Fastjson1 升级到 Fastjson2 的时间序列化问题如果你的项目正在从 Fastjson1 升级到 Fastjson2在时间序列化方面可能会遇到一些行为不一致的地方需要特别注意。5.1 默认行为的差异Fastjson1在未配置SerializerFeature.WriteDateUseDateFormat且未设置全局日期格式的情况下默认会将Date序列化为毫秒时间戳数字。这与 Fastjson2 的默认 ISO 字符串行为截然不同。Fastjson2默认序列化为 ISO 8601 字符串。升级影响直接替换依赖后所有没有显式配置日期格式的接口其时间字段的输出会从数字突然变成字符串很可能导致前端解析失败。解决方案升级后必须按照本文第3节的方法显式配置全局的时间戳序列化器以保持与 Fastjson1 默认行为的兼容或者推动前端适配新的格式。5.2 API 与配置方式的变化Fastjson1 中常用的JSON.toJSONStringWithDateFormat()、SerializerFeature.WriteDateUseDateFormat等 API 在 Fastjson2 中已不存在或行为改变。Fastjson2 更强调通过ObjectWriter/ObjectReader、JSONWriter.Feature和注解来进行配置。迁移步骤移除所有SerializerFeature相关代码。使用JSONWriter.Feature替换功能类似的特性但注意没有直接对应WriteDateAsTimestamp的。对于日期格式控制优先使用JSONField(format“...”)注解。对于全局默认行为必须使用JSON.config()配合自定义ObjectWriter来实现。5.3 常见错误“属性丢失”或“冒号缺失”在热搜词中看到 “java实体序列化json字符串 冒号缺失” 和 “fastjson2 json.praseobject 属性丢失”。这些问题虽然不直接是时间序列化导致的但在升级过程中可能因为其他原因出现。属性丢失检查字段的 Getter 方法是否符合 Java Bean 规范getXxx,isXxx。Fastjson2 默认使用getter进行序列化。或者使用JSONField注解显式指定字段名。确保升级后没有因为字段名大小写等问题导致序列化策略变化。冒号缺失这通常是序列化结果字符串格式错误极有可能是自定义的ObjectWriter实现有误没有正确调用JSONWriter的方法或者直接操作了底层字符串导致 JSON 格式破坏。务必使用jsonWriter.writeInt64()、jsonWriter.writeString()等标准方法写入值。6. 总结与最佳实践建议经过以上分析我们可以总结出在 Fastjson2 中优雅处理时间戳序列化的最佳路径明确需求统一约定在项目伊始团队内部应约定时间字段的传输格式。毫秒级时间戳因其通用性和高效性在绝大多数场景下是最佳选择。全局配置一劳永逸在应用启动入口使用JSON.config()注册自定义的ObjectWriter将java.util.Date、java.time.LocalDateTime等常用时间类型全局序列化为时间戳。这是最彻底、影响范围最广的方式。善用注解灵活覆盖对于少数需要特殊格式的字段如只显示生日的年月日使用JSONField(format “...” )进行精细化的局部控制。注解的优先级高于全局配置。妥善处理时区在自定义ObjectWriter中特别是处理LocalDateTime时明确指定时区建议统一使用 UTC。在反序列化时确保系统时区设置正确或使用JSONField注解的timezone属性。Spring Boot 集成通过自定义WebMvcConfigurer配置FastJsonHttpMessageConverter并确保在配置 Converter 之前完成 Fastjson2 的全局配置初始化。升级兼容性从 Fastjson1 升级时将时间戳序列化配置作为必须的迁移步骤并进行充分的接口测试避免对前端造成破坏性变更。最后记住一点序列化配置是系统与外界通信的“协议”。保持协议的清晰、一致和高效是构建稳定、可维护系统的重要一环。通过合理的 Fastjson2 配置让时间数据以最简洁、最有力的方式——时间戳在你的系统中流动。
返回列表