Spring Boot 国际化(i18n)完全指南
一、什么是 i18n?
i18n 是 "internationalization" 的缩写(i 和 n 之间有 18 个字母),核心思想是:把用户可见的文本从代码中抽离到外部资源文件,运行时根据语言环境动态加载对应文件,实现多语言切换而无需改代码。
二、核心机制
工作原理
客户端请求(携带 Accept-Language: en_US)
↓
LocaleResolver 解析语言环境 → Locale.US
↓
MessageSource 按优先级查找资源文件:
1. messages_en_US.properties ← 精确匹配
2. messages_en.properties ← 语言匹配
3. messages.properties ← 默认兜底
↓
找到 key 对应的 value → 返回文案
三个核心组件
| 组件 | 职责 |
|---|---|
| MessageSource | 负责根据 key + Locale 加载对应语言的文本 |
| LocaleResolver | 负责从请求中解析出用户的语言偏好 |
| 资源文件 (*.properties) | 存储各语言的 key-value 文案 |
LocaleResolver 的常见实现
| 类型 | 判断依据 | 适用场景 |
|---|---|---|
AcceptHeaderLocaleResolver |
HTTP 请求头 Accept-Language |
API 服务、微服务 |
CookieLocaleResolver |
Cookie 中存储的语言偏好 | Web 应用 |
SessionLocaleResolver |
Session 中存储的语言偏好 | 传统 Web 应用 |
FixedLocaleResolver |
固定语言,不可更改 | 单语言应用 |
三、资源文件规则
命名规范
{basename}.properties ← 默认(兜底)
{basename}_{language}.properties ← 按语言
{basename}_{language}_{country}.properties ← 按语言+地区
查找优先级(以 Locale("zh", "CN") 为例)
1. messages_zh_CN.properties ← 最精确
2. messages_zh.properties ← 语言级别
3. messages.properties ← 默认兜底
如果高优先级文件中没有某个 key,会自动 fallback 到低优先级文件。
文件编码
.properties文件默认使用 ISO-8859-1 编码- 中文需要写成 Unicode 转义形式:
\u4F1A\u5458= "会员" - 配置
encoding: UTF-8后可直接写中文(Spring Boot 推荐方式)
四、配置方式
方式一:application.yml 配置(推荐)
yaml
spring:
messages:
basename: i18n/messages # 资源文件路径前缀(相对于 classpath)
cache-duration: 3s # 缓存时长,开发时设短方便热更新
encoding: UTF-8 # 文件编码
fallback-to-system-locale: true # 是否回退到系统默认语言
| 配置项 | 说明 |
|---|---|
basename |
文件路径前缀,多个用逗号分隔:i18n/messages,i18n/errors |
cache-duration |
缓存刷新间隔,生产环境可设大(如 1h),开发设小(如 3s) |
encoding |
设为 UTF-8 后 properties 文件可直接写中文 |
fallback-to-system-locale |
找不到对应语言文件时是否用 JVM 默认 Locale |
方式二:Java Config 配置
java
@Configuration
public class I18nConfig {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasename("i18n/messages");
source.setDefaultEncoding("UTF-8");
source.setCacheSeconds(3);
return source;
}
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
}
两种方式效果相同。当 Java Bean 和 YAML 同时配置时,Java Bean 优先。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
五、完整通用示例
以一个用户注册模块为例,演示完整的 i18n 实现。
1. 目录结构
src/main/resources/
├── application.yml
└── i18n/
├── messages.properties # 默认(中文)
├── messages_zh_CN.properties # 中文(可为空,默认已是中文)
└── messages_en_US.properties # 英文
2. 资源文件内容
messages.properties(默认,中文兜底):
properties
# 用户模块
user.register.username.empty=用户名不能为空
user.register.username.duplicate=用户名"{0}"已被注册
user.register.password.too.short=密码长度不能少于{0}位
user.register.email.invalid=邮箱格式不正确
user.register.success=注册成功,欢迎{0}!
# 通用
common.param.invalid=参数校验失败
common.system.error=系统繁忙,请稍后重试
messages_en_US.properties(英文):
properties
# User module
user.register.username.empty=Username cannot be empty
user.register.username.duplicate=Username "{0}" is already taken
user.register.password.too.short=Password must be at least {0} characters
user.register.email.invalid=Invalid email format
user.register.success=Registration successful, welcome {0}!
# Common
common.param.invalid=Parameter validation failed
common.system.error=System is busy, please try again later
messages_zh_CN.properties(留空即可,fallback 到默认文件):
properties
# 留空,默认文件已是中文
3. application.yml
yaml
spring:
messages:
basename: i18n/messages
cache-duration: 3s
encoding: UTF-8
4. 配置类
java
@Configuration
public class I18nConfig {
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
return resolver;
}
}
5. 封装工具类(方便全局调用)
java
@Component
public class I18nUtil {
private static MessageSource messageSource;
@Resource
public void setMessageSource(MessageSource messageSource) {
I18nUtil.messageSource = messageSource;
}
/**
* 获取国际化消息.
*
* @param key 消息 key
* @param args 占位符参数
* @return 对应语言的消息文本
*/
public static String getMessage(String key, Object... args) {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(key, args, locale);
}
/**
* 获取国际化消息(带默认值).
*/
public static String getMessage(String key, String defaultMsg, Object... args) {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(key, args, defaultMsg, locale);
}
}
6. Service 层使用
java
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public void register(UserRegisterRequest request) {
// 校验用户名
if (StringUtils.isBlank(request.getUsername())) {
throw new BusinessException(I18nUtil.getMessage("user.register.username.empty"));
}
// 校验密码长度(带占位符参数)
if (request.getPassword().length() < 8) {
throw new BusinessException(
I18nUtil.getMessage("user.register.password.too.short", 8));
}
// 校验用户名重复
if (userMapper.existsByUsername(request.getUsername())) {
throw new BusinessException(
I18nUtil.getMessage("user.register.username.duplicate", request.getUsername()));
}
// 保存用户
userMapper.insert(buildUser(request));
log.info("用户注册成功, username={}", request.getUsername());
}
}
7. Controller 层
java
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/register")
public Result<String> register(@RequestBody UserRegisterRequest request) {
try {
userService.register(request);
String successMsg = I18nUtil.getMessage("user.register.success", request.getUsername());
return Result.success(successMsg);
} catch (BusinessException e) {
return Result.fail(e.getMessage());
} catch (Exception e) {
log.error("用户注册异常, error={}", e.getMessage(), e);
return Result.fail(I18nUtil.getMessage("common.system.error"));
}
}
}
8. 运行效果
中文请求(或不带 Accept-Language):
POST /api/user/register
Body: {"username": "", "password": "123"}
响应:{"code": 500, "message": "用户名不能为空"}
英文请求:
POST /api/user/register
Headers: Accept-Language: en_US
Body: {"username": "", "password": "123"}
响应:{"code": 500, "message": "Username cannot be empty"}
带占位符参数:
POST /api/user/register
Headers: Accept-Language: en_US
Body: {"username": "tom", "password": "123"}
响应:{"code": 500, "message": "Password must be at least 8 characters"}
六、占位符语法
资源文件支持 {0}, {1}, {2} 等位置占位符:
properties
# {0} = 用户名, {1} = 日期
user.welcome=欢迎{0},您的账号创建于{1}
代码中传参:
java
messageSource.getMessage("user.welcome", new Object[]{"张三", "2026-06-16"}, locale);
// 输出:欢迎张三,您的账号创建于2026-06-16
七、参数校验 + i18n 整合
Spring Validation 的注解也支持 i18n,在 message 属性中引用资源 key:
java
@Data
public class UserRegisterRequest {
@NotBlank(message = "{user.register.username.empty}")
private String username;
@Size(min = 8, message = "{user.register.password.too.short}")
private String password;
@Email(message = "{user.register.email.invalid}")
private String email;
}
注意:用 {} 包裹 key 名。Spring 会自动从 MessageSource 查找对应文案。
八、常见问题
Q1: 为什么 zh_CN 文件是空的?
默认文件(messages.properties)里已经写了中文。查找链是 zh_CN → 默认,所以不需要重复写一遍。只有当默认文件用英文、要额外支持中文时才需要填 zh_CN。
Q2: cache-duration: 3s 生产环境要改吗?
要。开发时设 3 秒方便调试,生产环境建议设大一些(如 1h 或 -1 表示永不刷新),避免频繁读文件影响性能。
Q3: 一个项目能有多个 basename 吗?
可以,逗号分隔:
yaml
spring:
messages:
basename: i18n/messages,i18n/errors,i18n/validation
对应的文件结构:
i18n/
├── messages.properties
├── messages_en_US.properties
├── errors.properties
├── errors_en_US.properties
├── validation.properties
└── validation_en_US.properties
Q4: 找不到 key 时会怎样?
默认抛出 NoSuchMessageException。可以用带默认值的方法避免:
java
messageSource.getMessage("some.key", null, "默认文案", locale);
Q5: 如何动态切换语言(不靠请求头)?
使用 LocaleChangeInterceptor,通过 URL 参数切换:
java
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang"); // ?lang=en_US
return interceptor;
}
访问 http://localhost:8080/api/user?lang=en_US 即可切换到英文。
九、总结
| 要素 | 作用 |
|---|---|
messages.properties |
默认兜底文案 |
messages_{locale}.properties |
特定语言文案 |
MessageSource |
根据 key + locale 查找文案的核心接口 |
LocaleResolver |
从请求中解析用户语言偏好 |
{0} 占位符 |
支持动态参数替换 |
cache-duration |
控制文件缓存刷新频率 |
@NotBlank(message = "{key}") |
校验注解直接对接 i18n |