国际化

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  # 找不到匹配时不回退到系统 Locale

LocaleResolver

Spring Boot 默认使用 AcceptHeaderLocaleResolver(读取请求的 Accept-Language 头)。

四种实现对比

实现语言来源适用场景
AcceptHeaderLocaleResolverAccept-Language 请求头REST API、默认行为
SessionLocaleResolver服务端 Session传统 Web,登录后切换语言
CookieLocaleResolver客户端 CookieSPA / 无状态接口
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,无需额外配置。

相关链接