
Spring国际化核心原理详解:吃透这4个核心组件,搞定企业级多语言开发
前言:为什么需要国际化?企业级项目多语言场景痛点
在全球化业务扩张和多区域部署的背景下,企业级Spring应用的“多语言适配”已从“加分项”变成“必选项”。你是否遇到过这些痛点:
- 硬编码的提示语、异常信息散落在代码中,新增语言时需要逐行修改代码,效率低且易出错;
- 不同国家/地区用户使用系统时,看到的仍是固定语言,体验差;
- 微服务场景下,服务间调用的异常信息语言不一致,排查问题成本高;
- 想切换语言但不知道底层逻辑,只能照搬网上的配置,遇到问题无从下手。
Spring框架提供了一套成熟的国际化(i18n,Internationalization的缩写,因首字母I和尾字母N之间有18个字母得名)解决方案,其核心是通过4个核心组件实现“消息解耦+动态加载”,让多语言适配变得简单、可维护。本文作为系列开篇,将从底层原理到入门实操,彻底讲透Spring国际化的核心逻辑,帮你从“知其然”到“知其所以然”。
Spring国际化核心设计思想:基于Locale的消息解耦与动态解析
Spring国际化的本质是**“数据与展示分离”**:将所有需要多语言展示的文本(如提示语、异常信息、校验规则)从代码中抽离,存储在独立的配置文件中;当用户发起请求时,框架根据当前的Locale(语言环境,由“语言+地区”组成,如zh_CN表示中文-中国、en_US表示英文-美国),动态加载对应语言的文本并返回。
核心设计思路可总结为3步:
整个过程的核心依赖4个组件:MessageSource(消息源)、LocaleResolver(Locale解析器)、LocaleContextHolder(Locale容器)、MessageFormat(参数格式化),它们的联动关系如下:
#mermaid-svg-cmlYuTm5auNlZ31S{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cmlYuTm5auNlZ31S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cmlYuTm5auNlZ31S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cmlYuTm5auNlZ31S .error-icon{fill:#552222;}#mermaid-svg-cmlYuTm5auNlZ31S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cmlYuTm5auNlZ31S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cmlYuTm5auNlZ31S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cmlYuTm5auNlZ31S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cmlYuTm5auNlZ31S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cmlYuTm5auNlZ31S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cmlYuTm5auNlZ31S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cmlYuTm5auNlZ31S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cmlYuTm5auNlZ31S .marker.cross{stroke:#333333;}#mermaid-svg-cmlYuTm5auNlZ31S svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cmlYuTm5auNlZ31S p{margin:0;}#mermaid-svg-cmlYuTm5auNlZ31S .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-cmlYuTm5auNlZ31S .cluster-label text{fill:#333;}#mermaid-svg-cmlYuTm5auNlZ31S .cluster-label span{color:#333;}#mermaid-svg-cmlYuTm5auNlZ31S .cluster-label span p{background-color:transparent;}#mermaid-svg-cmlYuTm5auNlZ31S .label text,#mermaid-svg-cmlYuTm5auNlZ31S span{fill:#333;color:#333;}#mermaid-svg-cmlYuTm5auNlZ31S .node rect,#mermaid-svg-cmlYuTm5auNlZ31S .node circle,#mermaid-svg-cmlYuTm5auNlZ31S .node ellipse,#mermaid-svg-cmlYuTm5auNlZ31S .node polygon,#mermaid-svg-cmlYuTm5auNlZ31S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cmlYuTm5auNlZ31S .rough-node .label text,#mermaid-svg-cmlYuTm5auNlZ31S .node .label text,#mermaid-svg-cmlYuTm5auNlZ31S .image-shape .label,#mermaid-svg-cmlYuTm5auNlZ31S .icon-shape .label{text-anchor:middle;}#mermaid-svg-cmlYuTm5auNlZ31S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cmlYuTm5auNlZ31S .rough-node .label,#mermaid-svg-cmlYuTm5auNlZ31S .node .label,#mermaid-svg-cmlYuTm5auNlZ31S .image-shape .label,#mermaid-svg-cmlYuTm5auNlZ31S .icon-shape .label{text-align:center;}#mermaid-svg-cmlYuTm5auNlZ31S .node.clickable{cursor:pointer;}#mermaid-svg-cmlYuTm5auNlZ31S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cmlYuTm5auNlZ31S .arrowheadPath{fill:#333333;}#mermaid-svg-cmlYuTm5auNlZ31S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cmlYuTm5auNlZ31S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cmlYuTm5auNlZ31S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cmlYuTm5auNlZ31S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cmlYuTm5auNlZ31S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cmlYuTm5auNlZ31S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cmlYuTm5auNlZ31S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cmlYuTm5auNlZ31S .cluster text{fill:#333;}#mermaid-svg-cmlYuTm5auNlZ31S .cluster span{color:#333;}#mermaid-svg-cmlYuTm5auNlZ31S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cmlYuTm5auNlZ31S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cmlYuTm5auNlZ31S rect.text{fill:none;stroke-width:0;}#mermaid-svg-cmlYuTm5auNlZ31S .icon-shape,#mermaid-svg-cmlYuTm5auNlZ31S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cmlYuTm5auNlZ31S .icon-shape p,#mermaid-svg-cmlYuTm5auNlZ31S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cmlYuTm5auNlZ31S .icon-shape rect,#mermaid-svg-cmlYuTm5auNlZ31S .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cmlYuTm5auNlZ31S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cmlYuTm5auNlZ31S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cmlYuTm5auNlZ31S :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
客户端请求
LocaleResolver解析Locale
LocaleContextHolder存储Locale到ThreadLocal
业务代码/框架调用MessageSource
MessageSource加载对应Locale的文本
MessageFormat格式化占位符参数
返回多语言文本给客户端
核心组件1:MessageSource – 国际化消息源(核心)
MessageSource是Spring国际化的核心引擎,负责加载多语言配置文件、根据code(消息唯一标识)和Locale解析对应的文本。所有国际化能力都围绕这个接口展开。
3.1 MessageSource接口3个核心方法详解
MessageSource是一个接口,定义了3个核心方法,覆盖不同场景的消息获取需求:
public interface MessageSource {
/**
* 核心方法:根据code、参数、Locale获取消息
* @param code 消息唯一标识(如validate.user.name.empty)
* @param args 消息中的占位符参数(如{0}=18, {1}=60)
* @param locale 目标语言环境
* @return 解析后的多语言文本
* @throws NoSuchMessageException 当code不存在且无默认值时抛出
*/
String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;
/**
* 重载方法:指定默认消息,避免code不存在时抛异常
* @param defaultMessage 兜底消息(code不存在时返回)
*/
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);
/**
* 高级方法:通过MessageSourceResolvable传递解析参数(如校验异常的Error对象)
*/
String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;
}
关键说明:
- 最常用的是第二个方法(带默认消息),生产环境必须使用该方法,避免因code拼写错误导致服务报错;
- MessageSourceResolvable通常用于框架内部(如Spring Validation的校验错误),业务代码极少直接使用。
3.2 常用实现类对比:ResourceBundleMessageSource vs ReloadableResourceBundleMessageSource
Spring提供了多个MessageSource实现类,其中最常用的是以下两个,需根据场景选择:
| ResourceBundleMessageSource | 基于JDK的ResourceBundle加载classpath下的.properties文件 | 轻量、原生、启动快 | 不支持配置文件热加载(修改后需重启应用);仅支持classpath资源 | 小型项目、配置变更频率低的场景 |
| ReloadableResourceBundleMessageSource | 扩展ResourceBundleMessageSource,支持文件系统/URL资源加载 | 支持配置文件热加载;可加载classpath外的资源;自定义缓存策略 | 启动时略慢;占用少量额外内存 | 中大型企业级项目、配置需要动态更新的场景 |
核心区别可视化:
#mermaid-svg-eullEqYKvML6W8ki{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eullEqYKvML6W8ki .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eullEqYKvML6W8ki .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eullEqYKvML6W8ki .error-icon{fill:#552222;}#mermaid-svg-eullEqYKvML6W8ki .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eullEqYKvML6W8ki .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eullEqYKvML6W8ki .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eullEqYKvML6W8ki .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eullEqYKvML6W8ki .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eullEqYKvML6W8ki .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eullEqYKvML6W8ki .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eullEqYKvML6W8ki .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eullEqYKvML6W8ki .marker.cross{stroke:#333333;}#mermaid-svg-eullEqYKvML6W8ki svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eullEqYKvML6W8ki p{margin:0;}#mermaid-svg-eullEqYKvML6W8ki .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-eullEqYKvML6W8ki .cluster-label text{fill:#333;}#mermaid-svg-eullEqYKvML6W8ki .cluster-label span{color:#333;}#mermaid-svg-eullEqYKvML6W8ki .cluster-label span p{background-color:transparent;}#mermaid-svg-eullEqYKvML6W8ki .label text,#mermaid-svg-eullEqYKvML6W8ki span{fill:#333;color:#333;}#mermaid-svg-eullEqYKvML6W8ki .node rect,#mermaid-svg-eullEqYKvML6W8ki .node circle,#mermaid-svg-eullEqYKvML6W8ki .node ellipse,#mermaid-svg-eullEqYKvML6W8ki .node polygon,#mermaid-svg-eullEqYKvML6W8ki .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eullEqYKvML6W8ki .rough-node .label text,#mermaid-svg-eullEqYKvML6W8ki .node .label text,#mermaid-svg-eullEqYKvML6W8ki .image-shape .label,#mermaid-svg-eullEqYKvML6W8ki .icon-shape .label{text-anchor:middle;}#mermaid-svg-eullEqYKvML6W8ki .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eullEqYKvML6W8ki .rough-node .label,#mermaid-svg-eullEqYKvML6W8ki .node .label,#mermaid-svg-eullEqYKvML6W8ki .image-shape .label,#mermaid-svg-eullEqYKvML6W8ki .icon-shape .label{text-align:center;}#mermaid-svg-eullEqYKvML6W8ki .node.clickable{cursor:pointer;}#mermaid-svg-eullEqYKvML6W8ki .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eullEqYKvML6W8ki .arrowheadPath{fill:#333333;}#mermaid-svg-eullEqYKvML6W8ki .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eullEqYKvML6W8ki .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eullEqYKvML6W8ki .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eullEqYKvML6W8ki .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eullEqYKvML6W8ki .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eullEqYKvML6W8ki .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eullEqYKvML6W8ki .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eullEqYKvML6W8ki .cluster text{fill:#333;}#mermaid-svg-eullEqYKvML6W8ki .cluster span{color:#333;}#mermaid-svg-eullEqYKvML6W8ki div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eullEqYKvML6W8ki .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eullEqYKvML6W8ki rect.text{fill:none;stroke-width:0;}#mermaid-svg-eullEqYKvML6W8ki .icon-shape,#mermaid-svg-eullEqYKvML6W8ki .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eullEqYKvML6W8ki .icon-shape p,#mermaid-svg-eullEqYKvML6W8ki .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eullEqYKvML6W8ki .icon-shape rect,#mermaid-svg-eullEqYKvML6W8ki .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eullEqYKvML6W8ki .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eullEqYKvML6W8ki .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eullEqYKvML6W8ki :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
ReloadableResourceBundleMessageSource
加载classpath/文件系统/URL资源
自定义缓存策略
缓存过期自动刷新
修改后无需重启
ResourceBundleMessageSource
加载classpath下.properties
缓存至JVM内存
修改后需重启生效
3.3 关键配置:编码、缓存、默认语言、多资源文件加载
以ReloadableResourceBundleMessageSource为例,核心配置项决定了国际化的可用性和性能:
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
// 1. 配置文件基础名称(classpath下的i18n/messages,会自动匹配messages_zh_CN.properties)
messageSource.setBasename("classpath:i18n/messages");
// 2. 多资源文件加载(按模块拆分,避免单个文件过大)
// messageSource.setBasenames("classpath:i18n/common", "classpath:i18n/validation", "classpath:i18n/error");
// 3. 编码格式(必须设置为UTF-8,解决中文乱码)
messageSource.setDefaultEncoding("UTF-8");
// 4. 默认语言(找不到对应Locale的消息时使用)
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
// 5. 缓存时间(秒):0=每次加载最新(开发环境),3600=1小时刷新(生产环境)
messageSource.setCacheSeconds(3600);
// 6. 当code不存在时,是否使用code作为默认消息(避免抛异常)
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
核心配置避坑:
- setDefaultEncoding("UTF-8")是必须项!如果不设置,.properties文件中的中文会出现乱码;
- setUseCodeAsDefaultMessage(true)建议开启,开发阶段可快速发现未配置的code(返回code本身),生产阶段需配合默认消息使用;
- 多模块项目建议拆分配置文件(如common/validation/error),避免单个文件上千行,维护困难。
核心组件2:LocaleResolver – 语言环境解析器
LocaleResolver负责解析当前请求的Locale,是连接“用户请求”和“MessageSource”的桥梁。Spring提供了3种主流实现,覆盖绝大多数业务场景。
4.1 3种主流实现类对比
4.1.1 AcceptHeaderLocaleResolver(默认)
工作原理:从HTTP请求头Accept-Language中解析Locale(如请求头Accept-Language: en-US,zh-CN;q=0.9表示优先使用英文,其次中文)。
核心配置:
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
// 设置默认Locale(请求头无Accept-Language时使用)
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
// 限制支持的语言列表(避免解析非法Locale)
resolver.setSupportedLocales(List.of(Locale.SIMPLIFIED_CHINESE, Locale.US));
return resolver;
}
优点:无需额外配置,符合HTTP协议规范,适配前端框架(如Vue/React)的默认请求头;
缺点:无法手动切换语言(依赖客户端请求头);
适用场景:纯前端控制语言、无需用户手动切换的场景(如移动端APP、海外站点)。
4.1.2 SessionLocaleResolver
工作原理:从用户Session中获取Locale,支持手动切换语言(切换后Locale存储在Session中,会话内生效)。
核心配置:
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setSupportedLocales(List.of(Locale.SIMPLIFIED_CHINESE, Locale.US));
return resolver;
}
// 语言切换接口
@GetMapping("/switchLocale")
public String switchLocale(@RequestParam String lang, HttpSession session) {
Locale locale = switch (lang) {
case "en" -> Locale.US;
case "zh" -> Locale.SIMPLIFIED_CHINESE;
default -> Locale.SIMPLIFIED_CHINESE;
};
// 将Locale存入Session
session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale);
return "Locale switched to: " + lang;
}
优点:支持用户手动切换语言,会话内保持;
缺点:依赖Session,分布式场景需配置Session共享(如Redis);
适用场景:PC端后台系统、需要用户手动切换语言的场景。
4.1.3 CookieLocaleResolver
工作原理:从Cookie中获取Locale,支持语言偏好持久化(切换后Cookie存储Locale,浏览器重启后仍生效)。
核心配置:
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setSupportedLocales(List.of(Locale.SIMPLIFIED_CHINESE, Locale.US));
// Cookie配置:名称、过期时间(7天)、路径
resolver.setCookieName("lang");
resolver.setCookieMaxAge(60 * 60 * 24 * 7);
resolver.setCookiePath("/");
return resolver;
}
// 语言切换接口
@GetMapping("/switchLocale")
public String switchLocale(@RequestParam String lang, HttpServletResponse response) {
Locale locale = switch (lang) {
case "en" -> Locale.US;
case "zh" -> Locale.SIMPLIFIED_CHINESE;
default -> Locale.SIMPLIFIED_CHINESE;
};
// 将Locale存入Cookie
((CookieLocaleResolver) localeResolver()).setLocale(null, response, locale);
return "Locale switched to: " + lang;
}
优点:语言偏好持久化,无需Session,适配分布式场景;
缺点:依赖Cookie,用户禁用Cookie时失效;
适用场景:面向C端的Web应用、需要持久化语言偏好的场景。
3种解析器对比可视化:
#mermaid-svg-LV91wIjpQ4cq7Pgy{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LV91wIjpQ4cq7Pgy .error-icon{fill:#552222;}#mermaid-svg-LV91wIjpQ4cq7Pgy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LV91wIjpQ4cq7Pgy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LV91wIjpQ4cq7Pgy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LV91wIjpQ4cq7Pgy .marker.cross{stroke:#333333;}#mermaid-svg-LV91wIjpQ4cq7Pgy svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LV91wIjpQ4cq7Pgy p{margin:0;}#mermaid-svg-LV91wIjpQ4cq7Pgy .mermaid-main-font{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-LV91wIjpQ4cq7Pgy .exclude-range{fill:#eeeeee;}#mermaid-svg-LV91wIjpQ4cq7Pgy .section{stroke:none;opacity:0.2;}#mermaid-svg-LV91wIjpQ4cq7Pgy .section0{fill:rgba(102, 102, 255, 0.49);}#mermaid-svg-LV91wIjpQ4cq7Pgy .section2{fill:#fff400;}#mermaid-svg-LV91wIjpQ4cq7Pgy .section1,#mermaid-svg-LV91wIjpQ4cq7Pgy .section3{fill:white;opacity:0.2;}#mermaid-svg-LV91wIjpQ4cq7Pgy .sectionTitle0{fill:#333;}#mermaid-svg-LV91wIjpQ4cq7Pgy .sectionTitle1{fill:#333;}#mermaid-svg-LV91wIjpQ4cq7Pgy .sectionTitle2{fill:#333;}#mermaid-svg-LV91wIjpQ4cq7Pgy .sectionTitle3{fill:#333;}#mermaid-svg-LV91wIjpQ4cq7Pgy .sectionTitle{text-anchor:start;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-LV91wIjpQ4cq7Pgy .grid .tick{stroke:lightgrey;opacity:0.8;shape-rendering:crispEdges;}#mermaid-svg-LV91wIjpQ4cq7Pgy .grid .tick text{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;fill:#333;}#mermaid-svg-LV91wIjpQ4cq7Pgy .grid path{stroke-width:0;}#mermaid-svg-LV91wIjpQ4cq7Pgy .today{fill:none;stroke:red;stroke-width:2px;}#mermaid-svg-LV91wIjpQ4cq7Pgy .task{stroke-width:2;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskText{text-anchor:middle;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutsideRight{fill:black;text-anchor:start;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutsideLeft{fill:black;text-anchor:end;}#mermaid-svg-LV91wIjpQ4cq7Pgy .task.clickable{cursor:pointer;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskText.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutsideLeft.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutsideRight.clickable{cursor:pointer;fill:#003163!important;font-weight:bold;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskText0,#mermaid-svg-LV91wIjpQ4cq7Pgy .taskText1,#mermaid-svg-LV91wIjpQ4cq7Pgy .taskText2,#mermaid-svg-LV91wIjpQ4cq7Pgy .taskText3{fill:white;}#mermaid-svg-LV91wIjpQ4cq7Pgy .task0,#mermaid-svg-LV91wIjpQ4cq7Pgy .task1,#mermaid-svg-LV91wIjpQ4cq7Pgy .task2,#mermaid-svg-LV91wIjpQ4cq7Pgy .task3{fill:#8a90dd;stroke:#534fbc;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutside0,#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutside2{fill:black;}#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutside1,#mermaid-svg-LV91wIjpQ4cq7Pgy .taskTextOutside3{fill:black;}#mermaid-svg-LV91wIjpQ4cq7Pgy .active0,#mermaid-svg-LV91wIjpQ4cq7Pgy .active1,#mermaid-svg-LV91wIjpQ4cq7Pgy .active2,#mermaid-svg-LV91wIjpQ4cq7Pgy .active3{fill:#bfc7ff;stroke:#534fbc;}#mermaid-svg-LV91wIjpQ4cq7Pgy .activeText0,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeText1,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeText2,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeText3{fill:black!important;}#mermaid-svg-LV91wIjpQ4cq7Pgy .done0,#mermaid-svg-LV91wIjpQ4cq7Pgy .done1,#mermaid-svg-LV91wIjpQ4cq7Pgy .done2,#mermaid-svg-LV91wIjpQ4cq7Pgy .done3{stroke:grey;fill:lightgrey;stroke-width:2;}#mermaid-svg-LV91wIjpQ4cq7Pgy .doneText0,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneText1,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneText2,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneText3{fill:black!important;}#mermaid-svg-LV91wIjpQ4cq7Pgy .crit0,#mermaid-svg-LV91wIjpQ4cq7Pgy .crit1,#mermaid-svg-LV91wIjpQ4cq7Pgy .crit2,#mermaid-svg-LV91wIjpQ4cq7Pgy .crit3{stroke:#ff8888;fill:red;stroke-width:2;}#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCrit0,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCrit1,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCrit2,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCrit3{stroke:#ff8888;fill:#bfc7ff;stroke-width:2;}#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCrit0,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCrit1,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCrit2,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCrit3{stroke:#ff8888;fill:lightgrey;stroke-width:2;cursor:pointer;shape-rendering:crispEdges;}#mermaid-svg-LV91wIjpQ4cq7Pgy .milestone{transform:rotate(45deg) scale(0.8,0.8);}#mermaid-svg-LV91wIjpQ4cq7Pgy .milestoneText{font-style:italic;}#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCritText0,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCritText1,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCritText2,#mermaid-svg-LV91wIjpQ4cq7Pgy .doneCritText3{fill:black!important;}#mermaid-svg-LV91wIjpQ4cq7Pgy .vert{stroke:navy;}#mermaid-svg-LV91wIjpQ4cq7Pgy .vertText{font-size:15px;text-anchor:middle;fill:navy!important;}#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCritText0,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCritText1,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCritText2,#mermaid-svg-LV91wIjpQ4cq7Pgy .activeCritText3{fill:black!important;}#mermaid-svg-LV91wIjpQ4cq7Pgy .titleText{text-anchor:middle;font-size:18px;fill:#333;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-LV91wIjpQ4cq7Pgy :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;} 01-07 01-14 01-21 01-28 02-04 02-11 02-18 02-25 03-03 03-10 03-17 03-24 03-31纯前端控制语言 PC端后台系统 C端Web应用 移动端APP/海外站点 会话内语言切换 语言偏好持久化 无需手动切换语言 分布式需Session共享 分布式场景 AcceptHeaderLocaleResolverSessionLocaleResolverCookieLocaleResolverLocaleResolver适用场景对比
核心组件3:LocaleContextHolder – 线程级Locale容器
LocaleContextHolder是Spring提供的工具类,基于ThreadLocal实现,用于在当前线程中存储和获取Locale,解决“跨层传递Locale”的问题。
5.1 基于ThreadLocal的实现原理
ThreadLocal的核心作用是“为每个线程提供独立的变量副本”,LocaleContextHolder通过ThreadLocal存储LocaleContext(包含Locale信息),确保不同请求线程的Locale互不干扰。
核心源码简化如下:
public abstract class LocaleContextHolder {
// 存储LocaleContext的ThreadLocal
private static final ThreadLocal<LocaleContext> localeContextHolder = new NamedThreadLocal<>("LocaleContext");
// 可继承的ThreadLocal(子线程继承父线程的Locale)
private static final ThreadLocal<LocaleContext> inheritableLocaleContextHolder = new NamedInheritableThreadLocal<>("LocaleContext");
// 获取当前线程的Locale
public static Locale getLocale() {
LocaleContext localeContext = getLocaleContext();
return (localeContext != null ? localeContext.getLocale() : null);
}
// 设置当前线程的Locale
public static void setLocale(@Nullable Locale locale) {
setLocale(locale, false);
}
// 清理当前线程的Locale(避免内存泄漏)
public static void resetLocaleContext() {
localeContextHolder.remove();
inheritableLocaleContextHolder.remove();
}
}
原理可视化:
#mermaid-svg-kYCvd8WXHD3ekjhP{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kYCvd8WXHD3ekjhP .error-icon{fill:#552222;}#mermaid-svg-kYCvd8WXHD3ekjhP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kYCvd8WXHD3ekjhP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kYCvd8WXHD3ekjhP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kYCvd8WXHD3ekjhP .marker.cross{stroke:#333333;}#mermaid-svg-kYCvd8WXHD3ekjhP svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kYCvd8WXHD3ekjhP p{margin:0;}#mermaid-svg-kYCvd8WXHD3ekjhP .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-kYCvd8WXHD3ekjhP .cluster-label text{fill:#333;}#mermaid-svg-kYCvd8WXHD3ekjhP .cluster-label span{color:#333;}#mermaid-svg-kYCvd8WXHD3ekjhP .cluster-label span p{background-color:transparent;}#mermaid-svg-kYCvd8WXHD3ekjhP .label text,#mermaid-svg-kYCvd8WXHD3ekjhP span{fill:#333;color:#333;}#mermaid-svg-kYCvd8WXHD3ekjhP .node rect,#mermaid-svg-kYCvd8WXHD3ekjhP .node circle,#mermaid-svg-kYCvd8WXHD3ekjhP .node ellipse,#mermaid-svg-kYCvd8WXHD3ekjhP .node polygon,#mermaid-svg-kYCvd8WXHD3ekjhP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kYCvd8WXHD3ekjhP .rough-node .label text,#mermaid-svg-kYCvd8WXHD3ekjhP .node .label text,#mermaid-svg-kYCvd8WXHD3ekjhP .image-shape .label,#mermaid-svg-kYCvd8WXHD3ekjhP .icon-shape .label{text-anchor:middle;}#mermaid-svg-kYCvd8WXHD3ekjhP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kYCvd8WXHD3ekjhP .rough-node .label,#mermaid-svg-kYCvd8WXHD3ekjhP .node .label,#mermaid-svg-kYCvd8WXHD3ekjhP .image-shape .label,#mermaid-svg-kYCvd8WXHD3ekjhP .icon-shape .label{text-align:center;}#mermaid-svg-kYCvd8WXHD3ekjhP .node.clickable{cursor:pointer;}#mermaid-svg-kYCvd8WXHD3ekjhP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kYCvd8WXHD3ekjhP .arrowheadPath{fill:#333333;}#mermaid-svg-kYCvd8WXHD3ekjhP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kYCvd8WXHD3ekjhP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kYCvd8WXHD3ekjhP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kYCvd8WXHD3ekjhP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kYCvd8WXHD3ekjhP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kYCvd8WXHD3ekjhP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kYCvd8WXHD3ekjhP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kYCvd8WXHD3ekjhP .cluster text{fill:#333;}#mermaid-svg-kYCvd8WXHD3ekjhP .cluster span{color:#333;}#mermaid-svg-kYCvd8WXHD3ekjhP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kYCvd8WXHD3ekjhP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kYCvd8WXHD3ekjhP rect.text{fill:none;stroke-width:0;}#mermaid-svg-kYCvd8WXHD3ekjhP .icon-shape,#mermaid-svg-kYCvd8WXHD3ekjhP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kYCvd8WXHD3ekjhP .icon-shape p,#mermaid-svg-kYCvd8WXHD3ekjhP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kYCvd8WXHD3ekjhP .icon-shape rect,#mermaid-svg-kYCvd8WXHD3ekjhP .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kYCvd8WXHD3ekjhP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kYCvd8WXHD3ekjhP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kYCvd8WXHD3ekjhP :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
请求线程1
ThreadLocal:zh_CN
请求线程2
ThreadLocal:en_US
异步线程
InheritableThreadLocal:zh_CN
5.2 如何全局获取当前请求的Locale?
在业务代码、异常处理器、工具类中,可通过以下方式全局获取当前请求的Locale:
// 方式1:核心方式,推荐使用
Locale currentLocale = LocaleContextHolder.getLocale();
// 方式2:在Controller中通过RequestContextUtils获取(底层还是调用LocaleContextHolder)
Locale currentLocale = RequestContextUtils.getLocale(request);
关键注意事项:
- LocaleContextHolder.getLocale()在Web场景下,由Spring的LocaleContextInterceptor自动设置(无需手动操作);
- 非Web场景(如定时任务、异步任务)中,LocaleContextHolder.getLocale()返回null或默认Locale,需手动设置;
- 异步任务中,若需继承父线程的Locale,需使用InheritableThreadLocal模式(通过LocaleContextHolder.setLocale(locale, true)设置)。
核心组件4:MessageFormat – 消息参数格式化
MessageFormat是JDK提供的工具类(Spring直接复用),负责解析消息文本中的占位符(如{0}、{1}),替换为动态参数值,支持数字、日期、时间等格式的定制化。
6.1 占位符解析规则({0}/{1})
MessageFormat的占位符格式为{索引[,格式类型[,格式样式]]},基础用法是通过索引匹配参数:
示例1:基础占位符
- 国际化配置:service.call.timeout=调用{0}服务超时,超时时间:{1}ms
- 代码调用:String message = messageSource.getMessage("service.call.timeout", new Object[]{"user-service", 5000}, Locale.SIMPLIFIED_CHINESE);
// 输出:调用user-service服务超时,超时时间:5000ms
示例2:带格式的占位符
- 国际化配置:user.register.time=用户{0}注册时间:{1,date,yyyy-MM-dd HH:mm:ss}
- 代码调用:String message = messageSource.getMessage("user.register.time", new Object[]{"张三", new Date()}, Locale.SIMPLIFIED_CHINESE);
// 输出:用户张三注册时间:2024-05-20 15:30:00
6.2 数字、日期等特殊格式的定制化
MessageFormat支持丰富的格式类型,常见的有:
| number | integer | {1,number,integer} | 5000 |
| number | currency | {1,number,currency} | ¥5,000.00(zh_CN)/$5,000.00(en_US) |
| date | short | {1,date,short} | 24-5-20 |
| date | long | {1,date,long} | 2024年5月20日 |
| time | medium | {1,time,medium} | 15:30:00 |
完整示例:
- 国际化配置(zh_CN):order.amount=订单{0}的金额:{1,number,currency},创建时间:{2,date,long}
- 国际化配置(en_US):order.amount=Order {0} amount: {1,number,currency}, create time: {2,date,long}
- 代码调用:// 中文环境
String zhMessage = messageSource.getMessage("order.amount", new Object[]{"ORD123456", 999.99, new Date()}, Locale.SIMPLIFIED_CHINESE);
// 输出:订单ORD123456的金额:¥999.99,创建时间:2024年5月20日// 英文环境
String enMessage = messageSource.getMessage("order.amount", new Object[]{"ORD123456", 999.99, new Date()}, Locale.US);
// 输出:Order ORD123456 amount: $999.99, create time: May 20, 2024
避坑指南:
- 占位符索引从0开始,参数数组的长度需与占位符数量匹配,否则会抛出IllegalArgumentException;
- 若消息文本中包含{或},需转义为'{'或'}'(如validate.password.regex=密码必须包含\\\\{数字\\\\}和\\\\{字母\\\\});
- 日期/数字格式依赖Locale,无需手动适配不同地区的格式(如中文显示¥,英文显示$)。
Spring国际化完整执行流程(Web场景)
结合以上4个核心组件,Web场景下Spring国际化的完整执行流程如下:
响应MessageFormatMessageSourceControllerLocaleContextHolderLocaleResolverDispatcherServlet客户端响应MessageFormatMessageSourceControllerLocaleContextHolderLocaleResolverDispatcherServlet客户端#mermaid-svg-4uMWbkKGA44C3cyv{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4uMWbkKGA44C3cyv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4uMWbkKGA44C3cyv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4uMWbkKGA44C3cyv .error-icon{fill:#552222;}#mermaid-svg-4uMWbkKGA44C3cyv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4uMWbkKGA44C3cyv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4uMWbkKGA44C3cyv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4uMWbkKGA44C3cyv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4uMWbkKGA44C3cyv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4uMWbkKGA44C3cyv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4uMWbkKGA44C3cyv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4uMWbkKGA44C3cyv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4uMWbkKGA44C3cyv .marker.cross{stroke:#333333;}#mermaid-svg-4uMWbkKGA44C3cyv svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4uMWbkKGA44C3cyv p{margin:0;}#mermaid-svg-4uMWbkKGA44C3cyv .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4uMWbkKGA44C3cyv text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-4uMWbkKGA44C3cyv .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4uMWbkKGA44C3cyv .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-4uMWbkKGA44C3cyv .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-4uMWbkKGA44C3cyv .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-4uMWbkKGA44C3cyv #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-4uMWbkKGA44C3cyv .sequenceNumber{fill:white;}#mermaid-svg-4uMWbkKGA44C3cyv #sequencenumber{fill:#333;}#mermaid-svg-4uMWbkKGA44C3cyv #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-4uMWbkKGA44C3cyv .messageText{fill:#333;stroke:none;}#mermaid-svg-4uMWbkKGA44C3cyv .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4uMWbkKGA44C3cyv .labelText,#mermaid-svg-4uMWbkKGA44C3cyv .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-4uMWbkKGA44C3cyv .loopText,#mermaid-svg-4uMWbkKGA44C3cyv .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-4uMWbkKGA44C3cyv .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-4uMWbkKGA44C3cyv .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-4uMWbkKGA44C3cyv .noteText,#mermaid-svg-4uMWbkKGA44C3cyv .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-4uMWbkKGA44C3cyv .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4uMWbkKGA44C3cyv .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4uMWbkKGA44C3cyv .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-4uMWbkKGA44C3cyv .actorPopupMenu{position:absolute;}#mermaid-svg-4uMWbkKGA44C3cyv .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-4uMWbkKGA44C3cyv .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-4uMWbkKGA44C3cyv .actor-man circle,#mermaid-svg-4uMWbkKGA44C3cyv line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-4uMWbkKGA44C3cyv :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}发起请求(携带Accept-Language: en-US)解析Locale将Locale(en_US)存入ThreadLocal处理请求调用getMessage("validate.user.name.empty", null, Locale)加载messages_en_US.properties格式化消息(无参数)返回格式化后的文本(Username cannot be empty)返回多语言文本封装文本并返回接收多语言响应
流程拆解:
入门实操:快速搭建Spring Boot国际化基础环境
理论讲完,接下来通过一个最小demo,快速搭建Spring Boot国际化环境,验证核心功能。
8.1 环境准备
- Spring Boot 3.x(2.x也适用,仅校验注解包名不同);
- JDK 17+;
- Maven/Gradle。
8.2 配置MessageSource和LocaleResolver
创建I18nConfig.java配置类:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import java.util.List;
import java.util.Locale;
@Configuration
public class I18nConfig {
/**
* 配置MessageSource(核心)
*/
@Bean
public ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
// 配置文件基础路径
messageSource.setBasename("classpath:i18n/messages");
// 编码格式(解决中文乱码)
messageSource.setDefaultEncoding("UTF-8");
// 默认语言
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
// 缓存时间(生产环境建议3600秒,开发环境设为0)
messageSource.setCacheSeconds(0);
// 支持的语言列表
messageSource.setSupportedLocales(List.of(Locale.SIMPLIFIED_CHINESE, Locale.US));
// code不存在时使用code作为默认消息
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
/**
* 配置LocaleResolver(默认使用AcceptHeaderLocaleResolver)
*/
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setSupportedLocales(List.of(Locale.SIMPLIFIED_CHINESE, Locale.US));
return resolver;
}
}
8.3 编写第一个多语言配置文件
在resources/i18n目录下创建以下文件:
messages_zh_CN.properties(中文)
# 基础提示语
hello.world=你好,世界!
user.name.empty=用户名不能为空
user.age.range=年龄必须在{0}到{1}之间
messages_en_US.properties(英文)
# 基础提示语
hello.world=Hello, World!
user.name.empty=Username cannot be empty
user.age.range=Age must be between {0} and {1}
8.4 编写测试接口
创建I18nController.java:
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Locale;
@RestController
@RequestMapping("/i18n")
public class I18nController {
private final MessageSource messageSource;
// 注入配置好的MessageSource
public I18nController(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* 测试基础国际化
*/
@GetMapping("/hello")
public String hello() {
// 获取当前Locale
Locale currentLocale = LocaleContextHolder.getLocale();
// 获取多语言消息
return messageSource.getMessage("hello.world", null, currentLocale);
}
/**
* 测试带参数的国际化
*/
@GetMapping("/age")
public String ageRange() {
Locale currentLocale = LocaleContextHolder.getLocale();
// 带参数的消息解析
return messageSource.getMessage("user.age.range", new Object[]{18, 60}, currentLocale);
}
}
8.5 测试:通过请求头切换语言
使用Postman/Curl发起请求,验证效果:
测试1:中文环境
curl -H "Accept-Language: zh-CN" http://localhost:8080/i18n/hello
# 输出:你好,世界!
curl -H "Accept-Language: zh-CN" http://localhost:8080/i18n/age
# 输出:年龄必须在18到60之间
测试2:英文环境
curl -H "Accept-Language: en-US" http://localhost:8080/i18n/hello
# 输出:Hello, World!
curl -H "Accept-Language: en-US" http://localhost:8080/i18n/age
# 输出:Age must be between 18 and 60
测试3:默认语言(请求头无Accept-Language)
curl http://localhost:8080/i18n/hello
# 输出:你好,世界!
本章小结:核心组件联动关系梳理
Spring国际化的4个核心组件各司其职,形成完整的闭环:
核心联动逻辑:
LocaleResolver解析Locale → LocaleContextHolder存储Locale → MessageSource使用Locale加载文本 → MessageFormat格式化文本 → 返回给客户端
掌握这4个组件的作用和联动关系,就能解决90%的Spring国际化问题,后续的参数校验、异常处理等实战场景,都是基于这个核心逻辑的延伸。
思考题:为什么MessageSource的bean名称必须是messageSource?
这是Spring框架的“约定优于配置”设计原则的体现:
验证方式:
将I18nConfig中的messageSource()方法改名为customMessageSource(),重启应用后调用/i18n/hello接口,会返回hello.world(而非“你好,世界!”),证明框架未加载自定义的MessageSource。
总结
本文从企业级项目的多语言痛点出发,系统讲解了Spring国际化的核心设计思想和4个核心组件:
- MessageSource是核心引擎,负责加载和解析多语言配置;
- LocaleResolver是Locale解析器,适配不同的Locale获取方式;
- LocaleContextHolder是线程级容器,解决Locale的全局获取问题;
- MessageFormat是参数格式化工具,支持动态参数和格式定制。
通过最小demo的实操,验证了核心组件的使用方式,掌握这些原理后,后续的参数校验国际化、异常信息国际化等实战场景,都能轻松应对。
下一篇文章将聚焦“Spring参数校验国际化实战”,结合Spring Validation实现校验提示语的多语言适配,敬请期待!




