副标题: 让你的应用说遍全球语言!从Hello到你好到こんにちは!
🎬 开场白:你的应用会说几国语言?
嘿,朋友!👋 想象一下这个场景:
美国用户打开你的网站:
"Welcome to our website!"
中国用户打开同样的网站:
"欢迎来到我们的网站!"
日本用户打开:
"私たちのウェブサイトへようこそ!"
同一个应用,不同的语言!🎉
这就是**国际化(i18n)**的魔力!
小知识:为什么叫i18n?因为internationalization首字母i和尾字母n之间有18个字母!😄
今天,我们就来揭秘Spring如何实现这个魔法!✨
🤔 什么是国际化(i18n)?
官方定义(别睡着😴)
国际化(Internationalization,简称i18n)是指设计和开发应用程序,使其能够适应不同语言和地区,无需修改代码即可支持多种语言。
人话翻译(醒醒!👀)
i18n = 让你的应用变成"变色龙" 🦎
没有i18n的应用(硬编码):
System.out.println("Welcome!"); // 只能说英语
有i18n的应用(智能切换):
System.out.println(getMessage("welcome"));
- 英语环境 → Welcome!
- 中文环境 → 欢迎!
- 日语环境 → ようこそ!
核心概念:
- 📝 i18n(国际化):让应用支持多语言
- 🌍 L10n(本地化):为特定地区定制内容
- 🗣️ Locale(语言环境):zh_CN、en_US、ja_JP
🏗️ Spring i18n架构
核心组件
┌─────────────────────────────────────┐
│ 应用程序 │
│ getMessage("welcome", locale) │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ MessageSource(消息源) │
│ - ResourceBundleMessageSource │
│ - ReloadableResourceBundleMessageSource │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ LocaleResolver(语言解析器) │
│ - AcceptHeaderLocaleResolver │
│ - CookieLocaleResolver │
│ - SessionLocaleResolver │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 资源文件(.properties) │
│ - messages_zh_CN.properties │
│ - messages_en_US.properties │
│ - messages_ja_JP.properties │
└─────────────────────────────────────┘
三大核心接口
- MessageSource:消息源,提供国际化消息
- LocaleResolver:解析当前用户的语言环境
- LocaleChangeInterceptor:拦截器,处理语言切换
🎨 生活化比喻:国际酒店前台
场景:五星级国际酒店
没有i18n(单语言前台)
客人(中国人):"你好!"
前台(只会英语):"Sorry, I don't understand."
客人:😓 尴尬了...
有i18n(智能前台)
客人走进来...
前台系统自动识别:
- 看护照 → 中国人 → 切换中文模式
- 用中文问候:"您好,欢迎光临!"
客人换成日本人:
- 系统识别 → 日本人 → 切换日语模式
- 用日语问候:"いらっしゃいませ!"
客人换成美国人:
- 系统识别 → 美国人 → 切换英语模式
- 用英语问候:"Welcome!"
同一个前台,智能切换语言!👏
Spring的i18n就是这个"智能前台系统"!
💻 实战案例一:基础配置
Step 1:创建资源文件
src/main/resources/
├── i18n/
│ ├── messages.properties # 默认(英文)
│ ├── messages_zh_CN.properties # 简体中文
│ ├── messages_en_US.properties # 美式英语
│ └── messages_ja_JP.properties # 日语
messages.properties(默认)
# 通用
app.name=My Application
app.welcome=Welcome!
app.goodbye=Goodbye!
# 用户相关
user.login.success=Login successful!
user.login.fail=Invalid username or password!
user.register.success=Registration successful!
# 验证消息
validation.email.invalid=Invalid email format
validation.password.short=Password must be at least 6 characters
validation.required={0} is required
messages_zh_CN.properties(中文)
# 通用
app.name=我的应用
app.welcome=欢迎!
app.goodbye=再见!
# 用户相关
user.login.success=登录成功!
user.login.fail=用户名或密码错误!
user.register.success=注册成功!
# 验证消息
validation.email.invalid=邮箱格式不正确
validation.password.short=密码至少6个字符
validation.required={0}不能为空
messages_ja_JP.properties(日语)
# 通用
app.name=私のアプリ
app.welcome=ようこそ!
app.goodbye=さようなら!
# 用户相关
user.login.success=ログイン成功!
user.login.fail=ユーザー名またはパスワードが無効です!
user.register.success=登録成功!
# 验证消息
validation.email.invalid=メール形式が無効です
validation.password.short=パスワードは6文字以上である必要があります
validation.required={0}は必須です
Step 2:配置MessageSource
@Configuration
public class I18nConfig {
/**
* 消息源配置
*/
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource =
new ResourceBundleMessageSource();
// 设置资源文件基础名
messageSource.setBasenames("i18n/messages");
// 设置默认编码
messageSource.setDefaultEncoding("UTF-8");
// 设置缓存时间(秒),-1表示永久缓存
messageSource.setCacheSeconds(3600);
// 找不到key时不抛异常,返回key本身
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
/**
* LocaleResolver配置(解析语言环境)
*/
@Bean
public LocaleResolver localeResolver() {
// 方式1:从Session中获取Locale
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
/**
* 语言切换拦截器
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
// 通过参数lang切换语言:?lang=zh_CN
interceptor.setParamName("lang");
return interceptor;
}
/**
* 注册拦截器
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LocaleChangeInterceptor localeChangeInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor);
}
}
}
Step 3:使用MessageSource
@RestController
@RequestMapping("/api")
public class I18nController {
@Autowired
private MessageSource messageSource;
/**
* 自动获取当前Locale
*/
@GetMapping("/welcome")
public String welcome(Locale locale) {
String message = messageSource.getMessage(
"app.welcome", // key
null, // 参数
locale // 语言环境
);
return message;
}
/**
* 带参数的消息
*/
@GetMapping("/greeting")
public String greeting(@RequestParam String name, Locale locale) {
// messages.properties: greeting=Hello, {0}!
String message = messageSource.getMessage(
"greeting",
new Object[]{name}, // 参数数组
locale
);
return message; // "Hello, Zhang San!"
}
/**
* 手动指定Locale
*/
@GetMapping("/test")
public Map<String, String> test() {
Map<String, String> result = new HashMap<>();
// 中文
result.put("zh_CN", messageSource.getMessage(
"app.welcome", null, Locale.SIMPLIFIED_CHINESE
));
// 英文
result.put("en_US", messageSource.getMessage(
"app.welcome", null, Locale.US
));
// 日语
result.put("ja_JP", messageSource.getMessage(
"app.welcome", null, Locale.JAPAN
));
return result;
}
}
Step 4:测试效果
# 默认中文
curl http://localhost:8080/api/welcome
# 输出:欢迎!
# 切换到英文
curl http://localhost:8080/api/welcome?lang=en_US
# 输出:Welcome!
# 切换到日语
curl http://localhost:8080/api/welcome?lang=ja_JP
# 输出:ようこそ!
# 带参数的消息
curl http://localhost:8080/api/greeting?name=Zhang%20San&lang=en_US
# 输出:Hello, Zhang San!
💻 实战案例二:四种LocaleResolver
1. AcceptHeaderLocaleResolver(请求头)
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
工作原理:从HTTP请求头Accept-Language中获取语言。
curl -H "Accept-Language: zh-CN" http://localhost:8080/api/welcome
# 输出:欢迎!
curl -H "Accept-Language: en-US" http://localhost:8080/api/welcome
# 输出:Welcome!
特点:
- ✅ 自动根据浏览器语言切换
- ❌ 用户无法手动切换
- ✅ 无状态,适合RESTful API
2. SessionLocaleResolver(Session)
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
工作原理:将Locale存储在Session中。
@GetMapping("/changeLanguage")
public String changeLanguage(@RequestParam String lang, HttpServletRequest request) {
LocaleResolver resolver = RequestContextUtils.getLocaleResolver(request);
if (resolver instanceof SessionLocaleResolver) {
Locale locale = StringUtils.parseLocaleString(lang);
((SessionLocaleResolver) resolver).setLocale(request, null, locale);
}
return "Language changed to: " + lang;
}
特点:
- ✅ 用户可以手动切换
- ✅ Session期间保持语言设置
- ❌ 需要Session支持
3. CookieLocaleResolver(Cookie)
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setCookieName("language");
resolver.setCookieMaxAge(3600); // 1小时
return resolver;
}
工作原理:将Locale存储在Cookie中。
特点:
- ✅ 跨Session保持语言设置
- ✅ 关闭浏览器后仍保持
- ❌ 依赖Cookie
4. FixedLocaleResolver(固定)
@Bean
public LocaleResolver localeResolver() {
FixedLocaleResolver resolver = new FixedLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
工作原理:固定语言,不可切换。
特点:
- ✅ 简单
- ❌ 无法切换语言
- 🎯 适合单语言应用
💻 实战案例三:Thymeleaf模板集成
application.yml
spring:
messages:
basename: i18n/messages
encoding: UTF-8
cache-duration: 3600
Controller
@Controller
public class WebController {
@GetMapping("/")
public String index(Model model, Locale locale) {
model.addAttribute("currentLocale", locale.toString());
return "index";
}
}
index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title th:text="#{app.name}">My App</title>
</head>
<body>
<h1 th:text="#{app.welcome}">Welcome</h1>
<!-- 带参数的消息 -->
<p th:text="#{user.greeting(${username})}">Hello, User!</p>
<!-- 语言切换链接 -->
<div>
<a th:href="@{/?lang=zh_CN}">中文</a> |
<a th:href="@{/?lang=en_US}">English</a> |
<a th:href="@{/?lang=ja_JP}">日本語</a>
</div>
<!-- 显示当前语言 -->
<p>Current Locale: <span th:text="${currentLocale}"></span></p>
</body>
</html>
💻 实战案例四:RESTful API国际化
方案1:通过请求头
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private MessageSource messageSource;
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request,
@RequestHeader(value = "Accept-Language", defaultValue = "zh-CN")
String lang) {
Locale locale = Locale.forLanguageTag(lang);
// 验证失败
if (!validate(request)) {
String message = messageSource.getMessage(
"user.login.fail", null, locale
);
return Result.fail(message);
}
// 登录成功
String message = messageSource.getMessage(
"user.login.success", null, locale
);
return Result.ok(message);
}
}
方案2:通过URL参数
@GetMapping("/profile")
public Result profile(@RequestParam(defaultValue = "zh_CN") String lang) {
Locale locale = StringUtils.parseLocaleString(lang);
String message = messageSource.getMessage(
"user.profile.title", null, locale
);
return Result.ok(message);
}
方案3:自定义响应包装
@Data
@AllArgsConstructor
public class I18nResult<T> {
private int code;
private String message;
private T data;
public static <T> I18nResult<T> ok(String messageKey, Locale locale) {
MessageSource ms = SpringContextHolder.getBean(MessageSource.class);
String message = ms.getMessage(messageKey, null, locale);
return new I18nResult<>(200, message, null);
}
public static <T> I18nResult<T> fail(String messageKey, Locale locale) {
MessageSource ms = SpringContextHolder.getBean(MessageSource.class);
String message = ms.getMessage(messageKey, null, locale);
return new I18nResult<>(500, message, null);
}
}
// 使用
@PostMapping("/register")
public I18nResult register(@RequestBody User user, Locale locale) {
// 业务逻辑...
return I18nResult.ok("user.register.success", locale);
}
💻 实战案例五:参数化消息
资源文件
# messages_zh_CN.properties
user.welcome=欢迎,{0}!
order.created=订单{0}创建成功,总金额:{1}元
email.sent={0}封邮件已发送到{1}
file.uploaded={0}上传成功,大小:{1}MB
使用
@Service
public class NotificationService {
@Autowired
private MessageSource messageSource;
public String welcomeUser(String username, Locale locale) {
return messageSource.getMessage(
"user.welcome",
new Object[]{username},
locale
);
// 中文:欢迎,张三!
// 英文:Welcome, Zhang San!
}
public String orderCreated(String orderId, BigDecimal amount, Locale locale) {
return messageSource.getMessage(
"order.created",
new Object[]{orderId, amount},
locale
);
// 中文:订单ORD20250123创建成功,总金额:99.00元
}
public String emailSent(int count, String email, Locale locale) {
return messageSource.getMessage(
"email.sent",
new Object[]{count, email},
locale
);
// 中文:3封邮件已发送到zhangsan@example.com
}
}
💻 实战案例六:动态切换语言
前端代码
<!-- 语言选择器 -->
<select id="languageSelector" onchange="changeLanguage()">
<option value="zh_CN">简体中文</option>
<option value="en_US">English</option>
<option value="ja_JP">日本語</option>
</select>
<script>
function changeLanguage() {
const lang = document.getElementById('languageSelector').value;
// 方式1:URL参数
window.location.href = `?lang=${lang}`;
// 方式2:AJAX请求
fetch(`/api/changeLanguage?lang=${lang}`, {
method: 'POST'
}).then(response => {
window.location.reload();
});
}
</script>
后端代码
@RestController
public class LanguageController {
@PostMapping("/api/changeLanguage")
public Result changeLanguage(@RequestParam String lang,
HttpServletRequest request,
HttpServletResponse response) {
Locale locale = StringUtils.parseLocaleString(lang);
// 获取LocaleResolver
LocaleResolver resolver = RequestContextUtils.getLocaleResolver(request);
// 设置新的Locale
resolver.setLocale(request, response, locale);
return Result.ok("Language changed to: " + lang);
}
@GetMapping("/api/currentLanguage")
public Result getCurrentLanguage(Locale locale) {
return Result.ok(locale.toString());
}
}
🎯 高级技巧
技巧1:自定义MessageSource
@Configuration
public class CustomMessageSourceConfig {
@Bean
public MessageSource messageSource() {
return new DatabaseMessageSource();
}
}
/**
* 从数据库读取国际化消息
*/
public class DatabaseMessageSource extends AbstractMessageSource {
@Autowired
private I18nMessageRepository repository;
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
// 从数据库查询
I18nMessage message = repository.findByCodeAndLocale(
code,
locale.toString()
);
if (message != null) {
return new MessageFormat(message.getText(), locale);
}
return null;
}
}
// 数据库表结构
@Entity
@Table(name = "i18n_messages")
public class I18nMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code; // 消息key
private String locale; // 语言环境
private String text; // 消息内容
// Getter和Setter...
}
技巧2:支持热更新
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
// 设置缓存时间(秒)
// 修改properties文件后,最多1秒后生效
messageSource.setCacheSeconds(1);
return messageSource;
}
技巧3:多个资源文件
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource =
new ResourceBundleMessageSource();
// 支持多个资源文件
messageSource.setBasenames(
"i18n/messages", // 通用消息
"i18n/validation", // 验证消息
"i18n/errors", // 错误消息
"i18n/business" // 业务消息
);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
技巧4:Spring Boot简化配置
# application.yml
spring:
messages:
basename: i18n/messages,i18n/validation,i18n/errors
encoding: UTF-8
cache-duration: 3600
fallback-to-system-locale: true
// 自动配置,无需手动创建MessageSource Bean
@RestController
public class SimpleController {
@Autowired
private MessageSource messageSource; // 自动注入
@GetMapping("/hello")
public String hello(Locale locale) {
return messageSource.getMessage("app.welcome", null, locale);
}
}
📊 LocaleResolver选择指南
| LocaleResolver | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| AcceptHeaderLocaleResolver | RESTful API | 自动识别浏览器语言 | 无法手动切换 |
| SessionLocaleResolver | 传统Web应用 | 用户可切换,Session内保持 | 需要Session |
| CookieLocaleResolver | 需要持久化语言设置 | 跨Session保持 | 依赖Cookie |
| FixedLocaleResolver | 单语言应用 | 简单 | 不支持多语言 |
推荐方案
/**
* 组合方案:优先Cookie,其次Header
*/
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setCookieName("APP_LANGUAGE");
resolver.setCookieMaxAge(7 * 24 * 3600); // 7天
return resolver;
}
⚠️ 常见坑点与避坑指南
坑点1:编码问题
# ❌ 错误:中文乱码
user.welcome=欢迎! # ISO-8859-1编码会乱码
# ✅ 方案1:使用Unicode转义
user.welcome=\u6B22\u8FCE\uFF01
# ✅ 方案2:设置编码(推荐)
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
ms.setBasenames("i18n/messages");
ms.setDefaultEncoding("UTF-8"); // 设置编码!
return ms;
}
坑点2:找不到资源文件
// ❌ 错误:路径写错
messageSource.setBasenames("messages"); // 找不到
// ✅ 正确
messageSource.setBasenames("i18n/messages"); // 基于classpath
// ✅ 或者使用绝对路径
messageSource.setBasenames("classpath:i18n/messages");
坑点3:key不存在
// ❌ 错误:找不到key时抛异常
String msg = messageSource.getMessage("nonexist.key", null, locale);
// 抛出:NoSuchMessageException
// ✅ 正确:设置返回key本身
messageSource.setUseCodeAsDefaultMessage(true);
String msg = messageSource.getMessage("nonexist.key", null, locale);
// 返回:"nonexist.key"
// ✅ 或者提供默认值
String msg = messageSource.getMessage(
"nonexist.key",
null,
"Default Message", // 默认值
locale
);
坑点4:参数顺序错误
# messages.properties
order.info=Order {0} created by {1}, amount: {2}
// ❌ 错误:参数顺序不对
messageSource.getMessage(
"order.info",
new Object[]{"张三", "ORD123", 99.00}, // 顺序错了
locale
);
// 输出:Order 张三 created by ORD123, amount: 99.00
// ✅ 正确
messageSource.getMessage(
"order.info",
new Object[]{"ORD123", "张三", 99.00},
locale
);
// 输出:Order ORD123 created by 张三, amount: 99.00
🎉 总结
核心要点
-
三大组件:
MessageSource:消息源LocaleResolver:语言解析器LocaleChangeInterceptor:语言切换拦截器
-
资源文件命名:
messages.properties # 默认 messages_{language}.properties # 语言 messages_{language}_{country}.properties # 语言+国家 -
常用LocaleResolver:
AcceptHeaderLocaleResolver:请求头SessionLocaleResolver:SessionCookieLocaleResolver:Cookie
-
Spring Boot自动配置:
spring.messages.basename: i18n/messages spring.messages.encoding: UTF-8 -
最佳实践:
- ✅ 统一资源文件位置
- ✅ 设置UTF-8编码
- ✅ 提供默认语言
- ✅ 使用参数化消息
- ✅ 考虑缓存策略
📚 参考资料
- Spring官方文档:Internationalization using MessageSource
- Java官方文档:Locale
- 《Spring实战》- Craig Walls
- Unicode转义工具:native2ascii
🎮 课后练习
练习1:多级回退
实现资源文件的多级回退:
zh_CN → zh → 默认
练习2:数据库国际化
实现从数据库读取国际化消息,支持在线编辑。
练习3:Excel导入导出
实现国际化消息的Excel批量导入导出功能。
💬 最后的话
国际化是让你的应用走向世界的第一步!🌍
虽然配置看起来有点繁琐,但一旦配置好,就能轻松支持全球用户。
记住这个公式:
好的i18n = 统一的资源文件 + 合适的LocaleResolver + 完善的翻译
现在,让你的应用说遍全球语言吧!🗣️✨
作者心声:我第一次做国际化时,把中文直接写在properties文件里,结果全是乱码😂。后来才知道要用UTF-8编码。希望这篇文章能帮你避开这些坑!
如果觉得有用,点赞收藏走一波!👍⭐
文档版本:v1.0
最后更新:2025-10-23
难度等级:⭐⭐⭐(中高级)