展示不同的语言文本。传统的基于本地资源文件.properties的方案虽然经典但在大型、快速迭代的项目中往往面临维护困难、更新滞后等问题。本文将介绍一种更为现代化、基于外部翻译平台和Spring Boot 自动配置的 i18n 方案。该方案不仅实现了业务代码与翻译文本的完全解耦还优雅地解决了全局异常处理和异步线程中的 i18n 上下文传递问题。一、方案设计总览#我们的目标是构建一个非侵入式、高内聚的 i18n 系统。其核心架构如下集中式翻译管理所有翻译文本词条都由一个独立的“多语言翻译平台”进行维护。产品、运营或翻译人员可以在该平台上新增、修改、发布不同语言的词条实现与开发流程的分离。SDK 动态拉取在 Spring Boot 应用启动时通过平台提供的 SDK 自动拉取当前项目所需的全部词条并缓存在内存中。SDK 还会负责监听后续的词条更新实现动态刷新无需重启应用。遵循 Spring 标准接口自定义的 i18n 组件将完全遵循 Spring 的MessageSource和LocaleResolver接口。这使得整个方案能够无缝融入 Spring 生态享受 Spring 框架带来的便利。自动化的语言识别通过自定义LocaleResolver应用能自动从 HTTP 请求的 Header如Accept-Language或 Cookie 中识别用户的语言偏好。全局上下文传递利用LocaleContextHolder存储当前请求的语言环境Locale并确保即使在异步线程中该上下文也能被正确传递和使用。下面我们将分步实现这个强大的 i18n 体系。二、核心实现步骤#第1步基石 - 自动配置 i18n 核心组件一切始于自动配置。我们将创建一个AutoConfiguration类它负责在 Spring 容器中注册我们 i18n 方案所需的核心 Bean。I18nAutoConfiguration.javaimport org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.MessageSource; import org.springframework.web.servlet.LocaleResolver; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.annotation.Order; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Primary; import javax.servlet.http.HttpServletRequest; // 假设这些是我们SDK或自定义的类 // import com.yourcompany.sdk.MsctProperties; // import com.yourcompany.sdk.IMsctResourceService; // import com.yourcompany.sdk.MsctMessageSource; // import com.yourcompany.sdk.MsctLocaleResolver; Configuration public class I18nAutoConfiguration { /** * 1. 配置 MessageSource * 这是 Spring i18n 的核心接口负责根据消息Code和Locale获取具体的翻译文本。 * 我们创建的 MsctMessageSource 实例其内部逻辑是不去读取.properties文件 * 而是从启动时拉取到内存的翻译缓存中查找。 * Order(Integer.MIN_VALUE) 确保它作为最高优先级的 MessageSource。 */ Bean(messageSource) // 明确Bean名称方便其他地方注入 Primary // 当有多个MessageSource时默认使用这个 Order(Integer.MIN_VALUE) public MessageSource messageSource(MsctProperties properties, IMsctResourceService resourceService) { // 这里的 MsctMessageSource 是关键它连接了翻译平台SDK和Spring的i18n机制 return new MsctMessageSource(properties, resourceService); } /** * 2. 配置 LocaleResolver * 它的职责是解析每次HTTP请求确定当前用户的Locale语言环境。 * 我们的 MsctLocaleResolver 会检查请求的Header或Cookie。 */ Bean Primary public LocaleResolver localeResolver(MsctProperties properties) { // 设置一个全局的默认语言例如 zh-CN String defaultLocaleStr properties.getMsct().getDefaultLanguage(); Locale defaultLocale parseLocale(defaultLocaleStr); // 实现一个解析字符串到Locale的方法 LocaleContextHolder.setDefaultLocale(defaultLocale); return new MsctLocaleResolver(defaultLocale); } /** * 3. 提供一个便捷的服务接口 (I18nService) * 封装对 MessageSource 的直接调用提供更简洁的API。 */ Bean public I18nService i18nService(MessageSource messageSource) { return new I18nService(messageSource); } }第2步识别用户语言 -LocaleResolver的实现MsctLocaleResolver的实现通常会继承 Spring 的AbstractLocaleResolver。MsctLocaleResolver.javaimport org.springframework.web.servlet.i18n.AbstractLocaleResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; import java.util.Optional; public class MsctLocaleResolver extends AbstractLocaleResolver { public MsctLocaleResolver(Locale defaultLocale) { setDefaultLocale(defaultLocale); } Override public Locale resolveLocale(HttpServletRequest request) { // 1. 优先从请求 Header 的 Accept-Language 中获取 String languageHeader request.getHeader(Accept-Language); if (languageHeader ! null !languageHeader.isEmpty()) { try { // Spring 提供了便捷的工具类来解析这个Header return request.getLocale(); } catch (Exception e) { // 解析异常则继续往下走 } } // 2. 其次可以自定义规则比如从Cookie或自定义Header lang 中获取 // String langCookie ... // 3. 如果都获取不到返回默认的Locale return Optional.ofNullable(getDefaultLocale()).orElse(Locale.SIMPLIFIED_CHINESE); } Override public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { // 如果需要将会话级别的locale持久化例如到Cookie则在此实现 // 对于无状态API此方法通常无需实现 throw new UnsupportedOperationException(Cannot change locale - read-only resolver); } }第3步i18n 的应用场景与最佳实践有了坚实的基础设施现在来看看如何在实际业务中优雅地使用它。场景一全局异常信息的国际化这是最能体现 i18n 价值的地方。当系统抛出异常时我们不应直接将e.getMessage()返回给前端因为异常信息通常是开发语言如英文且包含技术细节。正确的做法是异常信息应为一个“消息Code”由全局异常处理器将其转换为对用户友好的、本地化的文本。import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; ControllerAdvice public class GlobalExceptionHandler { Autowired private I18nService i18nService; // 拦截自定义的业务异常 ExceptionHandler(BusinessException.class) ResponseBody public JsonResponseString handleBusinessException(BusinessException ex) { // ex.getMessage() 此处应该是一个消息Code, e.g., error.user.not.found // ex.getArgs() 可以是填充占位符的参数, e.g., a user ID String translatedMessage i18nService.getMessage(ex.getMessage(), ex.getArgs()); return JsonResponse.fail(ex.getCode(), translatedMessage); } // 拦截其他所有未处理的异常 ExceptionHandler(Exception.class) ResponseBody public JsonResponseString handleAllExceptions(Exception ex) { // 对于未知异常返回一个通用的、已国际化的错误提示 // error.system.unexpected - 系统发生未知错误请联系管理员。 log.error(Unhandled exception occurred, ex); // 记录详细日志 String translatedMessage i18nService.getMessage(error.system.unexpected); return JsonResponse.fail(500, translatedMessage); } }场景二业务代码中的手动调用在某些场景下我们需要在业务逻辑中直接获取翻译文本例如生成动态文件名、日志信息或业务消息。import org.springframework.stereotype.Service; Service public class ExportService { Autowired private I18nService i18nService; public String generateExportFileName(String moduleName) { // export.filename.template - {0} 导出_{1}.xlsx // 假设当前用户的Locale是英文则会得到 {0} Export_{1}.xlsx String template i18nService.getMessage(export.filename.template); String timestamp // ...获取当前时间字符串... return String.format(template, moduleName, timestamp); } }场景三高级异步线程中的上下文传递LocaleContextHolder默认使用ThreadLocal来存储Locale这意味着在一个新线程中是无法自动获取到主线程的语言信息的。这在异步任务如Async或手动提交到线程池中是一个常见陷阱。解决方案是在启动异步任务前从主线程捕获Locale在异步任务开始时将其设置到子线程的上下文中。使用 AOP面向切面编程是处理此类横切关注点的最佳方式。import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.context.i18n.LocaleContextHolder; import java.util.Locale; import java.util.concurrent.Executor; Aspect Component public class AsyncI18nAspect { Autowired private Executor taskExecutor; // 注入你的线程池 // 定义一个切点环绕所有使用MyAsyncExport注解的方法 Around(annotation(myAsyncExport)) public Object handleAsyncExport(ProceedingJoinPoint point, MyAsyncExport myAsyncExport) throws Throwable { // 1. 在主线程中捕获当前的Locale final Locale locale LocaleContextHolder.getLocale(); final Object[] args point.getArgs(); // 2. 提交到线程池执行 taskExecutor.execute(() - { try { // 3. 在子线程中设置捕获到的Locale LocaleContextHolder.setLocale(locale); // 4. 执行原始的业务逻辑 point.proceed(args); } catch (Throwable e) { // 异常处理逻辑 log.error(Error during async export, e); } finally { // 5. 【极其重要】清理子线程的ThreadLocal防止内存泄漏 LocaleContextHolder.resetLocaleContext(); } }); // 异步任务已提交主线程可以立即返回 return buildSuccessResponse(); // 返回一个表示任务已提交的响应 } }四、总结#