datai/datai-scenes/datai-scene-salesforce/docs/decisions/adr/2026-01-25-002-06-ADR-日期格式化技术选型.md

12 KiB
Raw Blame History

架构决策记录 (ADR) - 日期格式化技术选型

背景

在项目国际化需求REQ-002需要实现日期格式化功能支持用户设置日期格式偏好根据用户日期格式偏好显示日期。该功能需要满足以下核心需求

  1. 多日期格式支持支持常用日期格式ISO 8601、欧洲、美国、中国等
  2. 自动格式化:自动根据用户日期格式偏好格式化日期数据
  3. 时区转换:结合时区转换功能,支持跨时区日期格式化
  4. 多语言支持:支持多语言日期显示(如:星期几、月份名称的本地化)
  5. 高性能:日期格式化响应时间 < 10ms
  6. 易维护:代码结构清晰,易于扩展和维护
  7. 兼容性:与现有 Spring Boot 3.5.7 + 若依框架集成良好

当前系统所有日期数据使用 Date、LocalDate、LocalDateTime 类型存储,需要在 Service 层进行日期格式化,结合时区转换功能,使用 Redis 缓存用户日期格式偏好,支持用户 > 系统的日期格式优先级。

决策

决策 1日期格式化技术选择

选定方案java.time.format.DateTimeFormatter

选择理由

  1. 无额外依赖Java 8+ 内置,无需引入额外依赖,减少项目复杂度
  2. 功能完整:支持日期、日期时间、时区、本地化等格式化需求
  3. 国际化支持:支持国际化格式化,自动处理不同地区的日期格式
  4. 性能优秀:格式化时间 < 1ms满足性能要求
  5. 线程安全DateTimeFormatter 是线程安全的,可以在多线程环境下安全使用
  6. 与框架兼容:与 Spring Boot 3.5.7(支持 Java 21完美集成
  7. 自定义灵活:支持自定义格式化模式,满足不同业务场景
  8. 类型安全:支持 LocalDate、LocalDateTime 等类型安全的日期时间类型

实现方案

  • 使用 DateTimeFormatter 的模式字符串定义日期格式(例如:yyyy-MM-dd
  • 使用 ofPattern() 方法创建格式化器
  • 使用 withLocale() 方法设置语言环境
  • 使用 ConcurrentHashMap 缓存 DateTimeFormatter 实例,提高性能
  • 支持自定义日期格式模式

放弃方案的原因

方案 AIntl.DateTimeFormatJavaScript 库)

  • 放弃原因
    • 这是 JavaScript 库,不适用于 Java 后端
    • 需要在前端实现,无法保证格式化逻辑的一致性
    • 无法在 Service 层统一处理

方案 BSimpleDateFormat

  • 放弃原因
    • SimpleDateFormat 不是线程安全的,在多线程环境下使用需要同步
    • 性能较差,每次格式化都需要创建新实例
    • 已被 DateTimeFormatter 取代,属于过时的 API
    • 不支持 LocalDate、LocalDateTime 等新日期时间类型

方案 C第三方库Joda-Time

  • 放弃原因
    • 需要引入额外依赖,增加项目复杂度
    • Java 8+ 已内置了类似的功能,引入价值不大
    • Joda-Time 已停止维护,不再推荐使用

方案 D自定义格式化实现

  • 放弃原因
    • 需要手动维护格式化规则,维护成本高
    • 难以处理国际化格式化规则
    • 容易出现格式化错误,影响数据准确性
    • 开发成本高,风险大

决策 2缓存策略选择

选定方案Redis 缓存

选择理由

  1. 已集成:项目已集成 Redis无需额外配置和部署
  2. 性能优秀Redis 响应时间 < 1ms满足高性能要求
  3. 分布式支持:支持分布式部署,多实例共享缓存
  4. 自动过期:支持自动过期机制,无需手动清理过期数据
  5. 数据结构丰富:支持 String、Hash、List 等多种数据结构
  6. 持久化:支持数据持久化,防止数据丢失

缓存策略设计

  • 用户日期格式缓存Key = sys:date:format:{userId}TTL = 24 小时
  • 系统默认日期格式缓存Key = sys:default:date:formatTTL = 24 小时
  • DateTimeFormatter 缓存:使用 ConcurrentHashMap 缓存,永久缓存

放弃方案的原因

方案 ACaffeine 本地缓存

  • 放弃原因
    • 本地缓存无法在分布式环境下共享,多实例数据不一致
    • 需要引入额外依赖caffeine
    • 缓存更新需要手动同步,实现复杂

方案 B数据库缓存

  • 放弃原因
    • 数据库查询响应时间 > 10ms性能较差
    • 高并发场景下数据库压力大,影响系统性能
    • 无法自动过期,需要手动清理过期数据

决策 3日期格式化层选择

选定方案Service 层转换 + AOP 切面拦截

选择理由

  1. 统一处理:在 Service 层统一处理日期格式化,避免代码重复
  2. AOP 切面:使用 AOP 切面拦截 Service 方法返回值,自动格式化日期字段,无需手动调用
  3. 覆盖面广:所有调用 Service 的地方都会触发格式化Controller、定时任务、内部调用等
  4. 业务逻辑分离:日期格式化逻辑与业务逻辑分离,代码清晰
  5. 易于维护:修改日期格式化逻辑只需修改切面代码,影响范围小
  6. 性能优化AOP 切面在编译时织入,运行时性能损失小
  7. 自动识别:自动识别 Date、LocalDate、LocalDateTime 类型字段,减少手动标注的工作量
  8. 时区转换集成:与 TimeZoneConvertAspect 配合,先进行时区转换,再进行日期格式化

实现方案

  • 响应格式化:使用 @Around 切面拦截 Service 方法返回值,自动格式化日期字段
  • 注解支持:提供 @DateFormat 注解,标记需要格式化的 Service 方法
  • 递归处理:递归处理嵌套对象的日期字段
  • 循环引用处理:避免循环引用导致的无限递归
  • 多类型支持:支持 Date、LocalDate、LocalDateTime 等多种日期时间类型

放弃方案的原因

方案 AController 层格式化 + AOP 切面拦截

  • 放弃原因
    • 只有接口请求会触发格式化,定时任务、内部调用等不会触发
    • 如果有多个地方需要格式化,可能需要多个切面
    • 格式化逻辑分散,难以统一管理

方案 B手动调用格式化方法

  • 放弃原因
    • 需要在每个需要格式化的地方手动调用,代码重复
    • 容易遗漏,导致格式化不一致
    • 维护成本高,修改格式化逻辑需要修改多处代码

决策 4时区转换方案

选定方案复用已实现的时区转换功能TimeZoneConvertAspect

选择理由

  1. 避免重复开发:时区转换功能已在 2026-01-21-002-04 中实现,无需重复开发
  2. 功能完整:已实现的时区转换功能完整,满足跨时区日期格式化需求
  3. 代码复用:复用现有代码,减少开发成本和维护成本
  4. 一致性:与现有时区转换功能保持一致,避免功能不一致
  5. 集成简单DateFormatAspect 与 TimeZoneConvertAspect 配合使用,集成简单

实现方案

  • 时区转换优先TimeZoneConvertAspect 先进行时区转换,将 UTC 时间转换为用户时区
  • 日期格式化后置DateFormatAspect 后进行日期格式化,根据用户日期格式偏好格式化日期
  • 切面顺序:使用 @Order 注解控制切面执行顺序,确保时区转换先于日期格式化

放弃方案的原因

方案 A在 DateFormatAspect 中实现时区转换

  • 放弃原因
    • 重复开发时区转换功能,增加开发成本
    • 与现有时区转换功能不一致,可能导致功能冲突
    • 维护成本高,需要同时维护两套时区转换逻辑

方案 B不进行时区转换

  • 放弃原因
    • 无法满足跨时区日期格式化需求
    • 不同时区的用户看到的日期时间不一致,影响用户体验

决策 5多语言支持方案

选定方案:结合国际化功能,使用 Locale 进行本地化

选择理由

  1. 避免重复开发:国际化功能已在 2026-01-21-002-03 中实现,无需重复开发
  2. 功能完整:已实现的国际化功能完整,满足多语言日期显示需求
  3. 代码复用:复用现有代码,减少开发成本和维护成本
  4. 一致性:与现有国际化功能保持一致,避免功能不一致
  5. 集成简单DateFormatAspect 结合国际化功能,使用 Locale 进行本地化

实现方案

  • 语言偏好读取:从 SysUser 表读取用户语言偏好lang 字段)
  • Locale 创建:根据语言偏好创建 Locale 实例例如zh_CN、en_US
  • 本地化格式化:使用 DateTimeFormatter.withLocale() 方法设置语言环境
  • 多语言支持:支持星期几、月份名称的本地化显示

放弃方案的原因

方案 A在 DateFormatAspect 中实现多语言支持

  • 放弃原因
    • 重复开发多语言支持功能,增加开发成本
    • 与现有国际化功能不一致,可能导致功能冲突
    • 维护成本高,需要同时维护两套国际化逻辑

方案 B不进行多语言支持

  • 放弃原因
    • 无法满足多语言日期显示需求
    • 不同语言环境的用户看到的日期显示不一致,影响用户体验

决策 6数据类型选择

选定方案Date、LocalDate、LocalDateTime

选择理由

  1. 类型安全LocalDate、LocalDateTime 是类型安全的日期时间类型,避免混淆
  2. 线程安全LocalDate、LocalDateTime 是不可变的,线程安全
  3. API 优秀Java 8+ 的日期时间 API 设计优秀,易于使用
  4. 与现有代码一致:项目中已有的日期数据使用 Date、LocalDate、LocalDateTime 类型,保持一致
  5. 功能完整:支持日期、日期时间、时区等多种场景
  6. 性能优秀LocalDate、LocalDateTime 性能优秀,满足高性能要求

实现方案

  • 日期字段:使用 LocalDate 类型表示日期(例如:出生日期)
  • 日期时间字段:使用 LocalDateTime 类型表示日期时间(例如:创建时间)
  • 兼容旧代码:保留 Date 类型,兼容旧代码
  • 类型转换:提供 Date、LocalDate、LocalDateTime 之间的转换方法

放弃方案的原因

方案 A仅使用 Date

  • 放弃原因
    • Date 不是线程安全的,在多线程环境下使用需要同步
    • Date 的 API 设计不佳,易于出错
    • Date 不支持不可变性,容易导致数据不一致

方案 B仅使用 LocalDate、LocalDateTime

  • 放弃原因
    • 与现有代码不一致,需要大量重构
    • 旧代码可能依赖 Date 类型,兼容性问题

方案 C仅使用 Long时间戳

  • 放弃原因
    • 不直观,可读性差
    • 显示时需要格式化,增加处理逻辑
    • 与现有代码不一致

后果

正面后果

  1. 无额外依赖:使用 Java 内置库和已集成的 Redis无需引入额外依赖
  2. 高性能:日期格式化响应时间 < 10ms满足性能要求
  3. 易维护:代码结构清晰,格式化逻辑统一,易于扩展和维护
  4. 数据准确:使用 DateTimeFormatter 保证格式化准确性
  5. 分布式支持Redis 缓存支持分布式部署,多实例共享缓存
  6. 功能完整:支持多日期格式、时区转换、多语言显示等核心功能
  7. 代码复用:复用已实现的时区转换和国际化功能,减少开发成本
  8. 类型安全:使用 LocalDate、LocalDateTime 等类型安全的日期时间类型

负面后果

  1. AOP 学习成本:开发人员需要了解 AOP 的使用方式
  2. 切面顺序控制:需要控制 DateFormatAspect 和 TimeZoneConvertAspect 的执行顺序
  3. 缓存一致性:分布式环境下需要保证缓存一致性
  4. 多类型支持:需要支持 Date、LocalDate、LocalDateTime 等多种日期时间类型,增加实现复杂度

相关文档