国际化
本章是 Spring MVC 教程中"高级特性与最佳实践"章节的基础设施内容。在讲解拦截器、跨域、缓存等高级机制之前,先理解国际化(i18n)——它解决的是"同一套系统如何服务不同语言用户"的问题。Spring MVC 的国际化支持不是孤立的翻译工具,而是与 LocaleResolver、ViewResolver 紧密协作的完整链路。
定义与作用
国际化(Internationalization)缩写为 i18n——因为单词首字母 I 和末字母 n 之间有 18 个字母。它指的是软件系统在不修改源代码的前提下,支持多种语言、多种地区格式的能力。与国际化相对的是本地化(Localization,L10n),指针对特定地区进行的具体适配工作。
在 Web 应用中,国际化通常涉及:
- 界面文本:按钮文字、提示信息、菜单标签的多语言切换
- 日期格式:中文环境用
2024年3月15日,美国环境用03/15/2024 - 数字格式:德国用逗号作为小数点(
1,5),美国用点号(1.5) - 货币符号:人民币
¥、美元$、欧元€
Spring MVC 的国际化机制由两个核心组件协作完成:
LocaleResolver:从客户端请求中解析出当前用户的语言环境(Locale)。Locale 包含语言代码(如
zh)和地区代码(如CN),组合成zh_CN。MessageSource:根据 Locale 加载对应的多语言资源文件,提供"按 key 取翻译"的服务。Controller 和视图模板通过 key 获取当前语言下的实际文本。
生活类比:跨国酒店的前台
想象一家跨国连锁酒店:
- LocaleResolver 是前台接待员:客人进门时,接待员通过观察(Accept-Header)、询问(Session 记录)或查看房卡(Cookie)判断客人来自哪个国家——中国、美国还是日本?
- MessageSource 是多语言服务手册:酒店准备了一本手册,同一个服务编号在不同语言页有不同翻译。
WELCOME_MSG在中文页是"欢迎光临",在英文页是"Welcome",在日文页是"ようこそ"。 - Controller 和 View 是服务员:服务员不需要学会所有语言,只需要记住服务编号(key),然后根据接待员判断的客人国籍(Locale),从手册对应页查找翻译。
这个类比的关键在于:判断语言环境(LocaleResolver)和提供翻译(MessageSource)是独立的两件事,通过 key 串联起来。
核心原理
国际化解析流程
流程解读:
- 请求进入:浏览器发送 HTTP 请求,可能携带
Accept-Language: zh-CN,en-US;q=0.9头,或 URL 参数?lang=zh_CN。 - Locale 解析:DispatcherServlet 调用 LocaleResolver 从请求中提取 Locale。不同的 LocaleResolver 采用不同策略(见下表)。
- 消息获取:Controller 或视图模板通过 MessageSource,传入 key 和 Locale,获取对应语言的文本。
- 资源加载:MessageSource 按命名规范找到
messages_zh_CN.properties,返回"welcome.title=员工管理系统"中的值。 - 响应渲染:视图模板将国际化文本填充到页面,返回给浏览器。
常见 LocaleResolver 实现对比
| 实现类 | 解析策略 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| AcceptHeaderLocaleResolver | 读取 HTTP 请求头 Accept-Language | 默认策略,适合无状态 REST API | 无需额外配置,浏览器自动发送 | 用户无法手动切换语言 |
| SessionLocaleResolver | 从 HttpSession 中读取 SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME | 需要用户手动切换语言的 Web 应用 | 切换后整个会话保持同一语言 | 会话过期后语言设置丢失 |
| CookieLocaleResolver | 从 Cookie 中读取语言偏好 | 希望语言偏好长期保存的站点 | 关闭浏览器后再打开,语言设置仍在 | 用户可能禁用 Cookie |
| FixedLocaleResolver | 固定使用系统默认 Locale | 单语言系统,强制禁用国际化 | 配置最简单 | 完全无国际化能力 |
选择建议:
- 如果系统只服务国内用户,用 AcceptHeaderLocaleResolver(默认即可,无需配置)
- 如果系统有语言切换按钮(如中英文切换),用 CookieLocaleResolver,让选择持久化
- 如果系统同时有 Web 页面和 REST API,可以配置两个 DispatcherServlet,分别用不同的 LocaleResolver
资源文件命名规范与结构
Spring MVC 的 MessageSource 默认加载 classpath 下名为 messages 开头的 properties 文件:
src/main/resources/
├── messages.properties # 默认语言(兜底)
├── messages_zh_CN.properties # 简体中文(中国大陆)
├── messages_zh_TW.properties # 繁体中文(中国台湾)
├── messages_en_US.properties # 美式英语
├── messages_en_GB.properties # 英式英语
└── messages_ja_JP.properties # 日语
命名规则:messages_语言代码_国家代码.properties
| 文件 | 语言代码 | 国家代码 | 匹配的 Locale |
|---|---|---|---|
| messages.properties | — | — | 所有未匹配的 Locale 的兜底 |
| messages_zh_CN.properties | zh | CN | new Locale("zh", "CN") |
| messages_en_US.properties | en | US | new Locale("en", "US") |
| messages_ja.properties | ja | — | new Locale("ja")(只指定语言) |
资源文件内容示例:
# messages.properties(默认/英文兜底)
app.title=Employee Management System
app.welcome=Welcome, {0}
button.save=Save
button.delete=Delete
error.notfound=Resource not found
# messages_zh_CN.properties(简体中文)
app.title=员工管理系统
app.welcome=欢迎,{0}
button.save=保存
button.delete=删除
error.notfound=资源不存在
占位符说明:{0}、{1} 等是消息格式化占位符,运行时由 MessageSource 替换为实际参数。例如 getMessage("app.welcome", new Object[]{"张三"}, locale) 返回 "欢迎,张三"。
完整示例
场景
飞翔科技的员工管理系统需要支持中英文双语切换,供国内总部和海外分公司共同使用。CTO 大翔要求:
- 页面右上角有语言切换按钮(中文 / English)
- 切换后整个会话保持所选语言
- 所有界面文本、错误提示、按钮标签都要国际化
- 日期和数字格式随语言环境自动变化
架构师白歌选择 CookieLocaleResolver 作为 Locale 持久化方案。小崔负责配置和 Controller,黄俪负责 Thymeleaf 模板中的国际化标签,李眉负责资源文件的翻译校对。
小崔的配置实现
// WebConfig.java
package com.feixiang.web.config;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import java.util.Locale;
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 配置 MessageSource:加载 classpath 下的 messages*.properties
*/
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages"); // 基础名 messages,自动匹配后缀
messageSource.setDefaultEncoding("UTF-8"); // 资源文件使用 UTF-8 编码
messageSource.setFallbackToSystemLocale(false); // 不匹配时不回退到系统默认语言
messageSource.setUseCodeAsDefaultMessage(true); // key 找不到时返回 key 本身,而非抛异常
return messageSource;
}
/**
* 配置 LocaleResolver:使用 Cookie 持久化语言选择
*/
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 默认简体中文
resolver.setCookieName("feixiang_lang"); // Cookie 名称
resolver.setCookieMaxAge(3600 * 24 * 30); // Cookie 有效期 30 天
return resolver;
}
/**
* 注册 LocaleChangeInterceptor:拦截 ?lang=xx 参数切换语言
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // URL 参数名:?lang=en_US
registry.addInterceptor(interceptor);
}
}
配置分析:
ResourceBundleMessageSource是 MessageSource 的标准实现,自动加载messages*.propertiessetBasename("messages")表示加载messages.properties、messages_zh_CN.properties等所有以messages开头的资源文件CookieLocaleResolver把用户选择的语言存入 Cookie,30 天内再次访问自动恢复LocaleChangeInterceptor拦截?lang=en_US这类 URL 参数,自动修改当前 Locale 并写入 Cookie
资源文件
# messages.properties(英文兜底)
app.title=Employee Management System
nav.home=Home
nav.employees=Employees
nav.departments=Departments
button.search=Search
button.reset=Reset
button.export=Export
msg.welcome=Welcome to {0}
error.access.denied=Access denied. Please contact administrator.
# messages_zh_CN.properties(简体中文)
app.title=员工管理系统
nav.home=首页
nav.employees=员工管理
nav.departments=部门管理
button.search=查询
button.reset=重置
button.export=导出
msg.welcome=欢迎使用{0}
error.access.denied=权限不足,请联系管理员。
小崔的 Controller
// EmployeeController.java
package com.feixiang.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.Locale;
@Controller
public class EmployeeController {
@Autowired
private MessageSource messageSource;
@GetMapping("/employees")
public String list(Model model, Locale locale) {
// 在 Controller 中获取国际化消息
String title = messageSource.getMessage("app.title", null, locale);
String welcome = messageSource.getMessage("msg.welcome",
new Object[]{title}, locale);
model.addAttribute("welcomeMsg", welcome);
return "employeeList";
}
}
注意:Spring MVC 会自动将 Locale 注入到 Controller 方法的参数中——这是 LocaleResolver 与 DispatcherServlet 协作的结果。
黄俪的 Thymeleaf 模板
<!-- employeeList.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title th:text="#{app.title}">Employee Management</title>
</head>
<body>
<!-- 语言切换按钮 -->
<div class="lang-switcher">
<a href="?lang=zh_CN">中文</a>
<a href="?lang=en_US">English</a>
</div>
<h1 th:text="${welcomeMsg}">Welcome</h1>
<!-- 导航菜单 -->
<nav>
<a th:href="@{/}" th:text="#{nav.home}">Home</a>
<a th:href="@{/employees}" th:text="#{nav.employees}">Employees</a>
<a th:href="@{/departments}" th:text="#{nav.departments}">Departments</a>
</nav>
<!-- 操作按钮 -->
<div class="actions">
<button th:text="#{button.search}">Search</button>
<button th:text="#{button.reset}">Reset</button>
<button th:text="#{button.export}">Export</button>
</div>
<!-- 错误提示 -->
<div th:if="${error}" class="error" th:text="#{error.access.denied}">
Access denied
</div>
</body>
</html>
Thymeleaf 国际化语法:
th:text="#{app.title}":从 MessageSource 读取 key 为app.title的消息th:text="${welcomeMsg}":读取 Controller 放入 Model 的属性(已在 Controller 中完成国际化)?lang=zh_CN和?lang=en_US:点击后 LocaleChangeInterceptor 拦截,修改 Cookie 中的语言设置,页面刷新后自动加载对应资源文件
李眉的部署检查
# 检查资源文件是否正确打包到 classpath
cd target/classes
ls messages*.properties
# 应输出:
# messages.properties
# messages_zh_CN.properties
李眉的常见问题排查:
- 中文乱码 → 检查
messageSource.setDefaultEncoding("UTF-8")是否配置 - 切换语言无效 → 检查 Cookie 是否被浏览器禁用,或 LocaleChangeInterceptor 是否注册
- key 找不到 → 检查
setUseCodeAsDefaultMessage(true),若设为 false 会抛NoSuchMessageException
易错场景与面试考点
误区一:资源文件使用 ISO-8859-1 编码存储中文
错误做法:在旧版 Spring 或某些 IDE 默认设置中,properties 文件用 ISO-8859-1 编码,中文需要写成 Unicode 转义序列:
# 错误:手动转义,维护困难
app.title=\u5458\u5DE5\u7BA1\u7406\u7CFB\u7EDF
纠正:Spring 5 和 Spring Boot 2.x 支持 UTF-8 编码的 properties 文件:
messageSource.setDefaultEncoding("UTF-8");
然后直接用 UTF-8 保存文件:
# 正确:直接写中文
app.title=员工管理系统
注意:如果项目需要兼容旧版 Spring(4.x 及以下),仍需使用 native2ascii 工具转换,或改用 YAML 格式的资源文件(Spring Boot 支持 messages.yml)。
误区二:LocaleResolver 和 LocaleChangeInterceptor 混用不当
错误配置:
@Bean
public LocaleResolver localeResolver() {
return new AcceptHeaderLocaleResolver(); // 只读请求头
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LocaleChangeInterceptor()); // 试图通过 URL 参数切换
}
问题:AcceptHeaderLocaleResolver 不支持 setLocale() 方法,LocaleChangeInterceptor 调用时会抛 UnsupportedOperationException。
纠正:如果要用 ?lang=xx 切换语言,必须使用支持写操作的 LocaleResolver:
@Bean
public LocaleResolver localeResolver() {
return new CookieLocaleResolver(); // ✅ 支持 setLocale()
}
误区三:在 REST API 中错误地使用 SessionLocaleResolver
错误场景:前后端分离项目中,前端每次请求都是独立的 AJAX 调用,没有会话概念。此时使用 SessionLocaleResolver 会导致语言切换只在某个 AJAX 请求中生效,下次请求又恢复默认。
纠正:前后端分离项目应使用 AcceptHeaderLocaleResolver(前端在请求头中携带 Accept-Language),或让前端在每次请求中携带语言参数,由自定义 LocaleResolver 解析。
面试高频:Spring MVC 国际化的核心组件是什么?它们如何协作?
标准回答:
Spring MVC 国际化由 LocaleResolver 和 MessageSource 两个核心组件协作完成:
LocaleResolver 负责"判断用户用什么语言"。它从 HTTP 请求中提取 Locale 信息——可能是请求头(AcceptHeaderLocaleResolver)、Session(SessionLocaleResolver)、Cookie(CookieLocaleResolver)或 URL 参数(配合 LocaleChangeInterceptor)。
MessageSource 负责"按 key 取对应语言的文本"。它管理一组
messages_*.properties资源文件,根据传入的 key 和 Locale,返回匹配的翻译文本。协作流程:DispatcherServlet 收到请求后,先调用 LocaleResolver 获取 Locale;Controller 或视图模板通过
#{key}或messageSource.getMessage(key, args, locale)获取翻译;MessageSource 根据 Locale 加载对应的 properties 文件,返回实际文本。
小结
国际化(i18n)不是简单的"翻译界面文字",而是一套从请求中识别语言环境、按 key 加载对应资源、在 Controller 和视图中统一使用的完整机制。Spring MVC 通过 LocaleResolver 解析语言偏好,通过 MessageSource 管理多语言资源文件,两者在 DispatcherServlet 的调度下无缝协作。
实现国际化时,必须注意三个关键点:选择合适的 LocaleResolver(AcceptHeader / Session / Cookie)、资源文件用 UTF-8 编码、兜底文件 messages.properties 必须存在。同时要避免在前后端分离项目中误用 SessionLocaleResolver,以及 AcceptHeaderLocaleResolver 与 LocaleChangeInterceptor 的不兼容配置。
本章与全局的关系:本章讲解了 Spring MVC 的多语言基础设施。下一章"拦截器机制"将深入讲解 LocaleChangeInterceptor 的工作原理——它本质上就是一个 HandlerInterceptor,在请求到达 Controller 之前修改当前 Locale。