国际化
Spring Boot 通过 MessageSource + LocaleResolver 实现国际化(i18n),无需额外依赖,开箱即用。
核心组件
| 组件 | 作用 |
|---|---|
MessageSource | 根据 Locale 加载消息文件,统一消息查找入口 |
LocaleResolver | 从请求中解析当前语言(Header / Session / Cookie / 参数) |
LocaleChangeInterceptor | 通过请求参数动态切换语言 |
消息文件
文件布局
放在 src/main/resources/ 下,Spring Boot 默认识别 messages 前缀:
resources/
├── messages.properties ← 默认/兜底
├── messages_zh_CN.properties ← 简体中文
└── messages_en_US.properties ← 英文
文件命名规则:
basename_语言_地区.properties,缺少精确匹配时逐级回退到默认文件。
消息内容
# messages_zh_CN.properties
greeting=你好,{0}!
user.not.found=用户 {0} 不存在
error.required={0} 不能为空
order.created=订单 {0} 已创建,金额:{1}# messages_en_US.properties
greeting=Hello, {0}!
user.not.found=User {0} not found
error.required={0} is required
order.created=Order {0} created, amount: {1}{0}、{1} 为位置参数,基于 java.text.MessageFormat 格式化。
配置
# application.yml
spring:
messages:
basename: messages # 文件前缀,多个用逗号分隔
encoding: UTF-8
cache-duration: 3600 # 消息文件缓存时长(秒),生产建议开启
fallback-to-system-locale: false # 找不到匹配时不回退到系统 LocaleLocaleResolver
Spring Boot 默认使用 AcceptHeaderLocaleResolver(读取请求的 Accept-Language 头)。
四种实现对比
| 实现 | 语言来源 | 适用场景 |
|---|---|---|
AcceptHeaderLocaleResolver | Accept-Language 请求头 | REST API、默认行为 |
SessionLocaleResolver | 服务端 Session | 传统 Web,登录后切换语言 |
CookieLocaleResolver | 客户端 Cookie | SPA / 无状态接口 |
FixedLocaleResolver | 固定语言 | 单语言系统 |
配置 CookieLocaleResolver(常用于前后端分离)
@Configuration
public class I18nConfig implements WebMvcConfigurer {
@Bean
public LocaleResolver localeResolver() {
CookieLocaleResolver resolver = new CookieLocaleResolver("lang");
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setCookieMaxAge(Duration.ofDays(365));
return resolver;
}
// 允许通过 ?lang=en_US 切换语言
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang");
return interceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}切换语言:GET /api/users?lang=en_US,之后所有请求自动使用该语言。
在代码中使用 MessageSource
@Service
@RequiredArgsConstructor
public class UserService {
private final MessageSource messageSource;
public String getGreeting(String name, Locale locale) {
// 获取带参数的消息
return messageSource.getMessage("greeting", new Object[]{name}, locale);
}
public void checkUser(Long id, Locale locale) {
User user = userRepo.findById(id).orElseThrow(() -> {
String msg = messageSource.getMessage(
"user.not.found", new Object[]{id}, locale);
return new BusinessException(msg);
});
}
}在 Controller 中注入 Locale
@RestController
@RequiredArgsConstructor
public class UserController {
private final MessageSource messageSource;
@GetMapping("/greeting")
public String greeting(@RequestParam String name, Locale locale) {
return messageSource.getMessage("greeting", new Object[]{name}, locale);
}
// Spring MVC 自动注入当前请求的 Locale
@PostMapping("/orders")
public ApiResponse<Order> createOrder(@RequestBody OrderRequest req, Locale locale) {
Order order = orderService.create(req);
String msg = messageSource.getMessage(
"order.created",
new Object[]{order.getId(), order.getAmount()},
locale
);
return ApiResponse.success(order, msg);
}
}MessageSourceUtil(封装工具类)
避免在业务代码中到处传递 Locale,可封装静态工具:
@Component
public class MessageUtil implements ApplicationContextAware {
private static MessageSource messageSource;
@Override
public void setApplicationContext(ApplicationContext ctx) {
messageSource = ctx.getBean(MessageSource.class);
}
public static String get(String code, Object... args) {
Locale locale = LocaleContextHolder.getLocale(); // 从当前线程获取 Locale
return messageSource.getMessage(code, args, code, locale);
}
}使用:
throw new BusinessException(MessageUtil.get("user.not.found", userId));参数校验国际化
JSR-303 校验注解的消息也可以国际化,在 ValidationMessages.properties 中覆盖默认消息:
# ValidationMessages_zh_CN.properties
jakarta.validation.constraints.NotBlank.message={0}不能为空
jakarta.validation.constraints.Size.message={0}长度必须在{min}到{max}之间
jakarta.validation.constraints.Email.message={0}格式不正确
# 自定义消息 key
user.name.required=用户名不能为空public class UserRequest {
@NotBlank(message = "{user.name.required}") // 引用消息 key
private String username;
@Size(min = 6, max = 20, message = "{jakarta.validation.constraints.Size.message}")
private String password;
}参数校验详见 参数校验。
在 Thymeleaf 中使用
<!-- #{key} 语法读取消息 -->
<h1 th:text="#{greeting(${user.name})}">Hello</h1>
<p th:text="#{user.not.found(${userId})}"></p>
<!-- 组合使用 -->
<button th:text="#{button.submit}">提交</button>Thymeleaf 模板详见 Thymeleaf。
异常消息国际化
结合 @ControllerAdvice 统一返回国际化错误信息:
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {
private final MessageSource messageSource;
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusiness(
BusinessException ex, Locale locale) {
String msg = messageSource.getMessage(
ex.getMessageCode(), ex.getArgs(), ex.getMessage(), locale);
return ResponseEntity.badRequest()
.body(new ErrorResponse(ex.getCode(), msg));
}
}全局异常处理详见 全局异常处理。
前后端分离方案
REST API 通常通过 Accept-Language 头传递语言,无需 Cookie/Session:
GET /api/users/1
Accept-Language: zh-CN,zh;q=0.9
客户端(axios)统一添加请求头:
axios.defaults.headers.common['Accept-Language'] = navigator.language;后端直接使用默认的 AcceptHeaderLocaleResolver,无需额外配置。