在微服务场景下,传统基于 messages.properties 的 classpath 国际化有两个明显短板:改文案要发版,以及多实例、多服务之间词条难以统一。
本文介绍一套已在生产形态项目中验证的方案:以 MySQL 为唯一数据源,Redis Hash 跨服务共享词典,Caffeine 做进程内加速,并通过 Redis Pub/Sub 在词条变更后失效各实例本地缓存。文中给出可直接复制使用的 建表 SQL、初始化数据、以及全部核心 Java 代码,不依赖任何外部仓库。
一、设计目标
| 目标 | 实现方式 |
|---|---|
| 运行时改文案,无需重启 | 词条表 + 管理 API,写库后同步 Redis 并广播 |
| 多微服务共享同一份词典 | Redis Hash:i18n:dict:{locale} |
| 低延迟读 | Caffeine 本地缓存 + 启动预热 |
| 与 Spring 生态兼容 | 自定义 @Primary MessageSource,排除 Boot 默认配置 |
| 统一 API 提示语 | businessCode 即 i18n key,message 为已翻译文案 |
| 枚举/下拉可国际化 | BaseEnum + TranslatorSpi + EnumUtils |
整体架构
模块划分建议:
- common 模块:
Result、BusinessException、BaseEnum、I18nCacheKeys等,不依赖 Redis。 - web-starter 模块:语言解析、词条查找、Caffeine、Pub/Sub 监听、
MessageSource自动配置。 - base 服务:词条 CRUD、唯一写入 Redis 的职责、启动时 DB → Redis 全量同步。
- 业务服务:只引入 web-starter,只读 Redis。
二、数据库设计
2.1 词条表
| |
2.2 语言表(供前端切换展示)
| |
2.3 初始化词条示例
message_code 与 API 的 businessCode 使用同一套编码。建议:99xxxx 全局系统码,10xxxx 基础模块,各业务模块自行分段。
| |
统一 API 响应示例:
| |
2.4 Redis 结构约定
| |
- Hash Key:
i18n:dict:zh_CN - Hash Field:
message_code - Hash Value:
message_value - 刷新载荷:
ALL或zh_CN:990000(locale:messageCode)
三、公共层代码(common 模块)
3.1 缓存 Key 工具
| |
3.2 翻译器 SPI(common 只定义接口,web 层实现)
| |
3.3 枚举国际化契约
| |
3.4 全局响应码枚举
| |
3.5 统一响应体
| |
3.6 业务异常
| |
3.7 业务码常量(示例)
| |
3.8 下拉选项与枚举工具
| |
| |
枚举定义示例:
| |
四、读路径代码(web-starter 模块)
4.1 排除 Boot 默认 MessageSource
application.properties:
| |
4.2 语言解析器
按优先级:lang 查询参数 → X-Language 请求头 → 默认 zh_CN。无状态 API,不使用 Session。
| |
4.3 Caffeine 本地缓存配置
| |
4.4 Redis + Caffeine 词条查找
| |
4.5 对接 Spring MessageSource
| |
4.6 静态翻译工具 I18nUtils
| |
4.7 TranslatorSpi 实现
| |
4.8 自动配置
| |
4.9 本地缓存失效(Pub/Sub 消费端)
| |
| |
| |
4.10 启动预热(可选,失败不阻断启动)
| |
五、写路径代码(base 服务)
5.1 Redis 写入仓储(仅 base 服务持有)
| |
5.2 词条变更后同步 Redis
| |
5.3 启动时全量同步
| |
六、业务层使用方式
6.1 ResultUtils(统一 API 出口)
| |
Controller 示例:
| |
6.2 全局异常处理
| |
业务代码只需抛码:
| |
6.3 客户端如何切换语言
| |
或:
| |
七、管理 API 约定
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/i18n/messages | 分页查询词条 |
| POST | /api/v1/i18n/messages | 新增 → 写 Redis → 发布刷新 |
| PUT | /api/v1/i18n/messages/{id} | 更新 → 同步 Redis |
| DELETE | /api/v1/i18n/messages/{id} | 删除 → 从 Redis 移除 |
| POST | /api/v1/i18n/messages/sync | 全量 DB → Redis,广播 ALL |
| GET | /api/v1/i18n/locales | 可选语言列表 |
八、端到端时序
一次 API 调用的读路径:
TpLocaleResolver解析语言,写入LocaleContextHolderResultUtils.fail("100001")调用I18nUtils.getMessageI18nRedisMessageLookup:Caffeine → Redis HashMessageFormat填充占位符,写入Result.message
一次后台改词的写路径:
- 更新 MySQL
putMessage写入i18n:dict:zh_CNpublishRefreshEntry("zh_CN", "100001")- 各实例
I18nCacheRefreshListener失效对应 Caffeine 键 - 下次请求从 Redis 重新加载
九、上线与运维清单
- 在
sys_i18n_locale增加语言(前端展示用) - 为每个
message_code插入各locale的message_value - 调用
POST .../messages/sync或重启 base 服务触发全量同步 - 确认所有微服务
spring.data.redis.database一致 - 查看启动日志:
i18n:dict:zh_CN词条数应大于 0 - 用
X-Language: en_US验证message字段
十、注意事项
- Redis 是运行时强依赖:DB 有词但 Redis 未同步时,接口会走
defaultMessage兜底。 - database 索引必须一致:写入与读取若落在不同 logical database,会表现为「永远中文」。
- Locale 规范化:查找时使用
zh_CN形式,不要把zh_CN_#Hans直接当 Hash 后缀。 - 占位符:统一
{0}、{1},与MessageFormat一致。 - Caffeine TTL(2 小时) 仅作兜底;正常靠 Pub/Sub 失效。
- 编码规划:提前划分业务码段,避免多团队冲突。
小结
这套方案把国际化从「静态 properties 文件」升级为「可运营的动态词典」:MySQL 负责真相来源,Redis 负责跨服务分发,Caffeine 负责单机性能,Pub/Sub 负责近实时一致性;业务侧通过 businessCode、BusinessException、BaseEnum 和 ResultUtils 统一出口,只关心「返回什么码」,由基础设施按请求语言渲染「说什么话」。
文中代码包名统一写作 com.example.*,落地时替换为你的工程包名即可;依赖方面需要 Spring Boot Web、Spring Data Redis、Caffeine、Lombok(可选)。