SpringBoot實(shí)現(xiàn)i18n國際化的兩種企業(yè)級方案
前言
在全球化業(yè)務(wù)場景下,系統(tǒng)適配多語言已成為標(biāo)配需求。SpringBoot作為主流的Java開發(fā)框架,提供了完善的國際化(i18n,internationalization的縮寫,因i和n之間有18個字母得名)解決方案。本文將從實(shí)戰(zhàn)角度出發(fā),完整講解兩種企業(yè)級i18n實(shí)現(xiàn)方案:基于配置文件的靜態(tài)實(shí)現(xiàn)(適配簡體中文、繁體中文、英文)和基于數(shù)據(jù)庫的動態(tài)實(shí)現(xiàn)(支持運(yùn)行時修改語言配置),同時覆蓋校驗(yàn)注解國際化、性能優(yōu)化、常見問題排查等核心要點(diǎn),所有代碼均可直接落地到生產(chǎn)項(xiàng)目。
一、國際化基礎(chǔ)認(rèn)知
1.1 核心概念
i18n的核心目標(biāo)是讓系統(tǒng)在不修改代碼的前提下,通過配置適配不同語言和地區(qū)的使用習(xí)慣。SpringBoot中實(shí)現(xiàn)i18n的核心依賴是:
MessageSource:消息源接口,負(fù)責(zé)加載和解析多語言消息,默認(rèn)實(shí)現(xiàn)為ResourceBundleMessageSource(基于配置文件)。Accept-Language:語言地區(qū)標(biāo)識,格式為語言代碼_國家/地區(qū)代碼,如:- 簡體中文:
zh_CN - 繁體中文:
zh_TW - 英文(美國):
en_US
- 簡體中文:
LocaleResolver:語言解析器,負(fù)責(zé)從請求中獲取/設(shè)置當(dāng)前Locale。LocaleChangeInterceptor:語言切換攔截器,用于攔截請求參數(shù)實(shí)現(xiàn)語言動態(tài)切換。
1.2 核心原理
SpringBoot啟動時,MessageSource會加載指定路徑下的多語言配置文件;當(dāng)業(yè)務(wù)代碼獲取國際化消息時,框架會根據(jù)當(dāng)前Accept-Language從對應(yīng)配置文件/數(shù)據(jù)源中匹配消息鍵(Key),返回對應(yīng)的消息值(Value)。
二、方式一:基于配置文件的i18n實(shí)現(xiàn)
2.1 環(huán)境準(zhǔn)備
2.1.1 依賴配置
新建SpringBoot項(xiàng)目(推薦2.7.x或3.2.x),核心依賴僅需spring-boot-starter-web,無需額外依賴:
<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 目錄結(jié)構(gòu)
在resources目錄下創(chuàng)建i18n文件夾,用于存放多語言配置文件,最終目錄結(jié)構(gòu):
resources/
├── application.yml # 核心配置
└── i18n/ # 國際化配置文件目錄
├── messages.properties # 默認(rèn)配置(無語言標(biāo)識)
├── messages_zh_CN.properties # 簡體中文
├── messages_zh_TW.properties # 繁體中文
└── messages_en_US.properties # 英文
2.2 多語言配置文件編寫
2.2.1 命名規(guī)則
配置文件命名必須遵循basename_語言代碼_國家代碼.properties規(guī)則:
basename:自定義前綴(如messages),需在application.yml中配置。- 無語言標(biāo)識的
messages.properties為默認(rèn)配置,當(dāng)匹配不到指定Locale的配置時,會使用該文件內(nèi)容。
2.2.2 配置文件內(nèi)容
- 默認(rèn)配置(messages.properties):兜底使用,建議與默認(rèn)語言(簡體中文)保持一致
# 通用提示 common.submit=提交 common.cancel=取消 # 用戶相關(guān) user.name=用戶名 user.age=年齡 # 校驗(yàn)提示 validate.required.id=主鍵不能為空 validate.required.name=姓名不能為空
- 簡體中文(messages_zh_CN.properties):
# 通用提示 common.submit=提交 common.cancel=取消 # 用戶相關(guān) user.name=用戶名 user.age=年齡 # 校驗(yàn)提示 validate.required.id=主鍵不能為空 validate.required.name=姓名不能為空
- 繁體中文(messages_zh_TW.properties):
# 通用提示 common.submit=提交 common.cancel=取消 # 用戶相關(guān) user.name=使用者名稱 user.age=年齡 # 校驗(yàn)提示 validate.required.id=主鍵不能為空 validate.required.name=姓名不能為空
- 英文(messages_en_US.properties):
# 通用提示 common.submit=Submit common.cancel=Cancel # 用戶相關(guān) user.name=Username user.age=Age # 校驗(yàn)提示 validate.required.id=Primary key cannot be empty validate.required.name=Name cannot be empty
注意:properties文件默認(rèn)編碼為ISO-8859-1,直接寫中文會亂碼!需將IDE的properties文件編碼設(shè)置為UTF-8(IDEA:Settings → File Encodings → Properties Files → 勾選Transparent native-to-ascii conversion)。
2.3 SpringBoot核心配置
在application.yml中配置國際化相關(guān)參數(shù),指定配置文件路徑、默認(rèn)語言、編碼等:
spring:
# 國際化配置
messages:
basename: i18n/messages # 配置文件路徑(無需寫.properties后綴)
encoding: UTF-8 # 解決中文亂碼
fallback-to-system-locale: false # 禁用系統(tǒng)語言回退
default-locale: zh_CN # 默認(rèn)語言:簡體中文
cache-duration: 3600s # 配置文件緩存時間(生產(chǎn)建議設(shè)置)
# Web配置(可選,用于請求參數(shù)解析)
web:
locale: zh_CN
2.4 自定義語言解析器與攔截器
默認(rèn)情況下,SpringBoot僅支持從請求頭Accept-Language獲取Locale,為了方便通過請求參數(shù)(如?Accept-Language=en-US)切換語言,需自定義LocaleResolver并注冊攔截器。
2.4.1 自定義LocaleResolver
創(chuàng)建config/I18nConfig.java,實(shí)現(xiàn)LocaleResolver接口:
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)
* 替代默認(rèn)的AcceptHeaderLocaleResolver
*/
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
// 設(shè)置默認(rèn)語言:簡體中文(與application.yml中保持一致)
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); // 默認(rèn):zh_CN
return resolver;
}
/**
* 自定義攔截器:從 Accept-Language Header 解析并設(shè)置 Locale
*/
@Bean
public HandlerInterceptor localeHeaderInterceptor(LocaleResolver localeResolver) {
return new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 1. 從請求頭中獲取語言標(biāo)識(自定義Header名:Accept-Language,可根據(jù)需求修改)
String acceptLanguage = request.getHeader("Accept-Language");
// 2. 設(shè)置默認(rèn)語言為空或者拋異常使用
Locale locale = Locale.SIMPLIFIED_CHINESE; // 默認(rèn)語言
// 3. 若Header中有值,則解析并設(shè)置Accept-Language;無值則使用默認(rèn)Accept-Language
if (acceptLanguage != null && !acceptLanguage.isEmpty()) {
try {
// 取第一個語言項(xiàng)(如 "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) {
// 解析失敗則使用默認(rèn)語言,不拋異常
}
}
// 使用容器中真實(shí)的 LocaleResolver 實(shí)例設(shè)置 Locale(存入 Session)
localeResolver.setLocale(request, response, locale);
return true;
}
};
}
/**
* 注冊攔截器到 Spring MVC 攔截器鏈
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeHeaderInterceptor(localeResolver()))
.addPathPatterns("/**")
.order(0); // 優(yōu)先級最高
}
}
關(guān)鍵說明:
SessionLocaleResolver:將Locale存儲在Session中,一次切換后,后續(xù)請求無需重復(fù)傳參。localeHeaderInterceptor:攔截請求頭Header中Accept-Language參數(shù),自動更新當(dāng)前Locale(如Accept-Language=zh-TW會切換為繁體中文)。
2.5 國際化消息使用示例
2.5.1 工具類封裝(推薦)
創(chuàng)建utils/I18nUtils.java,封裝獲取國際化消息的方法,簡化業(yè)務(wù)使用:
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;
/**
* 獲取國際化消息(使用當(dāng)前Locale)
* @param key 消息鍵
* @return 消息值
*/
public String getMessage(String key) {
return getMessage(key, null, LocaleContextHolder.getLocale());
}
/**
* 獲取國際化消息(帶參數(shù))
* @param key 消息鍵
* @param args 參數(shù)數(shù)組(如消息為"你好{0}",args=new Object[]{"張三"})
* @return 消息值
*/
public String getMessage(String key, Object[] args) {
return getMessage(key, args, LocaleContextHolder.getLocale());
}
/**
* 手動指定Locale獲取消息
* @param key 消息鍵
* @param args 參數(shù)數(shù)組
* @param locale 語言標(biāo)識
* @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():獲取當(dāng)前線程的Locale(由LocaleResolver解析)。messageSource.getMessage(key, args, locale):核心方法,參數(shù)說明:key:消息鍵(如user.name)。args:消息參數(shù)(用于替換消息中的占位符,如user.hello=你好{0})。locale:指定語言標(biāo)識。
2.5.2 控制器使用示例
創(chuàng)建controller/I18nController.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;
/**
* 測試基礎(chǔ)國際化消息
* 請求頭添加: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<>();
// 獲取當(dāng)前語言的消息
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;
}
/**
* 測試帶參數(shù)的國際化消息
*/
@GetMapping("/with-params")
public Map<String, String> getMessageWithParams() {
Map<String, String> result = new HashMap<>();
// 模擬帶參數(shù)的消息(需先在配置文件中添加: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 測試驗(yàn)證
啟動項(xiàng)目后,通過Postman/Browser訪問以下地址驗(yàn)證效果:
簡體中文:http://localhost:8080/api/i18n/basic?lang=zh_CN
返回:
{
"user.name":"用戶名",
"common.submit":"提交",
"validate.required.id":"主鍵不能為空"
}
繁體中文:http://localhost:8080/api/i18n/basic?lang=zh_TW
返回:
{
"user.name":"使用者名稱",
"common.submit":"提交",
"validate.required.id":"主鍵不能為空"
}
英文:http://localhost:8080/api/i18n/basic?lang=en_US
返回:
{
"user.name":"Username",
"common.submit":"Submit",
"validate.required.id":"Primary key cannot be empty"
}
2.7 默認(rèn)語言切換說明
默認(rèn)語言的生效優(yōu)先級:
SessionLocaleResolver中設(shè)置的setDefaultLocale()(代碼級)。application.yml中spring.messages.default-locale(配置級)。- 系統(tǒng)默認(rèn)Locale(兜底)。
若需修改默認(rèn)語言為英文,只需調(diào)整兩處:
// 1. I18nConfig中
localeResolver.setDefaultLocale(Locale.US);
// 2. application.yml中
spring:
messages:
default-locale: en_US
三、方式二:基于數(shù)據(jù)庫的動態(tài)i18n實(shí)現(xiàn)
基于配置文件的方式存在明顯缺陷:修改消息需重啟服務(wù)?;跀?shù)據(jù)庫的實(shí)現(xiàn)可實(shí)現(xiàn)運(yùn)行時動態(tài)配置多語言消息,適合頻繁變更或大規(guī)模多語言場景。
3.1 設(shè)計思路
- 設(shè)計數(shù)據(jù)庫表存儲多語言消息(鍵、語言、值)。
- 自定義
MessageSource實(shí)現(xiàn),重寫消息解析邏輯,從數(shù)據(jù)庫加載消息。 - 引入緩存(Caffeine)提升性能,避免頻繁查詢數(shù)據(jù)庫。
- 提供接口實(shí)現(xiàn)消息的新增/修改/刪除,支持動態(tài)刷新緩存。
3.2 數(shù)據(jù)庫表設(shè)計
3.2.1 建表語句(MySQL)
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 '語言標(biāo)識(zh_CN/zh_TW/en_US)', `message_value` varchar(500) NOT NULL COMMENT '消息值', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時間', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', `deleted` tinyint DEFAULT 0 COMMENT '刪除標(biāo)記(0-未刪,1-已刪)', PRIMARY KEY (`id`), UNIQUE KEY `uk_key_language` (`message_key`,`language`) COMMENT '消息鍵+語言唯一約束' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='國際化消息表';
3.2.2 測試數(shù)據(jù)插入
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 環(huán)境準(zhǔn)備
3.3.1 添加依賴
在原有依賴基礎(chǔ)上,添加數(shù)據(jù)庫相關(guān)依賴(以MyBatis-Plus為例):
<!-- 數(shù)據(jù)庫驅(qū)動 -->
<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 數(shù)據(jù)庫配置
在application.yml中添加數(shù)據(jù)庫配置:
spring:
# 數(shù)據(jù)庫配置
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 核心組件開發(fā)
3.4.1 實(shí)體類
創(chuàng)建entity/SysI18nMessage.java:
package com.example.i18n.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 國際化消息實(shí)體
*/
@Data
@TableName("sys_i18n_message")
public class SysI18nMessage {
/**
* 主鍵ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 消息鍵
*/
@TableField("message_key")
private String messageKey;
/**
* 語言標(biāo)識
*/
@TableField("language")
private String language;
/**
* 消息值
*/
@TableField("message_value")
private String messageValue;
/**
* 創(chuàng)建時間
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新時間
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 刪除標(biāo)記
*/
@TableField("deleted")
@TableLogic
private Integer deleted;
}
3.4.2 Mapper接口
創(chuàng)建mapper/SysI18nMessageMapper.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> {
/**
* 根據(jù)消息鍵和語言查詢消息
*/
@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);
/**
* 查詢所有消息(用于預(yù)加載緩存)
*/
@Select("SELECT message_key, language, message_value FROM sys_i18n_message WHERE deleted = 0")
List<SysI18nMessage> listAllMessages();
}
3.4.3 Service層
創(chuàng)建service/SysI18nMessageService.java(接口):
package com.example.i18n.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.i18n.entity.SysI18nMessage;
import java.util.Map;
/**
* 國際化消息服務(wù)
*/
public interface SysI18nMessageService extends IService<SysI18nMessage> {
/**
* 根據(jù)鍵和語言獲取消息
*/
String getMessage(String key, String language);
/**
* 加載所有消息到緩存
*/
Map<String, String> loadAllMessagesToCache();
/**
* 刷新緩存
*/
void refreshCache();
}
創(chuàng)建service/impl/SysI18nMessageServiceImpl.java(實(shí)現(xiàn)類):
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;
/**
* 國際化消息服務(wù)實(shí)現(xiàn)
*/
@Service
public class SysI18nMessageServiceImpl extends ServiceImpl<SysI18nMessageMapper, SysI18nMessage> implements SysI18nMessageService {
/**
* 緩存Key規(guī)則:messageKey + "_" + language
*/
private final Cache<String, String> i18nCache = Caffeine.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS) // 1小時過期
.maximumSize(10000) // 最大緩存10000條
.build();
/**
* 項(xiàng)目啟動時預(yù)加載所有消息到緩存
*/
@PostConstruct
public void initCache() {
loadAllMessagesToCache();
}
@Override
public String getMessage(String key, String language) {
// 構(gòu)造緩存Key
String cacheKey = key + "_" + language;
// 先查緩存,緩存未命中則查數(shù)據(jù)庫
return i18nCache.get(cacheKey, k -> {
String message = baseMapper.getMessageByKeyAndLanguage(key, language);
// 數(shù)據(jù)庫未找到則返回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:項(xiàng)目啟動時執(zhí)行initCache(),預(yù)加載所有消息到緩存,提升首次訪問性能。- Caffeine緩存:設(shè)置1小時過期+最大容量,避免緩存膨脹;緩存Key為
消息鍵_語言(如user.name_zh_CN)。 - 緩存穿透處理:數(shù)據(jù)庫未找到消息時,返回消息鍵本身,避免緩存穿透。
3.4.4 自定義MessageSource
創(chuàng)建config/DbMessageSource.java,繼承AbstractMessageSource(Spring提供的MessageSource抽象實(shí)現(xiàn)):
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;
/**
* 基于數(shù)據(jù)庫的MessageSource實(shí)現(xiàn)
*/
@Component
public class DbMessageSource extends AbstractMessageSource {
@Resource
private SysI18nMessageService sysI18nMessageService;
/**
* 核心方法:解析消息
*/
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
// 獲取語言標(biāo)識(如zh_CN)
String language = locale.toString();
// 從數(shù)據(jù)庫+緩存中獲取消息值
String message = sysI18nMessageService.getMessage(code, language);
// 若未找到,嘗試使用默認(rèn)語言(zh_CN)
if (message.equals(code) && !language.equals("zh_CN")) {
message = sysI18nMessageService.getMessage(code, "zh_CN");
}
// 封裝為MessageFormat(支持參數(shù)替換)
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 替換默認(rèn)MessageSource
在I18nConfig.java中注冊自定義的DbMessageSource,替換SpringBoot默認(rèn)的ResourceBundleMessageSource:
/**
* 注冊數(shù)據(jù)庫版MessageSource,優(yōu)先級高于默認(rèn)實(shí)現(xiàn)
*/
@Bean
@Primary // 標(biāo)記為首選Bean
public MessageSource messageSource(SysI18nMessageService sysI18nMessageService) {
DbMessageSource messageSource = new DbMessageSource();
// 設(shè)置默認(rèn)語言(與之前保持一致)
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
// 設(shè)置編碼
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
3.5 業(yè)務(wù)集成與測試
3.5.1 工具類適配
修改I18nUtils.java,注入自定義的DbMessageSource:
// 替換原有的MessageSource為自定義的DbMessageSource @Resource private DbMessageSource messageSource; // 其余方法無需修改,邏輯完全兼容
3.5.2 消息管理接口
創(chuàng)建controller/I18nManageController.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;
/**
* 國際化消息管理接口(動態(tài)配置)
*/
@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 測試驗(yàn)證
- 基礎(chǔ)消息查詢:訪問
http://localhost:8080/api/i18n/basic?lang=en_US,返回數(shù)據(jù)庫中的英文消息。 - 動態(tài)修改消息:
- 調(diào)用POST接口
http://localhost:8080/api/i18n/manage/save,傳入JSON:
- 調(diào)用POST接口
{
"messageKey": "user.name",
"language": "en_US",
"messageValue": "User Name"
}
- 調(diào)用刷新緩存接口:
http://localhost:8080/api/i18n/manage/refresh-cache。 - 再次訪問基礎(chǔ)查詢接口,
user.name會返回User Name(無需重啟服務(wù))。
四、進(jìn)階優(yōu)化措施
4.1 緩存優(yōu)化(數(shù)據(jù)庫方式)
- 多級緩存:結(jié)合Caffeine(本地緩存)+ Redis(分布式緩存),適配集群場景。
- 緩存預(yù)熱:項(xiàng)目啟動時預(yù)加載所有消息到緩存,避免首次訪問數(shù)據(jù)庫。
- 緩存主動失效:消息修改后立即刷新緩存,而非等待過期。
- 批量加載:分頁加載大量消息,避免一次性加載過多數(shù)據(jù)導(dǎo)致內(nèi)存溢出。
4.2 語言解析器增強(qiáng)
擴(kuò)展LocaleResolver,支持多維度語言解析(優(yōu)先級從高到低):
- 請求參數(shù)(
lang)→ 2. Cookie → 3. 請求頭(Accept-Language)→ 4. Session → 5. 默認(rèn)語言。
示例代碼:
@Component
public class CustomLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 1. 優(yōu)先從請求參數(shù)獲取
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. 默認(rèn)語言
return Locale.SIMPLIFIED_CHINESE;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
// 設(shè)置Cookie,有效期7天
Cookie cookie = new Cookie("lang", locale.toString());
cookie.setMaxAge(60 * 60 * 24 * 7);
cookie.setPath("/");
response.addCookie(cookie);
}
}
4.3 動態(tài)刷新配置(配置文件方式)
使用Spring Cloud Config或Nacos實(shí)現(xiàn)配置文件的動態(tài)刷新,無需重啟服務(wù):
- 將多語言配置文件放到配置中心。
- 引入
spring-cloud-starter-config依賴。 - 配置
@RefreshScope,實(shí)現(xiàn)配置熱更新。
4.4 異常處理國際化
全局異常處理器中使用國際化工具類,返回多語言異常信息:
@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 -> {
// 獲取國際化后的校驗(yàn)提示
String message = i18nUtils.getMessage(fieldError.getDefaultMessage());
errors.put(fieldError.getField(), message);
});
return ResponseEntity.badRequest().body(errors);
}
}
五、常見問題與解決方案
5.1 校驗(yàn)注解(@NotNull等)國際化適配
問題描述
直接使用@NotNull(message = "主鍵不能為空")是硬編碼,無法實(shí)現(xiàn)國際化。
解決方案
- 核心原理:JSR-303校驗(yàn)框架默認(rèn)讀取
ValidationMessages.properties配置文件,可將校驗(yàn)消息鍵指向i18n配置。 - 實(shí)現(xiàn)步驟:
- 步驟1:在i18n配置文件/數(shù)據(jù)庫中添加校驗(yàn)消息鍵(如
validate.required.id=主鍵不能為空)。 - 步驟2:校驗(yàn)注解中使用
{鍵名}引用國際化消息:
- 步驟1:在i18n配置文件/數(shù)據(jù)庫中添加校驗(yàn)消息鍵(如
public class UserDTO {
@NotNull(message = "{validate.required.id}")
private Long id;
@NotBlank(message = "{validate.required.name}")
private String name;
// 省略getter/setter
}
- 步驟3:配置校驗(yàn)框架使用自定義的MessageSource:
@Bean
public Validator validator(MessageSource messageSource) {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
// 設(shè)置校驗(yàn)消息源為自定義的DbMessageSource/ResourceBundleMessageSource
validator.setValidationMessageSource(messageSource);
return validator;
}
@Override
public Validator getValidator() {
return validator(messageSource);
}
5.2 配置文件中文亂碼
問題描述
properties文件中寫中文,讀取后顯示亂碼。
解決方案
- IDE配置:IDEA中設(shè)置
File → Settings → Editor → File Encodings:Properties Files (*.properties):編碼設(shè)為UTF-8。- 勾選
Transparent native-to-ascii conversion。
- 配置文件指定編碼:
spring.messages.encoding=UTF-8。 - 手動轉(zhuǎn)碼:使用
native2ascii工具將中文轉(zhuǎn)為ASCII編碼(不推薦)。
5.3 默認(rèn)語言不生效
問題描述
配置了默認(rèn)語言,但未傳參時仍使用系統(tǒng)語言。
解決方案
- 檢查
LocaleResolver是否設(shè)置了setDefaultLocale()。 - 確認(rèn)
application.yml中spring.messages.fallback-to-system-locale=false(禁用系統(tǒng)語言回退)。 - 檢查自定義
LocaleResolver的resolveLocale方法,默認(rèn)分支是否返回指定的默認(rèn)Locale。
5.4 數(shù)據(jù)庫方式性能問題
問題描述
高并發(fā)場景下,數(shù)據(jù)庫查詢頻繁,響應(yīng)慢。
解決方案
- 增加Caffeine本地緩存,設(shè)置合理的過期時間和最大容量。
- 集群場景下使用Redis分布式緩存,避免每個節(jié)點(diǎn)都查詢數(shù)據(jù)庫。
- 對熱點(diǎn)消息(如通用提示)進(jìn)行永久緩存,非熱點(diǎn)消息設(shè)置較短過期時間。
- 數(shù)據(jù)庫表添加索引(已在建表語句中添加
uk_key_language唯一索引)。
5.5 動態(tài)修改語言后不生效(適配 Header 方式)
問題描述
傳參lang=en_US后,返回的仍為默認(rèn)語言。
解決方案
- 檢查自定義 Header 攔截器是否注冊到 SpringMVC 攔截器鏈,且攔截路徑包含目標(biāo)接口(需確保addPathPatterns(“/**”))。
- 確認(rèn) Header 攔截器中request.getHeader(“lang”)的 Header 名稱與實(shí)際請求一致(如前端傳的是Lang/LANG會導(dǎo)致讀取不到,HTTP Header 不區(qū)分大小寫,但建議統(tǒng)一小寫)。
- 檢查攔截器中 Locale 解析邏輯:
- 確認(rèn)lang的格式是否符合解析規(guī)則(如en_US是下劃線分隔,而非en-US);
- 檢查異常處理邏輯,若解析失敗是否回退到默認(rèn) Locale(避免解析異常導(dǎo)致 Locale 未設(shè)置)。
- 檢查LocaleResolver的setLocale方法是否正確實(shí)現(xiàn)(如SessionLocaleResolver需確保 Session
- 正常生效,無 Session 失效 / 隔離問題)。
- 排查是否存在攔截器執(zhí)行順序問題:確保語言攔截器優(yōu)先于其他業(yè)務(wù)攔截器執(zhí)行(可通過order()指定優(yōu)先級,如registry.addInterceptor(xxx).order(0))。
5.6 數(shù)據(jù)庫消息未找到時返回Key本身
問題描述
數(shù)據(jù)庫中未配置某個消息鍵,返回的是鍵名而非兜底消息。
解決方案
在DbMessageSource的resolveCode方法中,增加兜底邏輯:
// 若未找到當(dāng)前語言的消息,嘗試默認(rèn)語言,仍未找到則返回兜底提示
if (message.equals(code)) {
message = sysI18nMessageService.getMessage(code, "zh_CN");
if (message.equals(code)) {
message = "未找到對應(yīng)的提示信息:" + code;
}
}
5.7 Header 中語言標(biāo)識格式錯誤導(dǎo)致切換失敗
問題描述
Header 傳入Accept-Language=en-US(中劃線分隔)或lang=english(非標(biāo)準(zhǔn)格式),語言切換不生效,始終返回默認(rèn)語言。
解決方案
- 攔截器中增加格式兼容邏輯,支持中劃線 / 下劃線兩種格式:
// 兼容en-US、zh-CN等中劃線格式
String langHeader = request.getHeader("Accept-Language").replace("-", "_");
- 增加語言標(biāo)識白名單校驗(yàn),僅允許合法的語言值:
// 定義合法語言列表
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 {
// 非法值使用默認(rèn)語言
localeResolver().setLocale(request, response, Locale.SIMPLIFIED_CHINESE);
}
- 前端規(guī)范:約定前端僅傳遞zh_CN/zh_TW/en_US三種格式,避免非法值。
5.8 跨域請求時 Header 中的 lang 參數(shù)丟失
問題描述
前后端分離項(xiàng)目中,前端跨域請求時攜帶Accept-Language Header,但后端無法讀取到該值,語言切換失效。
解決方案
- 配置跨域(CORS)允許自定義 Header:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // 生產(chǎn)環(huán)境替換為具體域名
.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):
// Axios示例
axios({
url: "http://localhost:8080/api/i18n/basic",
method: "GET",
headers: {
"Accept-Language": "en_US"
},
withCredentials: true // 關(guān)鍵:跨域攜帶Cookie/Session(SessionLocaleResolver依賴)
});
5.9 自定義 MessageSource 優(yōu)先級低于默認(rèn)實(shí)現(xiàn)導(dǎo)致校驗(yàn)注解國際化不生效
問題描述
校驗(yàn)注解中使用{validate.required.id}引用國際化鍵,但返回的仍是鍵名而非國際化值
解決方案
- 確保自定義的
MessageSource(如DbMessageSource)添加@Primary注解,優(yōu)先級高于默認(rèn)的ResourceBundleMessageSource:
@Bean
@Primary // 標(biāo)記為首選Bean
public MessageSource messageSource(SysI18nMessageService sysI18nMessageService) {
DbMessageSource messageSource = new DbMessageSource();
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
messageSource.setDefaultEncoding("UTF-8");
return messageSource;
}
- 檢查校驗(yàn)器配置是否正確注入自定義
MessageSource,而非默認(rèn)實(shí)現(xiàn):
// 確保注入的是自定義的MessageSource
@Resource
private MessageSource messageSource;
@Bean
public Validator validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource);
return validator;
}
- 排查是否存在多個
MessageSourceBean,導(dǎo)致 Spring 注入錯誤的實(shí)例。
六、總結(jié)
6.1 兩種方式對比
| 特性 | 基于配置文件 | 基于數(shù)據(jù)庫 |
|---|---|---|
| 靈活性 | 低(需重啟服務(wù)) | 高(運(yùn)行時動態(tài)修改) |
| 性能 | 高(內(nèi)存加載) | 中(需緩存優(yōu)化) |
| 維護(hù)成本 | 低(文件管理) | 高(需開發(fā)管理接口) |
| 適用場景 | 小型系統(tǒng)、消息變更少 | 大型系統(tǒng)、多語言頻繁變更 |
6.2 核心要點(diǎn)回顧
- SpringBoot i18n的核心是
MessageSource、LocaleResolver、LocaleChangeInterceptor三大組件。 - 配置文件方式需遵循命名規(guī)則,注意編碼問題;數(shù)據(jù)庫方式需自定義
MessageSource并結(jié)合緩存優(yōu)化。 - 校驗(yàn)注解國際化需將message值設(shè)為
{鍵名},并配置校驗(yàn)框架使用自定義MessageSource。 - 生產(chǎn)環(huán)境中,數(shù)據(jù)庫方式需做好緩存優(yōu)化,配置文件方式可結(jié)合配置中心實(shí)現(xiàn)動態(tài)刷新。
通過本文的兩種實(shí)現(xiàn)方案,你可以根據(jù)項(xiàng)目規(guī)模和需求選擇合適的國際化方式,同時規(guī)避常見問題,實(shí)現(xiàn)高效、穩(wěn)定的多語言適配。
以上就是SpringBoot實(shí)現(xiàn)i18n國際化的兩種企業(yè)級方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot實(shí)現(xiàn)i18n國際化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java獲取網(wǎng)絡(luò)圖片上傳到OSS的方法
這篇文章主要為大家詳細(xì)介紹了java獲取網(wǎng)絡(luò)圖片上傳到OSS,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-10-10
SpringMVC 響應(yīng)數(shù)據(jù)和結(jié)果視圖從環(huán)境搭建到實(shí)戰(zhàn)全解析
SpringMVC中,Controller方法的返回值決定了響應(yīng)方式,本文詳細(xì)講解了SpringMVC的開發(fā)環(huán)境搭建、Controller方法返回值分類、轉(zhuǎn)發(fā)與重定向機(jī)制,以及JSON異步交互的實(shí)現(xiàn),本文介紹SpringMVC 響應(yīng)數(shù)據(jù)和結(jié)果視圖從環(huán)境搭建到實(shí)戰(zhàn)全解析,感興趣的朋友跟隨小編一起看看吧2025-11-11
詳解Spring 基于 Aspect 注解的增強(qiáng)實(shí)現(xiàn)
本篇文章主要介紹了詳解Spring 基于 Aspect 注解的增強(qiáng)實(shí)現(xiàn),非常具有實(shí)用價值,需要的朋友可以參考下2017-04-04
SpringBoot @ConfigurationProperties使用詳解
這篇文章主要介紹了SpringBoot @ConfigurationProperties使用詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法
本文主要介紹了詳解Java構(gòu)建樹結(jié)構(gòu)的公共方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04

