Spring Boot 实现多语言国际化拦截器
Spring Boot 实现多语言国际化拦截器 - 完整教程
本文将详细介绍如何在 Spring Boot 项目中实现一套完整的多语言国际化(i18n)拦截器方案,包括请求拦截器和响应拦截器的双重设计,实现零侵入、灵活切换的多语言支持。
📋 目录
- 方案概述
- 核心设计思路
- 技术实现
- 前端集成
- 最佳实践
- 常见问题
方案概述
💡 设计目标
- 零侵入:业务代码使用工具类,自动翻译
- 灵活切换:前端通过请求头动态切换语言
- 安全可控:精准控制翻译范围,避免重复翻译
- 标准兼容:基于 Spring i18n 标准实现
🎯 支持的语言
- 🇨🇳 zh-CN:简体中文(默认)
- 🇺🇸 en-US:英文
- 🇭🇰 zh-HK:繁體中文
🏗️ 整体架构
┌─────────────────────────────────────────────────────────────────┐
│ HTTP 请求 │
│ Header: X-Language: en-US │
└───────────────────────────┬─────────────────────────────────────┘│▼
┌─────────────────────────────────────────────────────────────────┐
│ LanguageInterceptor(请求拦截) │
│ 1. 从请求头获取 X-Language │
│ 2. 解析为 Locale 对象(en-US → Locale.US) │
│ 3. 设置到 LocaleContextHolder │
└───────────────────────────┬─────────────────────────────────────┘│▼
┌─────────────────────────────────────────────────────────────────┐
│ Controller 层 │
│ MessageUtils.message("error.not.found") │
│ └─> 从 LocaleContextHolder 获取当前 Locale │
│ └─> 从 messages_en_US.properties 读取翻译 │
│ └─> 返回 "Not found" │
└───────────────────────────┬─────────────────────────────────────┘│▼
┌─────────────────────────────────────────────────────────────────┐
│ I18nResponseBodyAdvice(响应拦截) │
│ 1. 检查返回值是否为统一响应对象 │
│ 2. 翻译框架固定消息(如 "成功" → "Success") │
│ 3. 业务错误消息已在 Controller 翻译,不再处理 │
└───────────────────────────┬─────────────────────────────────────┘│▼
┌─────────────────────────────────────────────────────────────────┐
│ HTTP 响应 │
│ { "code": 200, "message": "Success", "data": {...} } │
└─────────────────────────────────────────────────────────────────┘
核心设计思路
1. 双重拦截器设计
为什么需要两个拦截器?
| 拦截器 | 职责 | 拦截点 |
|---|---|---|
| LanguageInterceptor | 解析请求头中的语言设置 | HandlerInterceptor.preHandle |
| I18nResponseBodyAdvice | 翻译响应中的固定消息 | ResponseBodyAdvice |
设计理由:
- ✅ 职责分离:请求处理和响应处理分离,符合单一职责原则
- ✅ 执行时机:请求拦截器在 Controller 执行前设置 Locale,响应拦截器在返回前翻译
- ✅ 灵活控制:可以独立启用/禁用某个拦截器
2. 翻译策略:白名单模式
关键决策:只翻译框架固定消息,不翻译业务消息
原因:
- ✅ 避免重复翻译:业务错误消息已在异常处理器中翻译
- ✅ 避免误判:不会将业务消息当作消息键去查询
- ✅ 性能优化:不需要每次都查询 MessageSource
流程对比:
❌ 全部翻译模式(不推荐)
Controller: MessageUtils.message("error.not.found") → "Not found"↓
ResponseBodyAdvice: translateMessage("Not found") → 查询 MessageSource↓
性能浪费 + 可能误判✅ 白名单模式(推荐)
Controller: MessageUtils.message("error.not.found") → "Not found"↓
ResponseBodyAdvice: "Not found" 不在白名单 → 不翻译↓
精准控制 + 性能优化
技术实现
第一步:配置 Spring 国际化
1.1 添加配置
# application.yml
spring:messages:basename: i18n/messagesencoding: UTF-8cache-duration: 3600 # 生产环境缓存1小时# cache-duration: -1 # 开发环境不缓存
1.2 创建资源文件
文件结构:
src/main/resources/└─ i18n/├─ messages_zh_CN.properties # 简体中文├─ messages_en_US.properties # 英文└─ messages_zh_HK.properties # 繁體中文
messages_zh_CN.properties:
# 通用消息
operation.success=操作成功
operation.failed=操作失败# 错误消息
error.not.found=数据不存在
error.unauthorized=没有权限
error.server.internal=服务器内部错误# 验证消息
validation.required={0}不能为空
validation.invalid={0}格式不正确
messages_en_US.properties:
# General messages
operation.success=Operation successful
operation.failed=Operation failed# Error messages
error.not.found=Not found
error.unauthorized=Unauthorized
error.server.internal=Internal server error# Validation messages
validation.required={0} is required
validation.invalid={0} format is invalid
messages_zh_HK.properties:
# 通用消息
operation.success=操作成功
operation.failed=操作失敗# 錯誤消息
error.not.found=數據不存在
error.unauthorized=沒有權限
error.server.internal=服務器內部錯誤# 驗證消息
validation.required={0}不能為空
validation.invalid={0}格式不正確
第二步:实现 MessageUtils 工具类
package com.example.utils;import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Locale;/*** 国际化消息工具类*/
@Slf4j
@Component
public class MessageUtils {@Resourceprivate MessageSource messageSource;private static MessageSource staticMessageSource;@PostConstructpublic void init() {staticMessageSource = messageSource;}/*** 获取国际化消息* @param code 消息键* @param args 参数* @return 翻译后的消息*/public static String message(String code, Object... args) {try {Locale locale = LocaleContextHolder.getLocale();return staticMessageSource.getMessage(code, args, locale);} catch (Exception e) {log.warn("获取国际化消息失败: code={}, args={}", code, args, e);return code;}}/*** 获取国际化消息(指定默认值)* @param code 消息键* @param defaultMessage 默认消息* @param args 参数* @return 翻译后的消息*/public static String message(String code, String defaultMessage, Object... args) {try {Locale locale = LocaleContextHolder.getLocale();return staticMessageSource.getMessage(code, args, defaultMessage, locale);} catch (Exception e) {log.warn("获取国际化消息失败: code={}, defaultMessage={}", code, defaultMessage, e);return defaultMessage;}}
}
第三步:实现 LanguageInterceptor(请求拦截器)
package com.example.interceptor;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.support.RequestContextUtils;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;/*** 多语言拦截器* 从请求头中获取语言参数,设置当前线程的 Locale*/
@Slf4j
@Component
public class LanguageInterceptor implements HandlerInterceptor {/*** 请求头中的语言参数名*/private static final String LANGUAGE_HEADER = "X-Language";/*** 默认语言(简体中文)*/private static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {try {// 从请求头获取语言参数String language = request.getHeader(LANGUAGE_HEADER);// 解析 LocaleLocale locale = DEFAULT_LOCALE;if (StringUtils.hasText(language)) {locale = parseLocale(language);log.debug("多语言拦截器 - 请求头语言: {}, 解析后的Locale: {}", language, locale);} else {log.debug("未指定语言,使用默认语言: {}", locale);}// 设置 LocaleLocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);if (localeResolver != null) {localeResolver.setLocale(request, response, locale);}} catch (Exception e) {log.error("语言拦截器异常,使用默认语言", e);// 异常不阻塞请求}return true;}/*** 解析语言字符串为 Locale 对象* @param language 语言字符串(如 en-US、zh-CN)* @return Locale 对象*/private Locale parseLocale(String language) {switch (language.toLowerCase()) {case "en-us":return Locale.US;case "zh-cn":return Locale.SIMPLIFIED_CHINESE;case "zh-hk":return new Locale("zh", "HK"); // 繁體中文default:log.warn("不支持的语言: {}, 使用默认语言", language);return DEFAULT_LOCALE;}}
}
第四步:实现 I18nResponseBodyAdvice(响应拦截器)
package com.example.interceptor;import com.example.domain.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;import java.lang.reflect.Field;
import java.util.Locale;/*** 多语言响应拦截器* 自动翻译统一响应对象中的框架固定消息*/
@Slf4j
@RestControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE) // 最后执行,确保在其他拦截器之后
public class I18nResponseBodyAdvice implements ResponseBodyAdvice<Object> {@Overridepublic boolean supports(MethodParameter returnType,Class<? extends HttpMessageConverter<?>> converterType) {// 只处理统一响应对象return returnType.getParameterType().equals(Result.class) ||returnType.getParameterType().getName().endsWith(".Result");}@Overridepublic Object beforeBodyWrite(Object body,MethodParameter returnType,MediaType selectedContentType,Class<? extends HttpMessageConverter<?>> selectedConverterType,ServerHttpRequest request,ServerHttpResponse response) {if (body == null) {return null;}try {// 获取 message 字段String message = getMessageFromResult(body);if (message == null || message.isEmpty()) {return body;}// 翻译消息(只翻译白名单中的固定消息)String translatedMessage = translateMessage(message);// 如果翻译后的消息与原消息不同,则更新if (!message.equals(translatedMessage)) {setMessageToResult(body, translatedMessage);log.debug("多语言翻译 - 原消息: {}, 翻译后: {}", message, translatedMessage);}} catch (Exception e) {log.error("多语言翻译失败", e);// 异常不影响业务}return body;}/*** 翻译消息(白名单模式)* 只翻译框架固定消息,不翻译业务消息*/private String translateMessage(String message) {Locale locale = LocaleContextHolder.getLocale();String language = locale.toString();// 白名单:只翻译这些固定消息switch (message) {case "成功":case "操作成功":return language.startsWith("en") ? "Success" : language.startsWith("zh_HK") ? "成功" : "成功";case "失败":case "操作失败":return language.startsWith("en") ? "Failed" : language.startsWith("zh_HK") ? "失敗" : "失败";default:// 其他消息不翻译(已在业务层翻译)return message;}}/*** 从结果对象中获取 message 字段*/private String getMessageFromResult(Object result) {try {// 方式1:直接获取字段Field messageField = result.getClass().getDeclaredField("message");messageField.setAccessible(true);return (String) messageField.get(result);} catch (Exception e) {// 方式2:使用 getter 方法try {return (String) result.getClass().getMethod("getMessage").invoke(result);} catch (Exception ex) {log.debug("无法获取message字段", ex);return null;}}}/*** 设置结果对象的 message 字段*/private void setMessageToResult(Object result, String message) {try {// 方式1:直接设置字段Field messageField = result.getClass().getDeclaredField("message");messageField.setAccessible(true);messageField.set(result, message);} catch (Exception e) {// 方式2:使用 setter 方法try {result.getClass().getMethod("setMessage", String.class).invoke(result, message);} catch (Exception ex) {log.debug("无法设置message字段", ex);}}}
}
第五步:注册拦截器
package com.example.config;import com.example.interceptor.LanguageInterceptor;
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 javax.annotation.Resource;
import java.util.Locale;/*** Web MVC 配置*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Resourceprivate LanguageInterceptor languageInterceptor;/*** 配置 LocaleResolver*/@Beanpublic LocaleResolver localeResolver() {SessionLocaleResolver resolver = new SessionLocaleResolver();resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);return resolver;}/*** 注册拦截器*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(languageInterceptor).addPathPatterns("/**") // 拦截所有请求.excludePathPatterns("/static/**", "/public/**"); // 排除静态资源}
}
第六步:在业务代码中使用
6.1 统一响应对象
package com.example.domain;import lombok.Data;/*** 统一响应对象*/
@Data
public class Result<T> {private Integer code;private String message;private T data;public static <T> Result<T> success() {Result<T> result = new Result<>();result.setCode(200);result.setMessage("成功"); // 这个会被 I18nResponseBodyAdvice 翻译return result;}public static <T> Result<T> success(T data) {Result<T> result = success();result.setData(data);return result;}public static <T> Result<T> failed(String message) {Result<T> result = new Result<>();result.setCode(500);result.setMessage(message);return result;}
}
6.2 Controller 层使用
package com.example.controller;import com.example.domain.Result;
import com.example.utils.MessageUtils;
import org.springframework.web.bind.annotation.*;/*** 示例 Controller*/
@RestController
@RequestMapping("/api/demo")
public class DemoController {/*** 成功响应示例*/@GetMapping("/success")public Result<String> success() {// 返回成功,message 会被自动翻译return Result.success("Hello World");}/*** 错误响应示例*/@GetMapping("/error")public Result<Void> error() {// 使用 MessageUtils 翻译业务消息return Result.failed(MessageUtils.message("error.not.found"));}/*** 带参数的消息示例*/@GetMapping("/validation")public Result<Void> validation(@RequestParam String field) {// 带占位符的消息return Result.failed(MessageUtils.message("validation.required", field));}
}
6.3 全局异常处理器
package com.example.handler;import com.example.domain.Result;
import com.example.exception.BusinessException;
import com.example.utils.MessageUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** 全局异常处理器*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 业务异常处理*/@ExceptionHandler(BusinessException.class)public Result<Void> handleBusinessException(BusinessException e) {log.warn("业务异常: {}", e.getMessage());// 异常消息应该是消息键,这里进行翻译String message = MessageUtils.message(e.getMessage(), e.getMessage());return Result.failed(message);}/*** 系统异常处理*/@ExceptionHandler(Exception.class)public Result<Void> handleException(Exception e) {log.error("系统异常", e);return Result.failed(MessageUtils.message("error.server.internal"));}
}
前端集成
1. Axios 全局配置(推荐)
// request.js
import axios from 'axios';const service = axios.create({baseURL: '/api',timeout: 10000
});// 请求拦截器
service.interceptors.request.use(config => {// 从本地存储获取语言设置const language = localStorage.getItem('language') || 'zh-CN';config.headers['X-Language'] = language;return config;},error => {return Promise.reject(error);}
);export default service;
2. 语言切换组件
Vue 3 示例
<template><div class="language-switcher"><select v-model="currentLanguage" @change="changeLanguage"><option value="zh-CN">简体中文</option><option value="en-US">English</option><option value="zh-HK">繁體中文</option></select></div>
</template><script setup>
import { ref, onMounted } from 'vue';const currentLanguage = ref('zh-CN');onMounted(() => {// 从本地存储读取语言设置currentLanguage.value = localStorage.getItem('language') || 'zh-CN';
});const changeLanguage = () => {// 保存到本地存储localStorage.setItem('language', currentLanguage.value);// 刷新页面以应用新语言window.location.reload();
};
</script><style scoped>
.language-switcher select {padding: 8px 12px;border: 1px solid #ddd;border-radius: 4px;font-size: 14px;
}
</style>
React 示例
import { useState, useEffect } from 'react';function LanguageSwitcher() {const [language, setLanguage] = useState('zh-CN');useEffect(() => {// 从本地存储读取语言设置const savedLanguage = localStorage.getItem('language') || 'zh-CN';setLanguage(savedLanguage);}, []);const changeLanguage = (newLanguage) => {setLanguage(newLanguage);localStorage.setItem('language', newLanguage);// 刷新页面以应用新语言window.location.reload();};return (<div className="language-switcher"><select value={language} onChange={(e) => changeLanguage(e.target.value)}><option value="zh-CN">简体中文</option><option value="en-US">English</option><option value="zh-HK">繁體中文</option></select></div>);
}export default LanguageSwitcher;
3. 单个请求指定语言
// 临时使用英文请求
axios.get('/api/demo/success', {headers: {'X-Language': 'en-US'}
});
最佳实践
1. 消息键命名规范
推荐格式:模块.类型.具体内容
# ✅ 推荐(清晰、易维护)
error.not.found=数据不存在
error.unauthorized=没有权限
validation.required={0}不能为空
operation.success=操作成功# ❌ 不推荐(不清晰)
err001=数据不存在
msg1=成功
error=错误
2. 资源文件管理
分类组织:
# messages_zh_CN.properties# ========== 通用消息 ==========
operation.success=操作成功
operation.failed=操作失败# ========== 错误消息 ==========
error.not.found=数据不存在
error.unauthorized=没有权限
error.server.internal=服务器内部错误# ========== 验证消息 ==========
validation.required={0}不能为空
validation.invalid={0}格式不正确
validation.length={0}长度必须在{1}到{2}之间
3. 翻译质量保障
使用专业翻译工具:
- 📖 DeepL:质量最高(推荐)
- 📖 Google Translate:快速翻译
- 📖 专业翻译人员:重要文案
翻译检查清单:
- ✅ 术语统一
- ✅ 语气一致
- ✅ 占位符位置正确
- ✅ 长度适配 UI
4. 开发流程
新增消息的标准流程:
- 在资源文件中添加消息键(所有语言)
- 在代码中使用 MessageUtils.message()
- 测试所有语言
# 测试简体中文
curl -H "X-Language: zh-CN" http://localhost:8080/api/demo/error# 测试英文
curl -H "X-Language: en-US" http://localhost:8080/api/demo/error# 测试繁体中文
curl -H "X-Language: zh-HK" http://localhost:8080/api/demo/error
5. 性能优化
资源文件缓存:
# 生产环境:缓存资源文件
spring:messages:cache-duration: 3600 # 1小时# 开发环境:不缓存,方便修改
spring:messages:cache-duration: -1
避免频繁查询:
// ❌ 不推荐:循环中查询
for (Item item : items) {String msg = MessageUtils.message("template") + item.getName();
}// ✅ 推荐:循环外查询
String template = MessageUtils.message("template");
for (Item item : items) {String msg = template + item.getName();
}
常见问题
Q1:前端不传 X-Language 会怎样?
A:使用默认语言(简体中文)
// 响应示例
{"code": 200,"message": "成功","data": null
}
Q2:支持动态切换语言吗?
A:支持,每次请求都可以不同
// 请求1:使用简体中文
axios.get('/api/demo/success', { headers: { 'X-Language': 'zh-CN' } });
// 响应:{ "message": "成功" }// 请求2:使用英文
axios.get('/api/demo/success', { headers: { 'X-Language': 'en-US' } });
// 响应:{ "message": "Success" }
实现原理:Locale 存储在 LocaleContextHolder(ThreadLocal),每个请求独立。
Q3:为什么业务消息不在 I18nResponseBodyAdvice 中翻译?
A:避免重复翻译
执行流程:
Controller 抛异常↓
GlobalExceptionHandler 捕获└─ MessageUtils.message("error.not.found") // ✅ 第一次翻译└─ 返回 Result.failed("Not found")↓
I18nResponseBodyAdvice 拦截└─ "Not found" 不在白名单└─ 不翻译 // ✅ 避免重复翻译↓
返回给前端
Q4:如何添加新的语言支持?
步骤:
- 添加资源文件:
messages_ja_JP.properties - 添加解析规则:
private Locale parseLocale(String language) {switch (language.toLowerCase()) {case "zh-cn": return Locale.SIMPLIFIED_CHINESE;case "en-us": return Locale.US;case "zh-hk": return new Locale("zh", "HK");case "ja-jp": return Locale.JAPAN; // 新增default: return DEFAULT_LOCALE;}
}
- 更新白名单翻译规则(如果需要)
Q5:支持占位符参数吗?
A:完全支持
# messages_zh_CN.properties
user.greeting=您好,{0}!您有{1}条新消息。# messages_en_US.properties
user.greeting=Hello, {0}! You have {1} new messages.
String message = MessageUtils.message("user.greeting", "张三", 5);
// 简体中文:您好,张三!您有5条新消息。
// 英文:Hello, 张三! You have 5 new messages.
Q6:如何调试拦截器?
步骤1:启用日志
# application.yml
logging:level:com.example.interceptor: DEBUG
步骤2:发送请求
curl -H "X-Language: en-US" http://localhost:8080/api/demo/error
步骤3:查看日志
[DEBUG] LanguageInterceptor - 多语言拦截器 - 请求头语言: en-US, 解析后的Locale: en_US
[DEBUG] I18nResponseBodyAdvice - 多语言翻译 - 原消息: 成功, 翻译后: Success
Q7:可以在 Service 层获取当前语言吗?
A:可以
@Service
public class NotificationService {public void sendEmail(Long userId) {Locale locale = LocaleContextHolder.getLocale();String language = locale.toString();if (language.startsWith("en")) {sendEnglishEmail(userId);} else {sendChineseEmail(userId);}}
}
注意:
- ✅ 同步调用:可用
- ⚠️ 异步调用:需手动传递 Locale
总结
本文介绍了如何在 Spring Boot 中实现一套完整的多语言国际化拦截器方案:
核心组件
- LanguageInterceptor:解析请求头中的语言参数
- I18nResponseBodyAdvice:翻译响应中的固定消息
- MessageUtils:业务代码中获取翻译消息
设计亮点
- ✅ 双重拦截器:请求拦截 + 响应拦截
- ✅ 白名单模式:只翻译框架固定消息,避免重复翻译
- ✅ 零侵入:业务代码使用工具类,自动翻译
- ✅ 灵活切换:前端通过请求头动态切换
- ✅ 异常保护:翻译失败不影响业务
适用场景
- ✅ 多语言后台管理系统
- ✅ 国际化 SaaS 平台
- ✅ 面向海外用户的 API 服务
- ✅ 需要动态切换语言的应用
完整示例代码
完整的示例代码已上传至 GitHub(示例地址),包括:
- ✅ 完整的拦截器实现
- ✅ 前端集成示例(Vue 3 + React)
- ✅ 单元测试和集成测试
- ✅ Docker 部署配置
希望这篇教程对你有帮助!如果有任何问题,欢迎在评论区讨论。
作者:[南风]
发布时间:2025-11-05
标签:Spring Boot 国际化 i18n 拦截器 多语言
