文章目录
- [MessageSource 国际化使用教程](#MessageSource 国际化使用教程)
-
- 写在前面
- [1. 先搞懂两个概念:Locale 和 ResourceBundle](#1. 先搞懂两个概念:Locale 和 ResourceBundle)
-
- [1.1 Locale ------ 语言和地区的"身份证"](#1.1 Locale —— 语言和地区的"身份证")
- [1.2 ResourceBundle ------ Java 原生的国际化方案](#1.2 ResourceBundle —— Java 原生的国际化方案)
- [2. 快速入门:5 分钟跑通第一个国际化示例](#2. 快速入门:5 分钟跑通第一个国际化示例)
-
- [2.1 创建 Spring Boot 项目](#2.1 创建 Spring Boot 项目)
- [2.2 创建资源文件](#2.2 创建资源文件)
- [2.3 注入 MessageSource 并使用](#2.3 注入 MessageSource 并使用)
- [3. MessageSource 的三种实现](#3. MessageSource 的三种实现)
-
- [3.1 ResourceBundleMessageSource](#3.1 ResourceBundleMessageSource)
- [3.2 ReloadableResourceBundleMessageSource](#3.2 ReloadableResourceBundleMessageSource)
- [3.3 StaticMessageSource](#3.3 StaticMessageSource)
- [3.4 三种实现对比与选型](#3.4 三种实现对比与选型)
- [4. 基础用法详解](#4. 基础用法详解)
-
- [4.1 资源文件命名规范与查找顺序](#4.1 资源文件命名规范与查找顺序)
- [4.2 getMessage 的四种调用方式](#4.2 getMessage 的四种调用方式)
- [4.3 带参数的消息格式化](#4.3 带参数的消息格式化)
- [4.4 找不到消息时的降级策略](#4.4 找不到消息时的降级策略)
- [5. 进阶特性](#5. 进阶特性)
-
- [5.1 消息嵌套:在一个消息中引用另一个消息](#5.1 消息嵌套:在一个消息中引用另一个消息)
- [5.2 复数与选择格式:ChoiceFormat](#5.2 复数与选择格式:ChoiceFormat)
- [5.3 热加载配置](#5.3 热加载配置)
- [6. Web 环境下的动态语言切换](#6. Web 环境下的动态语言切换)
-
- [6.1 LocaleResolver 的三种实现](#6.1 LocaleResolver 的三种实现)
- [6.2 LocaleChangeInterceptor:通过 URL 参数切换语言](#6.2 LocaleChangeInterceptor:通过 URL 参数切换语言)
- [6.3 在 Controller 和 Thymeleaf 中使用](#6.3 在 Controller 和 Thymeleaf 中使用)
- [7. Spring Boot 自动配置与自定义](#7. Spring Boot 自动配置与自定义)
-
- [7.1 spring.messages.* 配置项](#7.1 spring.messages.* 配置项)
- [7.2 多模块项目中的资源文件](#7.2 多模块项目中的资源文件)
- [7.3 在非 Spring 管理的 Bean 中获取 MessageSource](#7.3 在非 Spring 管理的 Bean 中获取 MessageSource)
- [8. 实战:搭建一个多语言后台系统](#8. 实战:搭建一个多语言后台系统)
-
- [8.1 需求说明](#8.1 需求说明)
- [8.2 组织资源文件结构](#8.2 组织资源文件结构)
- [8.3 封装统一的响应类](#8.3 封装统一的响应类)
- [8.4 在 Controller 中使用](#8.4 在 Controller 中使用)
- [8.5 常见问题与排错](#8.5 常见问题与排错)
- [9. 最佳实践总结](#9. 最佳实践总结)
-
- [9.1 资源文件管理](#9.1 资源文件管理)
- [9.2 代码层面](#9.2 代码层面)
- [9.3 性能建议](#9.3 性能建议)
- [10. 总结](#10. 总结)
MessageSource 国际化使用教程
写在前面
如果你做过面向海外用户的项目,一定遇到过这样的场景:同一个按钮,中文显示"提交",英文要显示"Submit",日文要显示"送信"......硬编码?那简直是灾难------改一个文案就要改代码、重新打包、重新部署。
Spring 的 MessageSource 就是为了解决这个问题而生的。它让你把所有文案从代码中剥离出来,放到独立的资源文件中,运行时根据用户的语言环境(Locale)自动匹配对应的文案。这篇教程会用最简单的方式,带你从零掌握 Spring MessageSource 的核心用法。
1. 先搞懂两个概念:Locale 和 ResourceBundle
在学 MessageSource 之前,需要先理解两个 Java 原生概念。
1.1 Locale ------ 语言和地区的"身份证"
java.util.Locale 用来标识一种语言+地区的组合,比如:
| Locale 实例 | 语言 | 地区 | 含义 |
|---|---|---|---|
Locale.CHINA |
zh | CN | 简体中文(中国) |
Locale.US |
en | US | 英语(美国) |
Locale.JAPAN |
ja | JP | 日语(日本) |
java
// 常用创建方式
Locale locale1 = Locale.CHINA; // 内置常量
Locale locale2 = new Locale("en", "US"); // 语言 + 国家
Locale locale3 = Locale.forLanguageTag("zh-CN"); // IETF BCP 47 标签
1.2 ResourceBundle ------ Java 原生的国际化方案
Java 原生提供了 ResourceBundle 来做国际化,但它有不少局限:
java
// 原生 ResourceBundle 用法
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.CHINA);
String greeting = bundle.getString("welcome"); // 你好
ResourceBundle 的痛点:
| 痛点 | 说明 |
|---|---|
| 不支持参数化 | 消息中的变量只能手动拼接,没有 {0} 占位符支持 |
| 不支持热加载 | 修改 properties 后必须重启应用 |
| 与 Spring 集成差 | 无法直接注入,无法在 Bean 中优雅使用 |
| 嵌套消息不支持 | 一个消息无法引用另一个消息 |
| 缓存不可控 | 默认缓存策略无法自定义 |
Spring 的 MessageSource 正是为了解决这些问题而设计的增强方案。
2. 快速入门:5 分钟跑通第一个国际化示例
2.1 创建 Spring Boot 项目
使用 Spring Initializr 创建一个 Spring Boot 项目,只需引入 spring-boot-starter 即可(Web 项目用 spring-boot-starter-web):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<groupId>spring-boot-starter-web</groupId>
</dependency>
注意 :Spring Boot 自动配置了
MessageSource,你不需要手动创建 Bean,开箱即用。
2.2 创建资源文件
在 src/main/resources/ 目录下创建以下文件:
messages.properties(默认资源文件,找不到匹配语言时的兜底):
properties
welcome=Welcome
user.login=Login
user.logout=Logout
messages_zh_CN.properties(简体中文):
properties
welcome=欢迎使用本系统
user.login=登录
user.logout=退出登录
messages_en_US.properties(美式英语):
properties
welcome=Welcome to the system
user.login=Login
user.logout=Sign Out
命名规则 :
messages是基名(basename),_zh_CN是 Locale 后缀。Spring 会按messages_zh_CN > messages_zh > messages的顺序查找。
2.3 注入 MessageSource 并使用
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Locale;
@RestController
public class HelloController {
@Autowired
private MessageSource messageSource;
@GetMapping("/hello")
public String hello() {
// 从 Spring 上下文中获取当前 Locale
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage("welcome", null, locale);
}
}
启动项目后:
- 浏览器默认发送
Accept-Language: zh-CN→ 返回欢迎使用本系统 - 用 Postman 设置
Accept-Language: en-US→ 返回Welcome to the system - 设置一个不存在的语言 → 回退到
messages.properties→ 返回Welcome
就这样,你的第一个国际化接口就完成了!
3. MessageSource 的三种实现
Spring 提供了三种 MessageSource 实现,适用于不同场景:
基于 JDK ResourceBundle
Spring 自实现
编程式
MessageSource 接口
ResourceBundleMessageSource
ReloadableResourceBundleMessageSource
StaticMessageSource
类路径资源, 不可热加载
支持热加载, 支持文件系统
硬编码消息, 主要用于测试
3.1 ResourceBundleMessageSource
最常用的实现,底层基于 JDK 的 ResourceBundle,只能读取类路径下的资源:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
@Configuration
public class I18nConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
// 设置基名(可以指定多个)
source.setBasenames("messages", "errors", "validation");
// 设置默认编码(解决中文乱码)
source.setDefaultEncoding("UTF-8");
return source;
}
}
3.2 ReloadableResourceBundleMessageSource
最推荐的实现,支持热加载------修改资源文件后无需重启应用即可生效:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
@Configuration
public class I18nConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
source.setBasenames("classpath:messages", "classpath:errors");
source.setDefaultEncoding("UTF-8");
// 每 5 秒检查一次资源文件是否有更新(开发环境推荐)
source.setCacheSeconds(5);
return source;
}
}
生产环境建议 :
setCacheSeconds(-1)表示永久缓存(默认值),避免频繁检查文件。
3.3 StaticMessageSource
编程式添加消息,主要用于测试:
java
import org.springframework.context.support.StaticMessageSource;
import java.util.Locale;
StaticMessageSource source = new StaticMessageSource();
source.addMessage("hello", Locale.CHINA, "你好");
source.addMessage("hello", Locale.US, "Hello");
System.out.println(source.getMessage("hello", null, Locale.CHINA)); // 你好
3.4 三种实现对比与选型
| 特性 | ResourceBundleMessageSource | ReloadableResourceBundleMessageSource | StaticMessageSource |
|---|---|---|---|
| 资源位置 | 仅类路径 | 类路径 + 文件系统 | 编程式 |
| 热加载 | 不支持 | 支持 | N/A |
| 编码控制 | 需通过 file.encoding | setDefaultEncoding() | N/A |
| 性能 | 高(JDK 缓存) | 可控(自定义缓存策略) | 最高(内存直取) |
| 适用场景 | 生产环境 | 开发环境 | 单元测试 |
选型建议:开发阶段用 ReloadableResourceBundleMessageSource(方便调试),生产环境也用它(设置永久缓存即可),一般不需要 ResourceBundleMessageSource。
4. 基础用法详解
4.1 资源文件命名规范与查找顺序
资源文件的命名必须遵循 basename_language_country_variant.properties 的格式。Spring 查找消息时的优先级从高到低:
messages_zh_CN.properties → 精确匹配(语言+国家)
messages_zh.properties → 仅匹配语言
messages.properties → 默认兜底
举个例子,当 Locale 为 zh_CN 时,查找 welcome 键的顺序:
1. messages_zh_CN.properties 中找 welcome → 找到就返回
2. messages_zh.properties 中找 welcome → 找到就返回
3. messages.properties 中找 welcome → 找到就返回
4. 抛出 NoSuchMessageException → 都没找到则报错
4.2 getMessage 的四种调用方式
MessageSource 接口提供了三个 getMessage 重载方法:
java
// 方式一:指定 Locale
String msg1 = messageSource.getMessage("welcome", null, Locale.CHINA);
// 方式二:指定 Locale + 默认值(找不到键时不报错,返回默认值)
String msg2 = messageSource.getMessage("not.exist", null, "默认欢迎语", Locale.CHINA);
// 方式三:带参数的消息格式化
String msg3 = messageSource.getMessage("user.greeting", new Object[]{"张三"}, Locale.CHINA);
// 方式四:通过 MessageSourceResolvable 解析(Spring 内部大量使用,如 Validator 错误消息)
4.3 带参数的消息格式化
这是 MessageSource 相比原生 ResourceBundle 的一大优势------支持 {0}、{1} 占位符:
资源文件:
properties
# messages_zh_CN.properties
user.greeting=你好,{0}!你今天有 {1} 条新消息。
order.status=订单 {0} 的状态已更新为 {1}。
代码:
java
String msg = messageSource.getMessage(
"user.greeting",
new Object[]{"张三", 5},
Locale.CHINA
);
// 结果:你好,张三!你今天有 5 条新消息。
4.4 找不到消息时的降级策略
有三种方式处理"键不存在"的情况:
java
// 策略一:提供默认值(推荐)
String msg = messageSource.getMessage("not.exist", null, "默认文案", Locale.CHINA);
// 返回:默认文案
// 策略二:使用 NoSuchMessageException 的 try-catch
try {
String msg = messageSource.getMessage("not.exist", null, Locale.CHINA);
} catch (NoSuchMessageException e) {
// 处理找不到的情况
}
// 策略三:确保 messages.properties 中有默认值兜底
// 只要默认资源文件中定义了该键,任何语言都不会抛异常
5. 进阶特性
5.1 消息嵌套:在一个消息中引用另一个消息
你可以在一个消息中嵌套引用另一个消息键,语法是 {0, message, 引用的键名}:
资源文件:
properties
# messages_zh_CN.properties
app.name=超级管理系统
welcome=欢迎来到 {0, message, app.name}
代码:
java
String msg = messageSource.getMessage("welcome", null, Locale.CHINA);
// 结果:欢迎来到 超级管理系统
原理 :当
{0}位置使用了message格式类型时,Spring 会把第三个参数(app.name)作为消息键去查找,然后用查到的值替换占位符。
5.2 复数与选择格式:ChoiceFormat
不同语言对复数有不同的处理方式。比如英文中 "1 item" vs "2 items",中文则没有这个区分。ChoiceFormat 可以帮你处理这类问题:
资源文件:
properties
# messages_en_US.properties
cart.items={0,choice,0#Your cart is empty|1#You have 1 item|1<You have {0} items}
# messages_zh_CN.properties
cart.items=您的购物车中有 {0} 件商品
代码:
java
// 英文环境
messageSource.getMessage("cart.items", new Object[]{0}, Locale.US); // Your cart is empty
messageSource.getMessage("cart.items", new Object[]{1}, Locale.US); // You have 1 item
messageSource.getMessage("cart.items", new Object[]{5}, Locale.US); // You have 5 items
// 中文环境
messageSource.getMessage("cart.items", new Object[]{5}, Locale.CHINA); // 您的购物车中有 5 件商品
ChoiceFormat 语法 :
limit#text表示等于 limit 时显示 text;limit<text表示大于 limit 时显示 text。
5.3 热加载配置
使用 ReloadableResourceBundleMessageSource 时,可以通过缓存策略控制热加载行为:
java
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
source.setBasenames("classpath:messages");
source.setDefaultEncoding("UTF-8");
// 缓存策略
source.setCacheSeconds(5); // 每 5 秒检查一次(开发环境)
// source.setCacheSeconds(-1); // 永久缓存(生产环境,默认值)
return source;
}
| 缓存时间 | 行为 | 适用环境 |
|---|---|---|
-1(默认) |
永久缓存,不检查文件更新 | 生产环境 |
0 |
每次请求都重新加载 | 不推荐(性能差) |
> 0 |
每隔 N 秒检查一次 | 开发环境 |
6. Web 环境下的动态语言切换
在 Web 应用中,用户的语言偏好通常来自三个地方:请求头、Cookie/Session、URL 参数。Spring 通过 LocaleResolver 来统一管理。
6.1 LocaleResolver 的三种实现
从请求头读取
从 Cookie 读取
从 Session 读取
LocaleResolver 接口
AcceptHeaderLocaleResolver
CookieLocaleResolver
SessionLocaleResolver
Accept-Language
设置 cookie 名即可持久化
会话级别, 关浏览器就没了
默认行为 :Spring Boot 自动配置了 AcceptHeaderLocaleResolver,它从 HTTP 请求头 Accept-Language 中读取语言偏好。
自定义 LocaleResolver(推荐 Cookie 方案,持久化且无需登录):
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
@Configuration
public class I18nConfig {
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver();
resolver.setDefaultLocale(Locale.CHINA); // 默认中文
resolver.setCookieName("lang"); // Cookie 名称
resolver.setCookieMaxAge(365 * 24 * 60 * 60); // Cookie 有效期 1 年
return resolver;
}
}
6.2 LocaleChangeInterceptor:通过 URL 参数切换语言
配置一个拦截器,让用户通过 ?lang=en_US 这样的方式切换语言:
java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // URL 参数名,默认是 "locale"
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}
现在你可以这样切换语言:
GET /hello?lang=zh_CN → 返回中文
GET /hello?lang=en_US → 返回英文
GET /hello?lang=ja_JP → 返回日文
6.3 在 Controller 和 Thymeleaf 中使用
在 Controller 中使用:
java
@GetMapping("/greeting")
public String greeting(Model model) {
// 方式一:直接注入 MessageSource
Locale locale = LocaleContextHolder.getLocale();
String welcome = messageSource.getMessage("welcome", null, locale);
model.addAttribute("welcome", welcome);
return "greeting";
}
在 Thymeleaf 模板中使用(更简洁):
Thymeleaf 原生支持 #{} 表达式来读取国际化消息:
html
<!-- 直接引用消息键 -->
<h1 th:text="#{welcome}">Welcome</h1>
<!-- 带参数的消息 -->
<p th:text="#{user.greeting(${userName}, ${msgCount})}">Hello</p>
提示 :在 Thymeleaf 中用
#{key}比${message}更简洁,推荐优先使用。
7. Spring Boot 自动配置与自定义
7.1 spring.messages.* 配置项
Spring Boot 在 application.yml 中提供了以下国际化配置项:
yaml
spring:
messages:
basename: messages,errors,validation # 资源文件基名,逗号分隔,默认 messages
encoding: UTF-8 # 编码,默认 UTF-8
fallback-to-system-locale: true # 找不到匹配 Locale 时是否回退到系统 Locale
use-code-as-default-message: false # 找不到消息时是否用键名本身作为默认值
cache-duration: -1 # 缓存秒数,-1 表示永久缓存
always-use-message-format: false # 是否总是使用 MessageFormat 解析
configurable: false # 是否暴露 MessageSource 为 ConfigurableMessageSource
几个容易踩坑的配置:
| 配置项 | 踩坑场景 | 建议 |
|---|---|---|
fallback-to-system-locale |
服务器是英文环境,你期望没匹配时走默认文件,结果走了英文 | 设为 false |
use-code-as-default-message |
设为 true 后拼写错误不会报错,返回键名本身 |
开发环境可设 true,生产环境设 false |
cache-duration |
开发时改了 properties 不生效 | 开发环境设为 5 |
7.2 多模块项目中的资源文件
在多模块项目中,每个模块可以有自己的资源文件。通过设置多个 basename 来加载:
yaml
spring:
messages:
basename: i18n/messages,i18n/errors,i18n/validation
对应的目录结构:
src/main/resources/
├── i18n/
│ ├── messages.properties
│ ├── messages_zh_CN.properties
│ ├── messages_en_US.properties
│ ├── errors.properties
│ ├── errors_zh_CN.properties
│ └── validation.properties
7.3 在非 Spring 管理的 Bean 中获取 MessageSource
有些工具类不在 Spring 容器中,无法通过 @Autowired 注入。解决方法是提供一个静态工具类:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Locale;
@Component
public class I18nUtil {
@Autowired
private MessageSource messageSource;
private static MessageSource staticMessageSource;
@PostConstruct
public void init() {
staticMessageSource = this.messageSource;
}
/**
* 获取国际化消息
*/
public static String getMessage(String code, Object... args) {
return staticMessageSource.getMessage(code, args, LocaleContextHolder.getLocale());
}
/**
* 获取国际化消息(指定 Locale)
*/
public static String getMessage(String code, Locale locale, Object... args) {
return staticMessageSource.getMessage(code, args, locale);
}
/**
* 获取国际化消息(带默认值)
*/
public static String getMessage(String code, String defaultMessage, Locale locale, Object... args) {
return staticMessageSource.getMessage(code, args, defaultMessage, locale);
}
}
使用方式:
java
// 在任何地方,即使不在 Spring 容器中
String welcome = I18nUtil.getMessage("welcome");
String greeting = I18nUtil.getMessage("user.greeting", "张三", 5);
8. 实战:搭建一个多语言后台系统
8.1 需求说明
假设我们要做一个多语言后台管理系统,需要国际化的内容包括:
- 登录页面的文案(用户名、密码、登录按钮等)
- 菜单名称
- 操作提示信息(成功、失败、警告等)
- 数据校验的错误消息
8.2 组织资源文件结构
src/main/resources/
├── i18n/
│ ├── menus.properties / menus_zh_CN.properties / menus_en_US.properties
│ ├── messages.properties / messages_zh_CN.properties / messages_en_US.properties
│ └── validation.properties / validation_zh_CN.properties / validation_en_US.properties
messages_zh_CN.properties:
properties
# 登录相关
login.title=系统登录
login.username=用户名
login.password=密码
login.button=登 录
login.success=登录成功
login.fail=用户名或密码错误
login.welcome=欢迎回来,{0}!
# 通用操作
operation.success=操作成功
operation.fail=操作失败
operation.confirm=确定要{0}吗?
operation.delete=删除
operation.save=保存
messages_en_US.properties:
properties
# Login
login.title=System Login
login.username=Username
login.password=Password
login.button=Sign In
login.success=Login successful
login.fail=Invalid username or password
login.welcome=Welcome back, {0}!
# Common operations
operation.success=Operation successful
operation.fail=Operation failed
operation.confirm=Are you sure to {0}?
operation.delete=Delete
operation.save=Save
8.3 封装统一的响应类
让 API 响应中的消息也支持国际化:
java
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import java.util.Locale;
public class Result<T> {
private int code;
private String message;
private T data;
private Result(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功响应(带国际化消息)
*/
public static <T> Result<T> success(MessageSource messageSource, String msgKey, Object... args) {
Locale locale = LocaleContextHolder.getLocale();
String message = messageSource.getMessage(msgKey, args, locale);
return new Result<>(200, message, null);
}
/**
* 成功响应(带数据和国际化消息)
*/
public static <T> Result<T> success(MessageSource messageSource, String msgKey, T data, Object... args) {
Locale locale = LocaleContextHolder.getLocale();
String message = messageSource.getMessage(msgKey, args, locale);
return new Result<>(200, message, data);
}
/**
* 失败响应
*/
public static <T> Result<T> fail(MessageSource messageSource, String msgKey, Object... args) {
Locale locale = LocaleContextHolder.getLocale();
String message = messageSource.getMessage(msgKey, args, locale);
return new Result<>(500, message, null);
}
// getter 省略...
}
8.4 在 Controller 中使用
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private MessageSource messageSource;
@PostMapping("/login")
public Result<?> login(@RequestBody LoginRequest request) {
User user = userService.login(request.getUsername(), request.getPassword());
if (user != null) {
// "登录成功,欢迎回来,张三!"
return Result.success(messageSource, "login.welcome", user.getName());
}
// "用户名或密码错误"
return Result.fail(messageSource, "login.fail");
}
@DeleteMapping("/{id}")
public Result<?> delete(@PathVariable Long id) {
userService.deleteById(id);
return Result.success(messageSource, "operation.success");
}
}
8.5 常见问题与排错
问题 1:中文乱码
原因:资源文件编码不是 UTF-8,或未配置 spring.messages.encoding=UTF-8。
解决:
yaml
spring:
messages:
encoding: UTF-8
同时确保 IDE 中 properties 文件的编码也是 UTF-8(IntelliJ IDEA:Settings → Editor → File Encodings → 勾选 "Transparent native-to-ascii conversion")。
问题 2:NoSuchMessageException
原因:资源文件中没有对应的键,且没有提供默认值。
解决:确保 messages.properties(默认文件)中定义了所有键作为兜底。
问题 3:参数 {0} 没有被替换
原因:getMessage 的第二个参数传了 null,或者消息键中的占位符格式不对。
解决:确保占位符是 {0}、{1} 格式(不是 ${0} 或 %s),并正确传入参数数组。
问题 4:修改 properties 后不生效
原因:默认使用 ResourceBundleMessageSource,不支持热加载。
解决:切换到 ReloadableResourceBundleMessageSource 并设置 cacheSeconds。
9. 最佳实践总结
9.1 资源文件管理
| 实践 | 说明 |
|---|---|
| 按模块拆分 | 不要把所有消息都放在一个文件里,按 messages、errors、validation 等模块拆分 |
| 默认文件兜底 | messages.properties 必须包含所有键,作为最后防线 |
| 键名规范 | 用 模块.操作.含义 格式,如 user.login.success,避免随意命名 |
| 统一编码 | 所有 properties 文件统一 UTF-8 |
9.2 代码层面
| 实践 | 说明 |
|---|---|
| 不要硬编码 Locale | 始终用 LocaleContextHolder.getLocale() 获取当前语言 |
| 封装工具类 | 提供 I18nUtil 之类的静态工具类,简化调用 |
| 提供默认值 | 关键场景下使用 getMessage(code, args, defaultMessage, locale) 避免异常 |
Controller 用 Thymeleaf 的 #{} |
在模板中直接用 #{key} 而不是在 Controller 中获取再传递 |
9.3 性能建议
| 实践 | 说明 |
|---|---|
| 生产环境关闭热加载 | cache-duration: -1 避免频繁检查文件 |
| 避免在循环中调用 | 如果同一消息需要多次使用,先取出来存入局部变量 |
| 大型项目考虑数据库方案 | 超过几千条消息时,可以考虑自定义 MessageSource 从数据库读取 |
10. 总结
这篇教程涵盖了 Spring MessageSource 国际化的核心内容:
- 为什么需要 MessageSource:相比原生 ResourceBundle 的优势
- 快速入门:5 分钟跑通第一个国际化示例
- 三种实现选型 :推荐
ReloadableResourceBundleMessageSource - 基础用法:资源文件命名、getMessage 调用、参数化消息
- 进阶特性:消息嵌套、ChoiceFormat、热加载
- Web 动态切换:LocaleResolver + LocaleChangeInterceptor
- Spring Boot 配置 :
spring.messages.*常用配置项 - 实战案例:多语言后台系统的完整实现
- 最佳实践:资源文件管理、代码规范、性能建议
掌握了这些,你就能在 Spring Boot 项目中优雅地实现国际化了。如果想进一步深入,可以了解:
- 数据库存储国际化消息(自定义
AbstractMessageSource) - 与
Validator的整合(@Valid校验消息国际化) - Spring Security 的认证消息国际化
希望这篇教程对你有帮助!