前言
在全球化业务场景下,系统适配多语言已成为标配需求。SpringBoot作为主流的Java开发框架,提供了完善的国际化(i18n,internationalization的缩写,因i和n之间有18个字母得名)解决方案。本文将从实战角度出发,完整讲解两种企业级i18n实现方案:基于配置文件的静态实现 (适配简体中文、繁体中文、英文)和基于数据库的动态实现(支持运行时修改语言配置),同时覆盖校验注解国际化、性能优化、常见问题排查等核心要点,所有代码均可直接落地到生产项目。
一、国际化基础认知
1.1 核心概念
i18n的核心目标是让系统在不修改代码的前提下,通过配置适配不同语言和地区的使用习惯。SpringBoot中实现i18n的核心依赖是:
MessageSource:消息源接口,负责加载和解析多语言消息,默认实现为ResourceBundleMessageSource(基于配置文件)。Accept-Language:语言地区标识,格式为语言代码_国家/地区代码,如:- 简体中文:
zh_CN - 繁体中文:
zh_TW - 英文(美国):
en_US
- 简体中文:
LocaleResolver:语言解析器,负责从请求中获取/设置当前Locale。LocaleChangeInterceptor:语言切换拦截器,用于拦截请求参数实现语言动态切换。
1.2 核心原理
SpringBoot启动时,MessageSource会加载指定路径下的多语言配置文件;当业务代码获取国际化消息时,框架会根据当前Accept-Language从对应配置文件/数据源中匹配消息键(Key),返回对应的消息值(Value)。
二、方式一:基于配置文件的i18n实现
2.1 环境准备
2.1.1 依赖配置
新建SpringBoot项目(推荐2.7.x或3.2.x),核心依赖仅需spring-boot-starter-web,无需额外依赖:
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 可选:简化配置文件编写(.yml) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
2.1.2 目录结构
在resources目录下创建i18n文件夹,用于存放多语言配置文件,最终目录结构:
resources/
├── application.yml # 核心配置
└── i18n/ # 国际化配置文件目录
├── messages.properties # 默认配置(无语言标识)
├── messages_zh_CN.properties # 简体中文
├── messages_zh_TW.properties # 繁体中文
└── messages_en_US.properties # 英文
2.2 多语言配置文件编写
2.2.1 命名规则
配置文件命名必须遵循basename_语言代码_国家代码.properties规则:
basename:自定义前缀(如messages),需在application.yml中配置。- 无语言标识的
messages.properties为默认配置,当匹配不到指定Locale的配置时,会使用该文件内容。
2.2.2 配置文件内容
- 默认配置(messages.properties):兜底使用,建议与默认语言(简体中文)保持一致
properties
# 通用提示
common.submit=提交
common.cancel=取消
# 用户相关
user.name=用户名
user.age=年龄
# 校验提示
validate.required.id=主键不能为空
validate.required.name=姓名不能为空
- 简体中文(messages_zh_CN.properties):
properties
# 通用提示
common.submit=提交
common.cancel=取消
# 用户相关
user.name=用户名
user.age=年龄
# 校验提示
validate.required.id=主键不能为空
validate.required.name=姓名不能为空
- 繁体中文(messages_zh_TW.properties):
properties
# 通用提示
common.submit=提交
common.cancel=取消
# 用户相关
user.name=使用者名稱
user.age=年齡
# 校验提示
validate.required.id=主鍵不能為空
validate.required.name=姓名不能為空
- 英文(messages_en_US.properties):
properties
# 通用提示
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
注意:properties文件默认编码为ISO-8859-1,直接写中文会乱码!需将IDE的properties文件编码设置为UTF-8(IDEA:Settings → File Encodings → Properties Files → 勾选Transparent native-to-ascii conversion)。
2.3 SpringBoot核心配置
在application.yml中配置国际化相关参数,指定配置文件路径、默认语言、编码等:
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
2.4 自定义语言解析器与拦截器
默认情况下,SpringBoot仅支持从请求头Accept-Language获取Locale,为了方便通过请求参数(如?Accept-Language=en-US)切换语言,需自定义LocaleResolver并注册拦截器。
2.4.1 自定义LocaleResolver
创建config/I18nConfig.java,实现LocaleResolver接口:
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 && !acceptLanguage.isEmpty()) {
try {
// 取第一个语言项(如 "zh-CN,en;q=0.8" → "zh-CN")
String primary = acceptLanguage.split(",")[0].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); // 优先级最高
}
}
关键说明:
SessionLocaleResolver:将Locale存储在Session中,一次切换后,后续请求无需重复传参。localeHeaderInterceptor:拦截请求头Header中Accept-Language参数,自动更新当前Locale(如Accept-Language=zh-TW会切换为繁体中文)。
2.5 国际化消息使用示例
2.5.1 工具类封装(推荐)
创建utils/I18nUtils.java,封装获取国际化消息的方法,简化业务使用:
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;
}
}
}
核心API说明:
LocaleContextHolder.getLocale():获取当前线程的Locale(由LocaleResolver解析)。messageSource.getMessage(key, args, locale):核心方法,参数说明:key:消息键(如user.name)。args:消息参数(用于替换消息中的占位符,如user.hello=你好{0})。locale:指定语言标识。
2.5.2 控制器使用示例
创建controller/I18nController.java,编写接口测试国际化效果:
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<String, String> getBasicMessage() {
Map<String, String> result = new HashMap<>();
// 获取当前语言的消息
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<String, String> getMessageWithParams() {
Map<String, String> result = new HashMap<>();
// 模拟带参数的消息(需先在配置文件中添加: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<String, String> getMessageByManualLocale() {
Map<String, String> result = new HashMap<>();
// 手动指定繁体中文
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;
}
}
2.6 测试验证
启动项目后,通过Postman/Browser访问以下地址验证效果:
-
简体中文:
http://localhost:8080/api/i18n/basic?lang=zh_CN返回:
json{ "user.name":"用户名", "common.submit":"提交", "validate.required.id":"主键不能为空" } -
繁体中文:
http://localhost:8080/api/i18n/basic?lang=zh_TW返回:
json{ "user.name":"使用者名稱", "common.submit":"提交", "validate.required.id":"主鍵不能為空" } -
英文:
http://localhost:8080/api/i18n/basic?lang=en_US返回:
json{ "user.name":"Username", "common.submit":"Submit", "validate.required.id":"Primary key cannot be empty" }
2.7 默认语言切换说明
默认语言的生效优先级:
SessionLocaleResolver中设置的setDefaultLocale()(代码级)。application.yml中spring.messages.default-locale(配置级)。- 系统默认Locale(兜底)。
若需修改默认语言为英文,只需调整两处:
java
// 1. I18nConfig中
localeResolver.setDefaultLocale(Locale.US);
// 2. application.yml中
spring:
messages:
default-locale: en_US
三、方式二:基于数据库的动态i18n实现
基于配置文件的方式存在明显缺陷:修改消息需重启服务。基于数据库的实现可实现运行时动态配置多语言消息,适合频繁变更或大规模多语言场景。
3.1 设计思路
- 设计数据库表存储多语言消息(键、语言、值)。
- 自定义
MessageSource实现,重写消息解析逻辑,从数据库加载消息。 - 引入缓存(Caffeine)提升性能,避免频繁查询数据库。
- 提供接口实现消息的新增/修改/删除,支持动态刷新缓存。
3.2 数据库表设计
3.2.1 建表语句(MySQL)
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='国际化消息表';
3.2.2 测试数据插入
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');
3.3 环境准备
3.3.1 添加依赖
在原有依赖基础上,添加数据库相关依赖(以MyBatis-Plus为例):
xml
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus(简化CRUD) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- 缓存:Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.20</version>
</dependency>
3.3.2 数据库配置
在application.yml中添加数据库配置:
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&characterEncoding=utf8&useSSL=false&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
3.4 核心组件开发
3.4.1 实体类
创建entity/SysI18nMessage.java:
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;
}
3.4.2 Mapper接口
创建mapper/SysI18nMessageMapper.java:
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<SysI18nMessage> {
/**
* 根据消息键和语言查询消息
*/
@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<SysI18nMessage> listAllMessages();
}
3.4.3 Service层
创建service/SysI18nMessageService.java(接口):
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<SysI18nMessage> {
/**
* 根据键和语言获取消息
*/
String getMessage(String key, String language);
/**
* 加载所有消息到缓存
*/
Map<String, String> loadAllMessagesToCache();
/**
* 刷新缓存
*/
void refreshCache();
}
创建service/impl/SysI18nMessageServiceImpl.java(实现类):
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<SysI18nMessageMapper, SysI18nMessage> implements SysI18nMessageService {
/**
* 缓存Key规则:messageKey + "_" + language
*/
private final Cache<String, String> 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 -> {
String message = baseMapper.getMessageByKeyAndLanguage(key, language);
// 数据库未找到则返回key本身
return message == null ? key : message;
});
}
@Override
public Map<String, String> loadAllMessagesToCache() {
List<SysI18nMessage> messageList = baseMapper.listAllMessages();
Map<String, String> messageMap = new HashMap<>();
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();
}
}
核心说明:
@PostConstruct:项目启动时执行initCache(),预加载所有消息到缓存,提升首次访问性能。- Caffeine缓存:设置1小时过期+最大容量,避免缓存膨胀;缓存Key为
消息键_语言(如user.name_zh_CN)。 - 缓存穿透处理:数据库未找到消息时,返回消息键本身,避免缓存穿透。
3.4.4 自定义MessageSource
创建config/DbMessageSource.java,继承AbstractMessageSource(Spring提供的MessageSource抽象实现):
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) && !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);
}
}
3.4.5 替换默认MessageSource
在I18nConfig.java中注册自定义的DbMessageSource,替换SpringBoot默认的ResourceBundleMessageSource:
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;
}
3.5 业务集成与测试
3.5.1 工具类适配
修改I18nUtils.java,注入自定义的DbMessageSource:
java
// 替换原有的MessageSource为自定义的DbMessageSource
@Resource
private DbMessageSource messageSource;
// 其余方法无需修改,逻辑完全兼容
3.5.2 消息管理接口
创建controller/I18nManageController.java,提供消息的新增/修改/刷新缓存接口:
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<SysI18nMessage> wrapper = new LambdaQueryWrapper<>();
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<String, String> listAllMessages() {
return sysI18nMessageService.loadAllMessagesToCache();
}
}
3.5.3 测试验证
- 基础消息查询 :访问
http://localhost:8080/api/i18n/basic?lang=en_US,返回数据库中的英文消息。 - 动态修改消息 :
-
调用POST接口
http://localhost:8080/api/i18n/manage/save,传入JSON:json{ "messageKey": "user.name", "language": "en_US", "messageValue": "User Name" } -
调用刷新缓存接口:
http://localhost:8080/api/i18n/manage/refresh-cache。 -
再次访问基础查询接口,
user.name会返回User Name(无需重启服务)。
-
四、进阶优化措施
4.1 缓存优化(数据库方式)
- 多级缓存:结合Caffeine(本地缓存)+ Redis(分布式缓存),适配集群场景。
- 缓存预热:项目启动时预加载所有消息到缓存,避免首次访问数据库。
- 缓存主动失效:消息修改后立即刷新缓存,而非等待过期。
- 批量加载:分页加载大量消息,避免一次性加载过多数据导致内存溢出。
4.2 语言解析器增强
扩展LocaleResolver,支持多维度语言解析(优先级从高到低):
- 请求参数(
lang)→ 2. Cookie → 3. 请求头(Accept-Language)→ 4. Session → 5. 默认语言。
示例代码:
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[0], split[1]);
}
// 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[0], split[1]);
}
}
}
// 3. 从请求头获取
String acceptLanguage = request.getHeader("Accept-Language");
if (StringUtils.hasText(acceptLanguage)) {
return Locale.forLanguageTag(acceptLanguage.split(",")[0]);
}
// 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);
}
}
4.3 动态刷新配置(配置文件方式)
使用Spring Cloud Config或Nacos实现配置文件的动态刷新,无需重启服务:
- 将多语言配置文件放到配置中心。
- 引入
spring-cloud-starter-config依赖。 - 配置
@RefreshScope,实现配置热更新。
4.4 异常处理国际化
全局异常处理器中使用国际化工具类,返回多语言异常信息:
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@Resource
private I18nUtils i18nUtils;
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationException(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getFieldErrors().forEach(fieldError -> {
// 获取国际化后的校验提示
String message = i18nUtils.getMessage(fieldError.getDefaultMessage());
errors.put(fieldError.getField(), message);
});
return ResponseEntity.badRequest().body(errors);
}
}
五、常见问题与解决方案
5.1 校验注解(@NotNull等)国际化适配
问题描述
直接使用@NotNull(message = "主键不能为空")是硬编码,无法实现国际化。
解决方案
- 核心原理 :JSR-303校验框架默认读取
ValidationMessages.properties配置文件,可将校验消息键指向i18n配置。 - 实现步骤 :
-
步骤1:在i18n配置文件/数据库中添加校验消息键(如
validate.required.id=主键不能为空)。 -
步骤2:校验注解中使用
{键名}引用国际化消息:javapublic class UserDTO { @NotNull(message = "{validate.required.id}") private Long id; @NotBlank(message = "{validate.required.name}") private String name; // 省略getter/setter } -
步骤3:配置校验框架使用自定义的MessageSource:
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); }
-
5.2 配置文件中文乱码
问题描述
properties文件中写中文,读取后显示乱码。
解决方案
- IDE配置:IDEA中设置
File → Settings → Editor → File Encodings:Properties Files (*.properties):编码设为UTF-8。- 勾选
Transparent native-to-ascii conversion。
- 配置文件指定编码:
spring.messages.encoding=UTF-8。 - 手动转码:使用
native2ascii工具将中文转为ASCII编码(不推荐)。
5.3 默认语言不生效
问题描述
配置了默认语言,但未传参时仍使用系统语言。
解决方案
- 检查
LocaleResolver是否设置了setDefaultLocale()。 - 确认
application.yml中spring.messages.fallback-to-system-locale=false(禁用系统语言回退)。 - 检查自定义
LocaleResolver的resolveLocale方法,默认分支是否返回指定的默认Locale。
5.4 数据库方式性能问题
问题描述
高并发场景下,数据库查询频繁,响应慢。
解决方案
- 增加Caffeine本地缓存,设置合理的过期时间和最大容量。
- 集群场景下使用Redis分布式缓存,避免每个节点都查询数据库。
- 对热点消息(如通用提示)进行永久缓存,非热点消息设置较短过期时间。
- 数据库表添加索引(已在建表语句中添加
uk_key_language唯一索引)。
5.5 动态修改语言后不生效(适配 Header 方式)
问题描述
传参lang=en_US后,返回的仍为默认语言。
解决方案
-
检查自定义 Header 拦截器是否注册到 SpringMVC 拦截器链,且拦截路径包含目标接口(需确保addPathPatterns("/**"))。
-
确认 Header 拦截器中request.getHeader("lang")的 Header 名称与实际请求一致(如前端传的是Lang/LANG会导致读取不到,HTTP Header 不区分大小写,但建议统一小写)。
-
检查拦截器中 Locale 解析逻辑:
确认lang的格式是否符合解析规则(如en_US是下划线分隔,而非en-US);
检查异常处理逻辑,若解析失败是否回退到默认 Locale(避免解析异常导致 Locale 未设置)。
-
检查LocaleResolver的setLocale方法是否正确实现(如SessionLocaleResolver需确保 Session
正常生效,无 Session 失效 / 隔离问题)。
-
排查是否存在拦截器执行顺序问题:确保语言拦截器优先于其他业务拦截器执行(可通过order()指定优先级,如registry.addInterceptor(xxx).order(0))。
5.6 数据库消息未找到时返回Key本身
问题描述
数据库中未配置某个消息键,返回的是键名而非兜底消息。
解决方案
在DbMessageSource的resolveCode方法中,增加兜底逻辑:
java
// 若未找到当前语言的消息,尝试默认语言,仍未找到则返回兜底提示
if (message.equals(code)) {
message = sysI18nMessageService.getMessage(code, "zh_CN");
if (message.equals(code)) {
message = "未找到对应的提示信息:" + code;
}
}
5.7 Header 中语言标识格式错误导致切换失败
问题描述
Header 传入Accept-Language=en-US(中划线分隔)或lang=english(非标准格式),语言切换不生效,始终返回默认语言。
解决方案
- 拦截器中增加格式兼容逻辑,支持中划线 / 下划线两种格式:
java
// 兼容en-US、zh-CN等中划线格式
String langHeader = request.getHeader("Accept-Language").replace("-", "_");
- 增加语言标识白名单校验,仅允许合法的语言值:
java
// 定义合法语言列表
Set<String> validLangs = new HashSet<>(Arrays.asList("zh_CN", "zh_TW", "en_US"));
if (validLangs.contains(langHeader)) {
// 正常解析
String[] langParts = langHeader.split("_");
Locale locale = new Locale(langParts[0], langParts[1]);
localeResolver().setLocale(request, response, locale);
} else {
// 非法值使用默认语言
localeResolver().setLocale(request, response, Locale.SIMPLIFIED_CHINESE);
}
- 前端规范:约定前端仅传递zh_CN/zh_TW/en_US三种格式,避免非法值。
5.8 跨域请求时 Header 中的 lang 参数丢失
问题描述
前后端分离项目中,前端跨域请求时携带Accept-Language Header,但后端无法读取到该值,语言切换失效。
解决方案
- 配置跨域(CORS)允许自定义 Header:
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);
}
}
- 前端请求时确保携带
Accept-LanguageHeader 且跨域请求开启withCredentials(若后端配置了allowCredentials=true):
java
// Axios示例
axios({
url: "http://localhost:8080/api/i18n/basic",
method: "GET",
headers: {
"Accept-Language": "en_US"
},
withCredentials: true // 关键:跨域携带Cookie/Session(SessionLocaleResolver依赖)
});
5.9 自定义 MessageSource 优先级低于默认实现导致校验注解国际化不生效
问题描述
校验注解中使用{validate.required.id}引用国际化键,但返回的仍是键名而非国际化值
解决方案
- 确保自定义的
MessageSource(如DbMessageSource)添加@Primary注解,优先级高于默认的ResourceBundleMessageSource:
java
@Bean
@Primary // 标记为首选Bean
public MessageSource messageSource(SysI18nMessageService sysI18nMessageService) {
DbMessageSource messageSource = new DbMessageSource();
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
- 检查校验器配置是否正确注入自定义
MessageSource,而非默认实现:
java
// 确保注入的是自定义的MessageSource
@Resource
private MessageSource messageSource;
@Bean
public Validator validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource);
return validator;
}
- 排查是否存在多个
MessageSourceBean,导致 Spring 注入错误的实例。
六、总结
6.1 两种方式对比
| 特性 | 基于配置文件 | 基于数据库 |
|---|---|---|
| 灵活性 | 低(需重启服务) | 高(运行时动态修改) |
| 性能 | 高(内存加载) | 中(需缓存优化) |
| 维护成本 | 低(文件管理) | 高(需开发管理接口) |
| 适用场景 | 小型系统、消息变更少 | 大型系统、多语言频繁变更 |
6.2 核心要点回顾
- SpringBoot i18n的核心是
MessageSource、LocaleResolver、LocaleChangeInterceptor三大组件。 - 配置文件方式需遵循命名规则,注意编码问题;数据库方式需自定义
MessageSource并结合缓存优化。 - 校验注解国际化需将message值设为
{键名},并配置校验框架使用自定义MessageSource。 - 生产环境中,数据库方式需做好缓存优化,配置文件方式可结合配置中心实现动态刷新。
通过本文的两种实现方案,你可以根据项目规模和需求选择合适的国际化方式,同时规避常见问题,实现高效、稳定的多语言适配。