正直青年打倒美国消灭日本 發表於 2026-1-13 09:23:19

SpringBoot实现i18n国际化的两种企业级方案

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>前言</li><li>一、国际化基础认知</li><ul class="second_class_ul"><li>1.1 核心概念</li><li>1.2 核心原理</li></ul><li>二、方式一:基于配置文件的i18n实现</li><ul class="second_class_ul"><li>2.1 环境准备</li><ul class="third_class_ul"><li>2.1.1 依赖配置</li><li>2.1.2 目录结构</li></ul><li>2.2 多语言配置文件编写</li><ul class="third_class_ul"><li>2.2.1 命名规则</li><li>2.2.2 配置文件内容</li></ul><li>2.3 SpringBoot核心配置</li><ul class="third_class_ul"></ul><li>2.4 自定义语言解析器与拦截器</li><ul class="third_class_ul"><li>2.4.1 自定义LocaleResolver</li></ul><li>2.5 国际化消息使用示例</li><ul class="third_class_ul"><li>2.5.1 工具类封装(推荐)</li><li>2.5.2 控制器使用示例</li></ul><li>2.6 测试验证</li><ul class="third_class_ul"></ul><li>2.7 默认语言切换说明</li><ul class="third_class_ul"></ul></ul><li>三、方式二:基于数据库的动态i18n实现</li><ul class="second_class_ul"><li>3.1 设计思路</li><ul class="third_class_ul"></ul><li>3.2 数据库表设计</li><ul class="third_class_ul"><li>3.2.1 建表语句(MySQL)</li><li>3.2.2 测试数据插入</li></ul><li>3.3 环境准备</li><ul class="third_class_ul"><li>3.3.1 添加依赖</li><li>3.3.2 数据库配置</li></ul><li>3.4 核心组件开发</li><ul class="third_class_ul"><li>3.4.1 实体类</li><li>3.4.2 Mapper接口</li><li>3.4.3 Service层</li><li>3.4.4 自定义MessageSource</li><li>3.4.5 替换默认MessageSource</li></ul><li>3.5 业务集成与测试</li><ul class="third_class_ul"><li>3.5.1 工具类适配</li><li>3.5.2 消息管理接口</li><li>3.5.3 测试验证</li></ul></ul><li>四、进阶优化措施</li><ul class="second_class_ul"><li>4.1 缓存优化(数据库方式)</li><ul class="third_class_ul"></ul><li>4.2 语言解析器增强</li><ul class="third_class_ul"></ul><li>4.3 动态刷新配置(配置文件方式)</li><ul class="third_class_ul"></ul><li>4.4 异常处理国际化</li><ul class="third_class_ul"></ul></ul><li>五、常见问题与解决方案</li><ul class="second_class_ul"><li>5.1 校验注解(@NotNull等)国际化适配</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.2 配置文件中文乱码</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.3 默认语言不生效</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.4 数据库方式性能问题</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.5 动态修改语言后不生效(适配 Header 方式)</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.6 数据库消息未找到时返回Key本身</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.7 Header 中语言标识格式错误导致切换失败</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.8 跨域请求时 Header 中的 lang 参数丢失</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul><li>5.9 自定义 MessageSource 优先级低于默认实现导致校验注解国际化不生效</li><ul class="third_class_ul"><li>问题描述</li><li>解决方案</li></ul></ul><li>六、总结</li><ul class="second_class_ul"><li>6.1 两种方式对比</li><ul class="third_class_ul"></ul><li>6.2 核心要点回顾</li><ul class="third_class_ul"></ul></ul></ul></div><p class="maodian"></p><h2>前言</h2>
<p>在全球化业务场景下,系统适配多语言已成为标配需求。SpringBoot作为主流的Java开发框架,提供了完善的国际化(i18n,internationalization的缩写,因i和n之间有18个字母得名)解决方案。本文将从实战角度出发,完整讲解两种企业级i18n实现方案:<strong>基于配置文件的静态实现</strong>(适配简体中文、繁体中文、英文)和<strong>基于数据库的动态实现</strong>(支持运行时修改语言配置),同时覆盖校验注解国际化、性能优化、常见问题排查等核心要点,所有代码均可直接落地到生产项目。</p>
<p class="maodian"></p><h2>一、国际化基础认知</h2>
<p class="maodian"></p><h3>1.1 核心概念</h3>
<p>i18n的核心目标是让系统在不修改代码的前提下,通过配置适配不同语言和地区的使用习惯。SpringBoot中实现i18n的核心依赖是:</p>
<ul><li><code>MessageSource</code>:消息源接口,负责加载和解析多语言消息,默认实现为<code>ResourceBundleMessageSource</code>(基于配置文件)。</li><li><code>Accept-Language</code>:语言地区标识,格式为<code>语言代码_国家/地区代码</code>,如:<ul><li>简体中文:<code>zh_CN</code></li><li>繁体中文:<code>zh_TW</code></li><li>英文(美国):<code>en_US</code></li></ul></li><li><code>LocaleResolver</code>:语言解析器,负责从请求中获取/设置当前Locale。</li><li><code>LocaleChangeInterceptor</code>:语言切换拦截器,用于拦截请求参数实现语言动态切换。</li></ul>
<p class="maodian"></p><h3>1.2 核心原理</h3>
<p>SpringBoot启动时,<code>MessageSource</code>会加载指定路径下的多语言配置文件;当业务代码获取国际化消息时,框架会根据当前<code>Accept-Language</code>从对应配置文件/数据源中匹配消息键(Key),返回对应的消息值(Value)。</p>
<p class="maodian"></p><h2>二、方式一:基于配置文件的i18n实现</h2>
<p class="maodian"></p><h3>2.1 环境准备</h3>
<p class="maodian"></p><h4>2.1.1 依赖配置</h4>
<p>新建SpringBoot项目(推荐2.7.x或3.2.x),核心依赖仅需<code>spring-boot-starter-web</code>,无需额外依赖:</p>
<div class="jb51code"><pre class="brush:xml;">&lt;dependencies&gt;
    &lt;dependency&gt;
      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
      &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- 可选:简化配置文件编写(.yml) --&gt;
    &lt;dependency&gt;
      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
      &lt;artifactId&gt;spring-boot-starter-validation&lt;/artifactId&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;
</pre></div>
<p class="maodian"></p><h4>2.1.2 目录结构</h4>
<p>在<code>resources</code>目录下创建<code>i18n</code>文件夹,用于存放多语言配置文件,最终目录结构:</p>
<div class="jb51code"><pre class="brush:yaml;">resources/
├── application.yml          # 核心配置
└── i18n/                  # 国际化配置文件目录
    ├── messages.properties# 默认配置(无语言标识)
    ├── messages_zh_CN.properties# 简体中文
    ├── messages_zh_TW.properties# 繁体中文
    └── messages_en_US.properties# 英文
</pre></div>
<p class="maodian"></p><h3>2.2 多语言配置文件编写</h3>
<p class="maodian"></p><h4>2.2.1 命名规则</h4>
<p>配置文件命名必须遵循<code>basename_语言代码_国家代码.properties</code>规则:</p>
<ul><li><code>basename</code>:自定义前缀(如<code>messages</code>),需在application.yml中配置。</li><li>无语言标识的<code>messages.properties</code>为<strong>默认配置</strong>,当匹配不到指定Locale的配置时,会使用该文件内容。</li></ul>
<p class="maodian"></p><h4>2.2.2 配置文件内容</h4>
<ol><li><strong>默认配置(messages.properties)</strong>:兜底使用,建议与默认语言(简体中文)保持一致</li></ol>
<div class="jb51code"><pre class="brush:yaml;"># 通用提示
common.submit=提交
common.cancel=取消
# 用户相关
user.name=用户名
user.age=年龄
# 校验提示
validate.required.id=主键不能为空
validate.required.name=姓名不能为空
</pre></div>
<ol start="2"><li><strong>简体中文(messages_zh_CN.properties)</strong>:</li></ol>
<div class="jb51code"><pre class="brush:yaml;"># 通用提示
common.submit=提交
common.cancel=取消
# 用户相关
user.name=用户名
user.age=年龄
# 校验提示
validate.required.id=主键不能为空
validate.required.name=姓名不能为空
</pre></div>
<ol start="3"><li><strong>繁体中文(messages_zh_TW.properties)</strong>:</li></ol>
<div class="jb51code"><pre class="brush:yaml;"># 通用提示
common.submit=提交
common.cancel=取消
# 用户相关
user.name=使用者名稱
user.age=年齡
# 校验提示
validate.required.id=主鍵不能為空
validate.required.name=姓名不能為空
</pre></div>
<ol start="4"><li><strong>英文(messages_en_US.properties)</strong>:</li></ol>
<div class="jb51code"><pre class="brush:yaml;"># 通用提示
common.submit=Submit
common.cancel=Cancel
# 用户相关
user.name=Username
user.age=Age
# 校验提示
validate.required.id=Primary key cannot be empty
validate.required.name=Name cannot be empty
</pre></div>
<blockquote><p>注意:properties文件默认编码为ISO-8859-1,直接写中文会乱码!需将IDE的properties文件编码设置为UTF-8(IDEA:Settings &rarr; File Encodings &rarr; Properties Files &rarr; 勾选Transparent native-to-ascii conversion)。</p></blockquote>
<p class="maodian"></p><h3>2.3 SpringBoot核心配置</h3>
<p>在<code>application.yml</code>中配置国际化相关参数,指定配置文件路径、默认语言、编码等:</p>
<div class="jb51code"><pre class="brush:yaml;">spring:
# 国际化配置
messages:
    basename: i18n/messages# 配置文件路径(无需写.properties后缀)
    encoding: UTF-8         # 解决中文乱码
    fallback-to-system-locale: false# 禁用系统语言回退
    default-locale: zh_CN    # 默认语言:简体中文
    cache-duration: 3600s   # 配置文件缓存时间(生产建议设置)
# Web配置(可选,用于请求参数解析)
web:
    locale: zh_CN
</pre></div>
<p class="maodian"></p><h3>2.4 自定义语言解析器与拦截器</h3>
<p>默认情况下,SpringBoot仅支持从请求头<code>Accept-Language</code>获取Locale,为了方便通过请求参数(如<code>?Accept-Language=en-US</code>)切换语言,需自定义<code>LocaleResolver</code>并注册拦截器。</p>
<p class="maodian"></p><h4>2.4.1 自定义LocaleResolver</h4>
<p>创建<code>config/I18nConfig.java</code>,实现<code>LocaleResolver</code>接口:</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

/**
* 国际化核心配置类(修改为Header拦截语言)
*/
@Configuration
public class I18nConfig implements WebMvcConfigurer {

    /**
   * 注册自定义LocaleResolver(基于Session存储Locale)
   * 替代默认的AcceptHeaderLocaleResolver
   */
    @Bean
    public LocaleResolver localeResolver() {
      SessionLocaleResolver resolver = new SessionLocaleResolver();
      // 设置默认语言:简体中文(与application.yml中保持一致)
      resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 默认:zh_CN
      return resolver;
    }

    /**
   * 自定义拦截器:从 Accept-Language Header 解析并设置 Locale
   */
    @Bean
    public HandlerInterceptor localeHeaderInterceptor(LocaleResolver localeResolver) {
      return new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                  throws Exception {
                // 1. 从请求头中获取语言标识(自定义Header名:Accept-Language,可根据需求修改)
                String acceptLanguage = request.getHeader("Accept-Language");
                // 2. 设置默认语言为空或者抛异常使用
                Locale locale = Locale.SIMPLIFIED_CHINESE; // 默认语言
                // 3. 若Header中有值,则解析并设置Accept-Language;无值则使用默认Accept-Language
                if (acceptLanguage != null &amp;&amp; !acceptLanguage.isEmpty()) {
                  try {
                        // 取第一个语言项(如 "zh-CN,en;q=0.8" → "zh-CN")
                        String primary = acceptLanguage.split(",").trim();
                        // Spring 工具类能正确解析 "zh-CN"、"en" 等格式
                        locale = StringUtils.parseLocale(primary);
                  } catch (Exception e) {
                        // 解析失败则使用默认语言,不抛异常
                  }
                }

                // 使用容器中真实的 LocaleResolver 实例设置 Locale(存入 Session)
                localeResolver.setLocale(request, response, locale);
                return true;
            }
      };
    }

    /**
   * 注册拦截器到 Spring MVC 拦截器链
   */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(localeHeaderInterceptor(localeResolver()))
                .addPathPatterns("/**")
                .order(0); // 优先级最高
    }
}
</pre></div>
<p><strong>关键说明</strong>:</p>
<ul><li><code>SessionLocaleResolver</code>:将Locale存储在Session中,一次切换后,后续请求无需重复传参。</li><li><code>localeHeaderInterceptor</code>:拦截请求头<code>Header</code>中<code>Accept-Language</code>参数,自动更新当前Locale(如<code>Accept-Language=zh-TW</code>会切换为繁体中文)。</li></ul>
<p class="maodian"></p><h3>2.5 国际化消息使用示例</h3>
<p class="maodian"></p><h4>2.5.1 工具类封装(推荐)</h4>
<p>创建<code>utils/I18nUtils.java</code>,封装获取国际化消息的方法,简化业务使用:</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.utils;

import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Locale;

/**
* 国际化工具类
*/
@Component
public class I18nUtils {

    @Resource
    private MessageSource messageSource;

    /**
   * 获取国际化消息(使用当前Locale)
   * @param key 消息键
   * @return 消息值
   */
    public String getMessage(String key) {
      return getMessage(key, null, LocaleContextHolder.getLocale());
    }

    /**
   * 获取国际化消息(带参数)
   * @param key 消息键
   * @param args 参数数组(如消息为"你好{0}",args=new Object[]{"张三"})
   * @return 消息值
   */
    public String getMessage(String key, Object[] args) {
      return getMessage(key, args, LocaleContextHolder.getLocale());
    }

    /**
   * 手动指定Locale获取消息
   * @param key 消息键
   * @param args 参数数组
   * @param locale 语言标识
   * @return 消息值
   */
    public String getMessage(String key, Object[] args, Locale locale) {
      try {
            // 从MessageSource中获取消息,若未找到则返回key本身
            return messageSource.getMessage(key, args, locale);
      } catch (Exception e) {
            return key;
      }
    }
}
</pre></div>
<p><strong>核心API说明</strong>:</p>
<ul><li><code>LocaleContextHolder.getLocale()</code>:获取当前线程的Locale(由LocaleResolver解析)。</li><li><code>messageSource.getMessage(key, args, locale)</code>:核心方法,参数说明:<ul><li><code>key</code>:消息键(如<code>user.name</code>)。</li><li><code>args</code>:消息参数(用于替换消息中的占位符,如<code>user.hello=你好{0}</code>)。</li><li><code>locale</code>:指定语言标识。</li></ul></li></ul>
<p class="maodian"></p><h4>2.5.2 控制器使用示例</h4>
<p>创建<code>controller/I18nController.java</code>,编写接口测试国际化效果:</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.controller;

import com.example.i18n.utils.I18nUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
* 国际化测试控制器
*/
@RestController
@RequestMapping("/api/i18n")
public class I18nController {

    @Resource
    private I18nUtils i18nUtils;

    /**
   * 测试基础国际化消息
   * 请求头添加:Accept-Language: en-US(或 zh-TW /zh-CN)
   * 访问示例:
   * - 简体中文:http://localhost:8080/api/i18n/basic
   * - 繁体中文:http://localhost:8080/api/i18n/basic
   * - 英文:http://localhost:8080/api/i18n/basic
   */
    @GetMapping("/basic")
    public Map&lt;String, String&gt; getBasicMessage() {
      Map&lt;String, String&gt; result = new HashMap&lt;&gt;();
      // 获取当前语言的消息
      result.put("user.name", i18nUtils.getMessage("user.name"));
      result.put("common.submit", i18nUtils.getMessage("common.submit"));
      result.put("validate.required.id", i18nUtils.getMessage("validate.required.id"));
      return result;
    }

    /**
   * 测试带参数的国际化消息
   */
    @GetMapping("/with-params")
    public Map&lt;String, String&gt; getMessageWithParams() {
      Map&lt;String, String&gt; result = new HashMap&lt;&gt;();
      // 模拟带参数的消息(需先在配置文件中添加:user.hello=你好{0},user.hello=你好{0}(繁),user.hello=Hello {0}(英))
      String helloMsg = i18nUtils.getMessage("user.hello", new Object[]{"张三"});
      result.put("user.hello", helloMsg);
      return result;
    }

    /**
   * 手动指定Locale获取消息
   */
    @GetMapping("/manual-locale")
    public Map&lt;String, String&gt; getMessageByManualLocale() {
      Map&lt;String, String&gt; result = new HashMap&lt;&gt;();
      // 手动指定繁体中文
      result.put("zh_TW.user.name", i18nUtils.getMessage("user.name", null, Locale.TRADITIONAL_CHINESE));
      // 手动指定英文
      result.put("en_US.user.name", i18nUtils.getMessage("user.name", null, new Locale("en", "US")));
      return result;
    }
}
</pre></div>
<p class="maodian"></p><h3>2.6 测试验证</h3>
<p>启动项目后,通过Postman/Browser访问以下地址验证效果:</p>
<p>简体中文:<code>http://localhost:8080/api/i18n/basic?lang=zh_CN</code><br />返回:</p>
<div class="jb51code"><pre class="brush:java;">{
"user.name":"用户名",
"common.submit":"提交",
"validate.required.id":"主键不能为空"
}
</pre></div>
<p>繁体中文:<code>http://localhost:8080/api/i18n/basic?lang=zh_TW</code><br />返回:</p>
<div class="jb51code"><pre class="brush:java;">{
"user.name":"使用者名稱",
"common.submit":"提交",
"validate.required.id":"主鍵不能為空"
}
</pre></div>
<p>英文:<code>http://localhost:8080/api/i18n/basic?lang=en_US</code><br />返回:</p>
<div class="jb51code"><pre class="brush:java;">{
"user.name":"Username",
"common.submit":"Submit",
"validate.required.id":"Primary key cannot be empty"
}
</pre></div>
<p class="maodian"></p><h3>2.7 默认语言切换说明</h3>
<p>默认语言的生效优先级:</p>
<ol><li><code>SessionLocaleResolver</code>中设置的<code>setDefaultLocale()</code>(代码级)。</li><li><code>application.yml</code>中<code>spring.messages.default-locale</code>(配置级)。</li><li>系统默认Locale(兜底)。</li></ol>
<p>若需修改默认语言为英文,只需调整两处:</p>
<div class="jb51code"><pre class="brush:java;">// 1. I18nConfig中
localeResolver.setDefaultLocale(Locale.US);

// 2. application.yml中
spring:
messages:
    default-locale: en_US
</pre></div>
<p class="maodian"></p><h2>三、方式二:基于数据库的动态i18n实现</h2>
<p>基于配置文件的方式存在明显缺陷:修改消息需重启服务。基于数据库的实现可实现<strong>运行时动态配置多语言消息</strong>,适合频繁变更或大规模多语言场景。</p>
<p class="maodian"></p><h3>3.1 设计思路</h3>
<ol><li>设计数据库表存储多语言消息(键、语言、值)。</li><li>自定义<code>MessageSource</code>实现,重写消息解析逻辑,从数据库加载消息。</li><li>引入缓存(Caffeine)提升性能,避免频繁查询数据库。</li><li>提供接口实现消息的新增/修改/删除,支持动态刷新缓存。</li></ol>
<p class="maodian"></p><h3>3.2 数据库表设计</h3>
<p class="maodian"></p><h4>3.2.1 建表语句(MySQL)</h4>
<div class="jb51code"><pre class="brush:sql;">CREATE TABLE `sys_i18n_message` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`message_key` varchar(100) NOT NULL COMMENT '消息键(全局唯一+语言)',
`language` varchar(20) NOT NULL COMMENT '语言标识(zh_CN/zh_TW/en_US)',
`message_value` varchar(500) NOT NULL COMMENT '消息值',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` tinyint DEFAULT 0 COMMENT '删除标记(0-未删,1-已删)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_key_language` (`message_key`,`language`) COMMENT '消息键+语言唯一约束'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='国际化消息表';
</pre></div>
<p class="maodian"></p><h4>3.2.2 测试数据插入</h4>
<div class="jb51code"><pre class="brush:sql;">INSERT INTO `sys_i18n_message` (`message_key`, `language`, `message_value`) VALUES
('user.name', 'zh_CN', '用户名'),
('user.name', 'zh_TW', '使用者名稱'),
('user.name', 'en_US', 'Username'),
('common.submit', 'zh_CN', '提交'),
('common.submit', 'zh_TW', '提交'),
('common.submit', 'en_US', 'Submit'),
('validate.required.id', 'zh_CN', '主键不能为空'),
('validate.required.id', 'zh_TW', '主鍵不能為空'),
('validate.required.id', 'en_US', 'Primary key cannot be empty');
</pre></div>
<p class="maodian"></p><h3>3.3 环境准备</h3>
<p class="maodian"></p><h4>3.3.1 添加依赖</h4>
<p>在原有依赖基础上,添加数据库相关依赖(以MyBatis-Plus为例):</p>
<div class="jb51code"><pre class="brush:xml;">&lt;!-- 数据库驱动 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;mysql&lt;/groupId&gt;
    &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
    &lt;scope&gt;runtime&lt;/scope&gt;
&lt;/dependency&gt;
&lt;!-- MyBatis-Plus(简化CRUD) --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.baomidou&lt;/groupId&gt;
    &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;3.5.3.1&lt;/version&gt;
&lt;/dependency&gt;
&lt;!-- 缓存:Caffeine --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.github.ben-manes.caffeine&lt;/groupId&gt;
    &lt;artifactId&gt;caffeine&lt;/artifactId&gt;
    &lt;version&gt;3.1.8&lt;/version&gt;
&lt;/dependency&gt;
&lt;!-- 连接池 --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
    &lt;artifactId&gt;druid-spring-boot-starter&lt;/artifactId&gt;
    &lt;version&gt;1.2.20&lt;/version&gt;
&lt;/dependency&gt;
</pre></div>
<p class="maodian"></p><h4>3.3.2 数据库配置</h4>
<p>在<code>application.yml</code>中添加数据库配置:</p>
<div class="jb51code"><pre class="brush:yaml;">spring:
# 数据库配置
datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/i18n_demo?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai
    username: root
    password: 123456
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.i18n.entity
configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
</pre></div>
<p class="maodian"></p><h3>3.4 核心组件开发</h3>
<p class="maodian"></p><h4>3.4.1 实体类</h4>
<p>创建<code>entity/SysI18nMessage.java</code>:</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.entity;

import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

import java.time.LocalDateTime;

/**
* 国际化消息实体
*/
@Data
@TableName("sys_i18n_message")
public class SysI18nMessage {

    /**
   * 主键ID
   */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
   * 消息键
   */
    @TableField("message_key")
    private String messageKey;

    /**
   * 语言标识
   */
    @TableField("language")
    private String language;

    /**
   * 消息值
   */
    @TableField("message_value")
    private String messageValue;

    /**
   * 创建时间
   */
    @TableField(value = "create_time", fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
   * 更新时间
   */
    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
   * 删除标记
   */
    @TableField("deleted")
    @TableLogic
    private Integer deleted;
}
</pre></div>
<p class="maodian"></p><h4>3.4.2 Mapper接口</h4>
<p>创建<code>mapper/SysI18nMessageMapper.java</code>:</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.i18n.entity.SysI18nMessage;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import java.util.List;

/**
* 国际化消息Mapper
*/
public interface SysI18nMessageMapper extends BaseMapper&lt;SysI18nMessage&gt; {

    /**
   * 根据消息键和语言查询消息
   */
    @Select("SELECT message_value FROM sys_i18n_message WHERE message_key = #{key} AND language = #{language} AND deleted = 0")
    String getMessageByKeyAndLanguage(@Param("key") String key, @Param("language") String language);

    /**
   * 查询所有消息(用于预加载缓存)
   */
    @Select("SELECT message_key, language, message_value FROM sys_i18n_message WHERE deleted = 0")
    List&lt;SysI18nMessage&gt; listAllMessages();
}
</pre></div>
<p class="maodian"></p><h4>3.4.3 Service层</h4>
<p>创建<code>service/SysI18nMessageService.java</code>(接口):</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.i18n.entity.SysI18nMessage;

import java.util.Map;

/**
* 国际化消息服务
*/
public interface SysI18nMessageService extends IService&lt;SysI18nMessage&gt; {

    /**
   * 根据键和语言获取消息
   */
    String getMessage(String key, String language);

    /**
   * 加载所有消息到缓存
   */
    Map&lt;String, String&gt; loadAllMessagesToCache();

    /**
   * 刷新缓存
   */
    void refreshCache();
}
</pre></div>
<p>创建<code>service/impl/SysI18nMessageServiceImpl.java</code>(实现类):</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.i18n.entity.SysI18nMessage;
import com.example.i18n.mapper.SysI18nMessageMapper;
import com.example.i18n.service.SysI18nMessageService;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
* 国际化消息服务实现
*/
@Service
public class SysI18nMessageServiceImpl extends ServiceImpl&lt;SysI18nMessageMapper, SysI18nMessage&gt; implements SysI18nMessageService {

    /**
   * 缓存Key规则:messageKey + "_" + language
   */
    private final Cache&lt;String, String&gt; i18nCache = Caffeine.newBuilder()
            .expireAfterWrite(1, TimeUnit.HOURS) // 1小时过期
            .maximumSize(10000) // 最大缓存10000条
            .build();

    /**
   * 项目启动时预加载所有消息到缓存
   */
    @PostConstruct
    public void initCache() {
      loadAllMessagesToCache();
    }

    @Override
    public String getMessage(String key, String language) {
      // 构造缓存Key
      String cacheKey = key + "_" + language;
      // 先查缓存,缓存未命中则查数据库
      return i18nCache.get(cacheKey, k -&gt; {
            String message = baseMapper.getMessageByKeyAndLanguage(key, language);
            // 数据库未找到则返回key本身
            return message == null ? key : message;
      });
    }

    @Override
    public Map&lt;String, String&gt; loadAllMessagesToCache() {
      List&lt;SysI18nMessage&gt; messageList = baseMapper.listAllMessages();
      Map&lt;String, String&gt; messageMap = new HashMap&lt;&gt;();
      for (SysI18nMessage message : messageList) {
            String cacheKey = message.getMessageKey() + "_" + message.getLanguage();
            messageMap.put(cacheKey, message.getMessageValue());
      }
      // 将所有消息放入缓存
      i18nCache.putAll(messageMap);
      return messageMap;
    }

    @Override
    public void refreshCache() {
      // 清空缓存并重新加载
      i18nCache.invalidateAll();
      loadAllMessagesToCache();
    }
}
</pre></div>
<p><strong>核心说明</strong>:</p>
<ul><li><code>@PostConstruct</code>:项目启动时执行<code>initCache()</code>,预加载所有消息到缓存,提升首次访问性能。</li><li>Caffeine缓存:设置1小时过期+最大容量,避免缓存膨胀;缓存Key为<code>消息键_语言</code>(如<code>user.name_zh_CN</code>)。</li><li>缓存穿透处理:数据库未找到消息时,返回消息键本身,避免缓存穿透。</li></ul>
<p class="maodian"></p><h4>3.4.4 自定义MessageSource</h4>
<p>创建<code>config/DbMessageSource.java</code>,继承<code>AbstractMessageSource</code>(Spring提供的MessageSource抽象实现):</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.config;

import com.example.i18n.service.SysI18nMessageService;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.text.MessageFormat;
import java.util.Locale;

/**
* 基于数据库的MessageSource实现
*/
@Component
public class DbMessageSource extends AbstractMessageSource {

    @Resource
    private SysI18nMessageService sysI18nMessageService;

    /**
   * 核心方法:解析消息
   */
    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
      // 获取语言标识(如zh_CN)
      String language = locale.toString();
      // 从数据库+缓存中获取消息值
      String message = sysI18nMessageService.getMessage(code, language);
      // 若未找到,尝试使用默认语言(zh_CN)
      if (message.equals(code) &amp;&amp; !language.equals("zh_CN")) {
            message = sysI18nMessageService.getMessage(code, "zh_CN");
      }
      // 封装为MessageFormat(支持参数替换)
      return createMessageFormat(message, locale);
    }

    /**
   * 重载方法:直接返回字符串(简化使用)
   */
    public String getMessage(String code, Locale locale) {
      return resolveCode(code, locale).format(null);
    }

    public String getMessage(String code, Object[] args, Locale locale) {
      return resolveCode(code, locale).format(args);
    }
}
</pre></div>
<p class="maodian"></p><h4>3.4.5 替换默认MessageSource</h4>
<p>在<code>I18nConfig.java</code>中注册自定义的<code>DbMessageSource</code>,替换SpringBoot默认的<code>ResourceBundleMessageSource</code>:</p>
<div class="jb51code"><pre class="brush:java;">/**
* 注册数据库版MessageSource,优先级高于默认实现
*/
@Bean
@Primary // 标记为首选Bean
public MessageSource messageSource(SysI18nMessageService sysI18nMessageService) {
    DbMessageSource messageSource = new DbMessageSource();
    // 设置默认语言(与之前保持一致)
    messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    // 设置编码
    messageSource.setDefaultEncoding("UTF-8");
    return messageSource;
}
</pre></div>
<p class="maodian"></p><h3>3.5 业务集成与测试</h3>
<p class="maodian"></p><h4>3.5.1 工具类适配</h4>
<p>修改<code>I18nUtils.java</code>,注入自定义的<code>DbMessageSource</code>:</p>
<div class="jb51code"><pre class="brush:java;">// 替换原有的MessageSource为自定义的DbMessageSource
@Resource
private DbMessageSource messageSource;

// 其余方法无需修改,逻辑完全兼容
</pre></div>
<p class="maodian"></p><h4>3.5.2 消息管理接口</h4>
<p>创建<code>controller/I18nManageController.java</code>,提供消息的新增/修改/刷新缓存接口:</p>
<div class="jb51code"><pre class="brush:java;">package com.example.i18n.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.i18n.entity.SysI18nMessage;
import com.example.i18n.service.SysI18nMessageService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Map;

/**
* 国际化消息管理接口(动态配置)
*/
@RestController
@RequestMapping("/api/i18n/manage")
public class I18nManageController {

    @Resource
    private SysI18nMessageService sysI18nMessageService;

    /**
   * 新增/修改国际化消息
   */
    @PostMapping("/save")
    public String saveMessage(@RequestBody SysI18nMessage message) {
      // 先删除已存在的同Key+语言的消息
      LambdaQueryWrapper&lt;SysI18nMessage&gt; wrapper = new LambdaQueryWrapper&lt;&gt;();
      wrapper.eq(SysI18nMessage::getMessageKey, message.getMessageKey())
                .eq(SysI18nMessage::getLanguage, message.getLanguage());
      sysI18nMessageService.remove(wrapper);
      // 保存新消息
      sysI18nMessageService.save(message);
      // 刷新缓存
      sysI18nMessageService.refreshCache();
      return "操作成功";
    }

    /**
   * 刷新缓存
   */
    @GetMapping("/refresh-cache")
    public String refreshCache() {
      sysI18nMessageService.refreshCache();
      return "缓存刷新成功";
    }

    /**
   * 查询所有消息
   */
    @GetMapping("/list-all")
    public Map&lt;String, String&gt; listAllMessages() {
      return sysI18nMessageService.loadAllMessagesToCache();
    }
}
</pre></div>
<p class="maodian"></p><h4>3.5.3 测试验证</h4>
<ol><li><strong>基础消息查询</strong>:访问<code>http://localhost:8080/api/i18n/basic?lang=en_US</code>,返回数据库中的英文消息。</li><li><strong>动态修改消息</strong>:<ul><li>调用POST接口<code>http://localhost:8080/api/i18n/manage/save</code>,传入JSON:</li></ul></li></ol>
<div class="jb51code"><pre class="brush:java;">{
"messageKey": "user.name",
"language": "en_US",
"messageValue": "User Name"
}
</pre></div>
<ul><li>调用刷新缓存接口:<code>http://localhost:8080/api/i18n/manage/refresh-cache</code>。</li><li>再次访问基础查询接口,<code>user.name</code>会返回<code>User Name</code>(无需重启服务)。</li></ul>
<p class="maodian"></p><h2>四、进阶优化措施</h2>
<p class="maodian"></p><h3>4.1 缓存优化(数据库方式)</h3>
<ol><li><strong>多级缓存</strong>:结合Caffeine(本地缓存)+ Redis(分布式缓存),适配集群场景。</li><li><strong>缓存预热</strong>:项目启动时预加载所有消息到缓存,避免首次访问数据库。</li><li><strong>缓存主动失效</strong>:消息修改后立即刷新缓存,而非等待过期。</li><li><strong>批量加载</strong>:分页加载大量消息,避免一次性加载过多数据导致内存溢出。</li></ol>
<p class="maodian"></p><h3>4.2 语言解析器增强</h3>
<p>扩展<code>LocaleResolver</code>,支持多维度语言解析(优先级从高到低):</p>
<ol><li>请求参数(<code>lang</code>)&rarr; 2. Cookie &rarr; 3. 请求头(<code>Accept-Language</code>)&rarr; 4. Session &rarr; 5. 默认语言。</li></ol>
<p>示例代码:</p>
<div class="jb51code"><pre class="brush:java;">@Component
public class CustomLocaleResolver implements LocaleResolver {

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
      // 1. 优先从请求参数获取
      String lang = request.getParameter("lang");
      if (StringUtils.hasText(lang)) {
            String[] split = lang.split("_");
            return new Locale(split, split);
      }
      // 2. 从Cookie获取
      Cookie[] cookies = request.getCookies();
      if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("lang".equals(cookie.getName())) {
                  String[] split = cookie.getValue().split("_");
                  return new Locale(split, split);
                }
            }
      }
      // 3. 从请求头获取
      String acceptLanguage = request.getHeader("Accept-Language");
      if (StringUtils.hasText(acceptLanguage)) {
            return Locale.forLanguageTag(acceptLanguage.split(","));
      }
      // 4. 默认语言
      return Locale.SIMPLIFIED_CHINESE;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
      // 设置Cookie,有效期7天
      Cookie cookie = new Cookie("lang", locale.toString());
      cookie.setMaxAge(60 * 60 * 24 * 7);
      cookie.setPath("/");
      response.addCookie(cookie);
    }
}
</pre></div>
<p class="maodian"></p><h3>4.3 动态刷新配置(配置文件方式)</h3>
<p>使用<code>Spring Cloud Config</code>或<code>Nacos</code>实现配置文件的动态刷新,无需重启服务:</p>
<ol><li>将多语言配置文件放到配置中心。</li><li>引入<code>spring-cloud-starter-config</code>依赖。</li><li>配置<code>@RefreshScope</code>,实现配置热更新。</li></ol>
<p class="maodian"></p><h3>4.4 异常处理国际化</h3>
<p>全局异常处理器中使用国际化工具类,返回多语言异常信息:</p>
<div class="jb51code"><pre class="brush:java;">@RestControllerAdvice
public class GlobalExceptionHandler {

    @Resource
    private I18nUtils i18nUtils;

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity&lt;Map&lt;String, String&gt;&gt; handleValidationException(MethodArgumentNotValidException e) {
      Map&lt;String, String&gt; errors = new HashMap&lt;&gt;();
      e.getBindingResult().getFieldErrors().forEach(fieldError -&gt; {
            // 获取国际化后的校验提示
            String message = i18nUtils.getMessage(fieldError.getDefaultMessage());
            errors.put(fieldError.getField(), message);
      });
      return ResponseEntity.badRequest().body(errors);
    }
}
</pre></div>
<p class="maodian"></p><h2>五、常见问题与解决方案</h2>
<p class="maodian"></p><h3>5.1 校验注解(@NotNull等)国际化适配</h3>
<p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><h4>问题描述</h4>
<p>直接使用<code>@NotNull(message = &quot;主键不能为空&quot;)</code>是硬编码,无法实现国际化。</p>
<p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><p class="maodian"></p><h4>解决方案</h4>
<ol><li><strong>核心原理</strong>:JSR-303校验框架默认读取<code>ValidationMessages.properties</code>配置文件,可将校验消息键指向i18n配置。</li><li><strong>实现步骤</strong>:<ul><li>步骤1:在i18n配置文件/数据库中添加校验消息键(如<code>validate.required.id=主键不能为空</code>)。</li><li>步骤2:校验注解中使用<code>{键名}</code>引用国际化消息:</li></ul></li></ol>
<div class="jb51code"><pre class="brush:java;">public class UserDTO {
    @NotNull(message = "{validate.required.id}")
    private Long id;

    @NotBlank(message = "{validate.required.name}")
    private String name;
    // 省略getter/setter
}
</pre></div>
<ul><li>步骤3:配置校验框架使用自定义的MessageSource:</li></ul>
<div class="jb51code"><pre class="brush:java;">@Bean
public Validator validator(MessageSource messageSource) {
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    // 设置校验消息源为自定义的DbMessageSource/ResourceBundleMessageSource
    validator.setValidationMessageSource(messageSource);
    return validator;
}

@Override
public Validator getValidator() {
    return validator(messageSource);
}
</pre></div>
<p class="maodian"></p><h3>5.2 配置文件中文乱码</h3>
<h4>问题描述</h4>
<p>properties文件中写中文,读取后显示乱码。</p>
<h4>解决方案</h4>
<ol><li>IDE配置:IDEA中设置<code>File &rarr; Settings &rarr; Editor &rarr; File Encodings</code>:
<ul><li><code>Properties Files (*.properties)</code>:编码设为UTF-8。</li><li>勾选<code>Transparent native-to-ascii conversion</code>。</li></ul></li><li>配置文件指定编码:<code>spring.messages.encoding=UTF-8</code>。</li><li>手动转码:使用<code>native2ascii</code>工具将中文转为ASCII编码(不推荐)。</li></ol>
<p class="maodian"></p><h3>5.3 默认语言不生效</h3>
<h4>问题描述</h4>
<p>配置了默认语言,但未传参时仍使用系统语言。</p>
<h4>解决方案</h4>
<ol><li>检查<code>LocaleResolver</code>是否设置了<code>setDefaultLocale()</code>。</li><li>确认<code>application.yml</code>中<code>spring.messages.fallback-to-system-locale=false</code>(禁用系统语言回退)。</li><li>检查自定义<code>LocaleResolver</code>的<code>resolveLocale</code>方法,默认分支是否返回指定的默认Locale。</li></ol>
<p class="maodian"></p><h3>5.4 数据库方式性能问题</h3>
<h4>问题描述</h4>
<p>高并发场景下,数据库查询频繁,响应慢。</p>
<h4>解决方案</h4>
<ol><li>增加Caffeine本地缓存,设置合理的过期时间和最大容量。</li><li>集群场景下使用Redis分布式缓存,避免每个节点都查询数据库。</li><li>对热点消息(如通用提示)进行永久缓存,非热点消息设置较短过期时间。</li><li>数据库表添加索引(已在建表语句中添加<code>uk_key_language</code>唯一索引)。</li></ol>
<p class="maodian"></p><h3>5.5 动态修改语言后不生效(适配 Header 方式)</h3>
<h4>问题描述</h4>
<p>传参<code>lang=en_US</code>后,返回的仍为默认语言。</p>
<h4>解决方案</h4>
<ol><li>检查自定义 Header 拦截器是否注册到 SpringMVC 拦截器链,且拦截路径包含目标接口(需确保addPathPatterns(&ldquo;/**&rdquo;))。</li><li>确认 Header 拦截器中request.getHeader(&ldquo;lang&rdquo;)的 Header 名称与实际请求一致(如前端传的是Lang/LANG会导致读取不到,HTTP Header 不区分大小写,但建议统一小写)。</li><li>检查拦截器中 Locale 解析逻辑:</li><li>确认lang的格式是否符合解析规则(如en_US是下划线分隔,而非en-US);</li><li>检查异常处理逻辑,若解析失败是否回退到默认 Locale(避免解析异常导致 Locale 未设置)。</li><li>检查LocaleResolver的setLocale方法是否正确实现(如SessionLocaleResolver需确保 Session</li><li>正常生效,无 Session 失效 / 隔离问题)。</li><li>排查是否存在拦截器执行顺序问题:确保语言拦截器优先于其他业务拦截器执行(可通过order()指定优先级,如registry.addInterceptor(xxx).order(0))。</li></ol>
<p class="maodian"></p><h3>5.6 数据库消息未找到时返回Key本身</h3>
<h4>问题描述</h4>
<p>数据库中未配置某个消息键,返回的是键名而非兜底消息。</p>
<h4>解决方案</h4>
<p>在<code>DbMessageSource</code>的<code>resolveCode</code>方法中,增加兜底逻辑:</p>
<div class="jb51code"><pre class="brush:java;">// 若未找到当前语言的消息,尝试默认语言,仍未找到则返回兜底提示
if (message.equals(code)) {
    message = sysI18nMessageService.getMessage(code, "zh_CN");
    if (message.equals(code)) {
      message = "未找到对应的提示信息:" + code;
    }
}
</pre></div>
<p class="maodian"></p><h3>5.7 Header 中语言标识格式错误导致切换失败</h3>
<h4>问题描述</h4>
<p>Header 传入Accept-Language=en-US(中划线分隔)或lang=english(非标准格式),语言切换不生效,始终返回默认语言。</p>
<h4>解决方案</h4>
<ol><li>拦截器中增加格式兼容逻辑,支持中划线 / 下划线两种格式:</li></ol>
<div class="jb51code"><pre class="brush:java;">// 兼容en-US、zh-CN等中划线格式
String langHeader = request.getHeader("Accept-Language").replace("-", "_");
</pre></div>
<ol start="2"><li>增加语言标识白名单校验,仅允许合法的语言值:</li></ol>
<div class="jb51code"><pre class="brush:java;">// 定义合法语言列表
Set&lt;String&gt; validLangs = new HashSet&lt;&gt;(Arrays.asList("zh_CN", "zh_TW", "en_US"));
if (validLangs.contains(langHeader)) {
    // 正常解析
    String[] langParts = langHeader.split("_");
    Locale locale = new Locale(langParts, langParts);
    localeResolver().setLocale(request, response, locale);
} else {
    // 非法值使用默认语言
    localeResolver().setLocale(request, response, Locale.SIMPLIFIED_CHINESE);
}
</pre></div>
<ol start="3"><li>前端规范:约定前端仅传递zh_CN/zh_TW/en_US三种格式,避免非法值。</li></ol>
<p class="maodian"></p><h3>5.8 跨域请求时 Header 中的 lang 参数丢失</h3>
<h4>问题描述</h4>
<p>前后端分离项目中,前端跨域请求时携带Accept-Language Header,但后端无法读取到该值,语言切换失效。</p>
<h4>解决方案</h4>
<ol><li>配置跨域(CORS)允许自定义 Header:</li></ol>
<div class="jb51code"><pre class="brush:java;">@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
      registry.addMapping("/**")
                .allowedOriginPatterns("*") // 生产环境替换为具体域名
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("Accept-Language", "Content-Type") // 允许lang Header
                .exposedHeaders("Accept-Language") // 暴露lang Header(可选)
                .allowCredentials(true)
                .maxAge(3600);
    }
}
</pre></div>
<ol start="2"><li>前端请求时确保携带<code>Accept-Language</code> Header 且跨域请求开启<code>withCredentials</code>(若后端配置了<code>allowCredentials=true</code>):</li></ol>
<div class="jb51code"><pre class="brush:java;">// Axios示例
axios({
url: "http://localhost:8080/api/i18n/basic",
method: "GET",
headers: {
    "Accept-Language": "en_US"
},
withCredentials: true // 关键:跨域携带Cookie/Session(SessionLocaleResolver依赖)
});
</pre></div>
<p class="maodian"></p><h3>5.9 自定义 MessageSource 优先级低于默认实现导致校验注解国际化不生效</h3>
<h4>问题描述</h4>
<p>校验注解中使用<code>{validate.required.id}</code>引用国际化键,但返回的仍是键名而非国际化值</p>
<h4>解决方案</h4>
<ol><li>确保自定义的<code>MessageSource</code>(如<code>DbMessageSource</code>)添加<code>@Primary</code>注解,优先级高于默认的<code>ResourceBundleMessageSource</code>:</li></ol>
<div class="jb51code"><pre class="brush:java;">@Bean
@Primary // 标记为首选Bean
public MessageSource messageSource(SysI18nMessageService sysI18nMessageService) {
    DbMessageSource messageSource = new DbMessageSource();
    messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
    messageSource.setDefaultEncoding("UTF-8");
    return messageSource;
}
</pre></div>
<ol start="2"><li>检查校验器配置是否正确注入自定义<code>MessageSource</code>,而非默认实现:</li></ol>
<div class="jb51code"><pre class="brush:java;">// 确保注入的是自定义的MessageSource
@Resource
private MessageSource messageSource;

@Bean
public Validator validator() {
    LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
    validator.setValidationMessageSource(messageSource);
    return validator;
}
</pre></div>
<ol start="3"><li>排查是否存在多个<code>MessageSource</code> Bean,导致 Spring 注入错误的实例。</li></ol>
<p class="maodian"></p><h2>六、总结</h2>
<p class="maodian"></p><h3>6.1 两种方式对比</h3>
<table><thead><tr><th>特性</th><th>基于配置文件</th><th>基于数据库</th></tr></thead><tbody><tr><td>灵活性</td><td>低(需重启服务)</td><td>高(运行时动态修改)</td></tr><tr><td>性能</td><td>高(内存加载)</td><td>中(需缓存优化)</td></tr><tr><td>维护成本</td><td>低(文件管理)</td><td>高(需开发管理接口)</td></tr><tr><td>适用场景</td><td>小型系统、消息变更少</td><td>大型系统、多语言频繁变更</td></tr></tbody></table>
<p class="maodian"></p><h3>6.2 核心要点回顾</h3>
<ol><li>SpringBoot i18n的核心是<code>MessageSource</code>、<code>LocaleResolver</code>、<code>LocaleChangeInterceptor</code>三大组件。</li><li>配置文件方式需遵循命名规则,注意编码问题;数据库方式需自定义<code>MessageSource</code>并结合缓存优化。</li><li>校验注解国际化需将message值设为<code>{键名}</code>,并配置校验框架使用自定义MessageSource。</li><li>生产环境中,数据库方式需做好缓存优化,配置文件方式可结合配置中心实现动态刷新。</li></ol>
<p>通过本文的两种实现方案,你可以根据项目规模和需求选择合适的国际化方式,同时规避常见问题,实现高效、稳定的多语言适配。</p>
<p>以上就是SpringBoot实现i18n国际化的两种企业级方案的详细内容,更多关于SpringBoot实现i18n国际化的资料请关注琼殿技术社区其它相关文章!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>SpringBoot实现国际化i18n详解</li><li>JAVA Springboot配置i18n国际化语言详细步骤</li><li>SpringBoot集成I18n国际化文件在jar包外生效问题</li><li>基于springboot i18n国际化后台多种语言设置的方式</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: SpringBoot实现i18n国际化的两种企业级方案