Spring MessageSource 国际化方案

文章目录

  • [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 资源文件管理

实践 说明
按模块拆分 不要把所有消息都放在一个文件里,按 messageserrorsvalidation 等模块拆分
默认文件兜底 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 国际化的核心内容:

  1. 为什么需要 MessageSource:相比原生 ResourceBundle 的优势
  2. 快速入门:5 分钟跑通第一个国际化示例
  3. 三种实现选型 :推荐 ReloadableResourceBundleMessageSource
  4. 基础用法:资源文件命名、getMessage 调用、参数化消息
  5. 进阶特性:消息嵌套、ChoiceFormat、热加载
  6. Web 动态切换:LocaleResolver + LocaleChangeInterceptor
  7. Spring Boot 配置spring.messages.* 常用配置项
  8. 实战案例:多语言后台系统的完整实现
  9. 最佳实践:资源文件管理、代码规范、性能建议

掌握了这些,你就能在 Spring Boot 项目中优雅地实现国际化了。如果想进一步深入,可以了解:

  • 数据库存储国际化消息(自定义 AbstractMessageSource
  • Validator 的整合(@Valid 校验消息国际化)
  • Spring Security 的认证消息国际化

希望这篇教程对你有帮助!

相关推荐
IT策士17 小时前
Django 从 0 到 1 打造完整电商平台:购物车页面增删改查商品数量
后端·python·django
还是鼠鼠17 小时前
AI掘金头条新闻系统 (Toutiao News)-封装通用成功响应格式
数据库·后端·python·fastapi·web
zhaokuangkuang_17 小时前
Java学习
java·学习·算法
暗冰ཏོ17 小时前
《Vue + React + Java + PHP 项目部署到服务器完整指南》
java·服务器·vue.js·react.js·项目部署
_Aaron___17 小时前
Spring AI 2.0 之后,MCP Server 该按远程企业服务来设计
java·人工智能·spring
NE_STOP17 小时前
Docker--Docker简介及系统架构
java
Daydream.V17 小时前
C++ 入门全攻略:从基础语法到核心特性
java·开发语言·c++
我是一颗柠檬17 小时前
【JDK8新特性】接口默认方法与静态方法Day8
java·开发语言·后端·intellij-idea
lulu121654407817 小时前
【开发者指南】Gemini 3.5开发入门:从API调用到Agent构建
java·开发语言·人工智能·python·ai编程
SimonKing17 小时前
从单机到高并发:手搓唯一编号的生成方案
java·后端·程序员