Spring Boot 国际化(i18n)终极指南:集成外部翻译平台与动态更新
在上一篇 文章中,我们探讨了构建一套解耦、强大的 i18n 体系的设计思想和顶层实现。然而,文中的一些核心组件如 MsctProperties、IMsctResourceService 以及动态更新机制都处于“待实现”状态。
本文是该系列的“实战篇”,我们将亲手完成所有缺失的拼图,提供一份可以直接在项目中使用的、生产级别的代码实现。读完本文,你将获得:
- 一个完整的、基于外部翻译平台的 i18n 自动配置方案。
- 一套内存缓存与定时任务相结合的翻译动态更新机制。
- 清晰、可扩展的代码结构,助你轻松应对未来的 i18n 需求。
一、回顾:我们的目标架构
MsctProperties: 类型安全地读取application.yml中的所有 i18n 相关配置。IMsctResourceService: 模拟与“多语言翻译平台”交互的SDK核心服务,负责拉取翻译数据。TranslationCacheManager: 单例 Bean,作为内存缓存,存储所有翻译词条,并内置一个定时任务,周期性地从IMsctResourceService拉取更新。MsctMessageSource: SpringMessageSource接口的实现,它的数据源不再是本地文件,而是TranslationCacheManager。I18nService: 一个便捷的门面(Facade)类,让业务代码能更简单地调用翻译功能。
二、核心实现详解
第1步:万物之源 - 配置类 MsctProperties
我们需要一个类来映射 application.yml 中的配置项,使其具有类型安全和自动提示的优点。
MsctProperties.java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.time.Duration;
@Component
@ConfigurationProperties(prefix = "msct.i18n")
public class MsctProperties {
/** 是否启用国际化功能 */
private boolean enabled = true;
/** 默认语言, e.g., zh-CN, en-US */
private String defaultLocale = "zh-CN";
/** SDK相关配置 */
private Sdk sdk = new Sdk();
// Getters and Setters ...
public static class Sdk {
/** 翻译平台的API地址 */
private String apiUrl;
/** 应用的租户ID或项目ID */
private String tenantId;
/** 访问API的密钥 */
private String apiKey;
/** 缓存刷新配置 */
private CacheRefresh cacheRefresh = new CacheRefresh();
// Getters and Setters ...
public static class CacheRefresh {
/** 是否开启定时刷新 */
private boolean enabled = true;
/** 首次启动后延迟多久开始刷新 */
private Duration initialDelay = Duration.ofMinutes(1);
/** 刷新任务的执行间隔 */
private Duration fixedDelay = Duration.ofHours(1);
// Getters and Setters ...
}
}
}
application.yml 配置示例:
msct:
i18n:
enabled: true
default-locale: zh-CN
sdk:
api-url: https://api.your-translation-platform.com
tenant-id: my-awesome-project
api-key: secret-key-xxxx
cache-refresh:
enabled: true
initial-delay: 1m # 1分钟
fixed-delay: 1h # 1小时
第2步:模拟SDK与缓存 - IMsctResourceService 和 TranslationCacheManager
这是整个方案的基石。我们先定义接口,然后创建一个模拟实现和一个缓存管理器。
IMsctResourceService.java (接口定义)
import java.util.Locale;
import java.util.Map;
/**
* 模拟与多语言翻译平台交互的服务接口
*/
public interface IMsctResourceService {
/**
* 根据租户ID拉取所有翻译资源
* @param tenantId 租户或项目ID
* @return 嵌套Map,结构:{ "消息Code": { Locale: "翻译文本" } }
* e.g., { "user.welcome": { Locale.US: "Welcome", Locale.CHINA: "欢迎" } }
*/
Map<String, Map<Locale, String>> fetchAllTranslations(String tenantId);
}
MockMsctResourceService.java (模拟实现)
在实际项目中,这个类会由SDK提供,或者由你根据平台API来实现,这里我们用一个Mock来让程序跑起来。
import org.springframework.stereotype.Service;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;
@Service
public class MockMsctResourceService implements IMsctResourceService {
@Override
public Map<String, Map<Locale, String>> fetchAllTranslations(String tenantId) {
System.out.println("【I18N SDK】: 正在从模拟平台拉取租户 '" + tenantId + "' 的翻译数据...");
Map<String, Map<Locale, String>> translations = new HashMap<>();
// 词条1: 简单欢迎语
Map<Locale, String> welcomeMessages = new HashMap<>();
welcomeMessages.put(Locale.SIMPLIFIED_CHINESE, "欢迎您, {0}!");
welcomeMessages.put(Locale.US, "Welcome, {0}!");
translations.put("user.welcome", welcomeMessages);
// 词条2: 系统错误提示
Map<Locale, String> errorMessages = new HashMap<>();
errorMessages.put(Locale.SIMPLIFIED_CHINESE, "系统发生未知错误,请联系管理员。");
errorMessages.put(Locale.US, "An unexpected system error occurred. Please contact the administrator.");
translations.put("error.system.unexpected", errorMessages);
// 模拟动态更新:每次调用时增加一个词条
if (Math.random() > 0.5) {
Map<Locale, String> dynamicMessages = new HashMap<>();
dynamicMessages.put(Locale.SIMPLIFIED_CHINESE, "这是一个动态更新的词条!");
dynamicMessages.put(Locale.US, "This is a dynamically updated entry!");
translations.put("dynamic.update.test", dynamicMessages);
System.out.println("【I18N SDK】: 发现并拉取了动态更新的词条 'dynamic.update.test'");
}
return translations;
}
}
TranslationCacheManager.java (缓存与动态更新核心)
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class TranslationCacheManager {
private final Map<String, Map<Locale, String>> cache = new ConcurrentHashMap<>();
private final IMsctResourceService resourceService;
private final MsctProperties properties;
public TranslationCacheManager(IMsctResourceService resourceService, MsctProperties properties) {
this.resourceService = resourceService;
this.properties = properties;
}
/**
* 在服务启动时立即执行一次,完成首次加载
*/
@PostConstruct
public void init() {
if (properties.isEnabled()) {
loadTranslations();
}
}
/**
* 根据配置的定时任务,周期性刷新缓存
*/
@Scheduled(
initialDelayString = "${msct.i18n.sdk.cache-refresh.initial-delay:1m}",
fixedDelayString = "${msct.i18n.sdk.cache-refresh.fixed-delay:1h}"
)
public void refreshCache() {
if (properties.isEnabled() && properties.getSdk().getCacheRefresh().isEnabled()) {
System.out.println("【I18N Cache】: 定时刷新任务开始...");
loadTranslations();
System.out.println("【I18N Cache】: 定时刷新任务结束。");
}
}
private void loadTranslations() {
Map<String, Map<Locale, String>> latestTranslations =
resourceService.fetchAllTranslations(properties.getSdk().getTenantId());
if (latestTranslations != null && !latestTranslations.isEmpty()) {
// 原子性地替换整个缓存,而不是逐条更新,避免并发问题
cache.clear();
cache.putAll(latestTranslations);
System.out.println("【I18N Cache】: 翻译缓存已更新, 共加载 " + cache.size() + " 个词条。");
}
}
/**
* 对外提供获取翻译文本的接口
* @return Optional<String>
*/
public Optional<String> getTranslation(String code, Locale locale) {
return Optional.ofNullable(cache.get(code))
.map(translations -> translations.get(locale));
}
}
别忘了在你的主启动类上添加 @EnableScheduling 来开启定时任务功能。
第3步:连接Spring的桥梁 - MsctMessageSource
这个类是 Spring MessageSource 接口的具体实现,它将调用转发到我们的 TranslationCacheManager。
MsctMessageSource.java
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.stereotype.Component;
import java.text.MessageFormat;
import java.util.Locale;
@Component("messageSource") // 注册为bean并命名为"messageSource"
public class MsctMessageSource extends AbstractMessageSource {
private final TranslationCacheManager cacheManager;
public MsctMessageSource(TranslationCacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
return cacheManager.getTranslation(code, locale)
.map(MessageFormat::new)
.orElse(null); // 返回null,Spring会继续查找父MessageSource(如果有的话)
}
}
第4步:便捷的门面 - I18nService 和 LocaleUtil
LocaleUtil.java
import org.springframework.util.StringUtils;
import java.util.Locale;
public final class LocaleUtil {
public static Locale parseLocale(String localeStr) {
return StringUtils.parseLocaleString(localeStr);
}
}
I18nService.java
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
@Service
public class I18nService {
private final MessageSource messageSource;
public I18nService(MessageSource messageSource) {
this.messageSource = messageSource;
}
/**
* 获取当前Locale下的翻译
* @param code 消息Code
* @return 翻译文本
*/
public String getMessage(String code) {
return getMessage(code, null);
}
/**
* 获取当前Locale下带参数的翻译
* @param code 消息Code
* @param args 占位符参数
* @return 翻译文本
*/
public String getMessage(String code, Object[] args) {
// 第三个参数是当找不到code时的默认消息,这里返回code本身
return messageSource.getMessage(code, args, code, LocaleContextHolder.getLocale());
}
}
第5步:整合 - 最终的 I18nAutoConfiguration
现在,我们可以用我们创建好的所有组件,来完成自动配置类了。
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import java.util.Collections;
@Configuration
// 只有在 msct.i18n.enabled = true (或未配置) 时,整个配置才生效
@ConditionalOnProperty(name = "msct.i18n.enabled", havingValue = "true", matchIfMissing = true)
public class I18nAutoConfiguration {
// TranslationCacheManager, MsctMessageSource, I18nService等
// 已经通过@Component或@Service注解自动注册了,这里无需再次@Bean声明。
@Bean
public LocaleResolver localeResolver(MsctProperties properties) {
// 使用Spring内置的AcceptHeaderLocaleResolver,它能自动解析Accept-Language
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
// 设置支持的语言列表,可选,但建议配置
resolver.setSupportedLocales(Collections.singletonList(LocaleUtil.parseLocale(properties.getDefaultLocale())));
// 设置默认语言
Locale defaultLocale = LocaleUtil.parseLocale(properties.getDefaultLocale());
resolver.setDefaultLocale(defaultLocale);
LocaleContextHolder.setDefaultLocale(defaultLocale);
return resolver;
}
}
三、测试一下
创建一个简单的Controller来验证我们的成果。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class I18nTestController {
@Autowired
private I18nService i18nService;
@GetMapping("/welcome")
public String welcome(@RequestParam String name) {
// 使用带参数的翻译
return i18nService.getMessage("user.welcome", new Object[]{name});
}
@GetMapping("/error-info")
public String errorInfo() {
return i18nService.getMessage("error.system.unexpected");
}
@GetMapping("/dynamic")
public String dynamic() {
// 测试动态更新,这个词条可能在服务启动时不存在
return i18nService.getMessage("dynamic.update.test");
}
}
使用 cURL 测试:
-
请求中文:
curl -X GET "http://localhost:8080/welcome?name=张三" -H "Accept-Language: zh-CN" # 预期输出: 欢迎您, 张三! -
请求英文:
curl -X GET "http://localhost:8080/welcome?name=John" -H "Accept-Language: en-US" # 预期输出: Welcome, John! -
测试动态更新:
启动服务后,立即访问/dynamic可能会返回dynamic.update.test。等待你的刷新周期(例如1分钟后)再次访问,如果你在MockMsctResourceService中模拟了更新逻辑,你将看到翻译好的文本。
四、总结
我们从一个设计思想出发,一步步实现了所有必要的组件,最终构建了一套完整、健壮、可动态更新的国际化方案。这套方案将翻译管理的复杂性从业务代码中彻底剥离,极大地提升了开发效率和系统的可维护性,是现代分布式应用中处理 i18n 问题的理想选择。
欢迎关注公众号"飞鸿影记(fhyblog)",探寻物件背后的逻辑,记录生活真实的影子。

作者:飞鸿影
出处:http://52fhy.cnblogs.com/
版权申明:没有标明转载或特殊申明均为作者原创。本文采用以下协议进行授权,自由转载 - 非商用 - 非衍生 - 保持署名 | Creative Commons BY-NC-ND 3.0,转载请注明作者及出处。


浙公网安备 33010602011771号