ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Spring Boot i18n 国际化实战:从资源文件到多语言接口完整指南

2026/8/11 20:40:13 拓冰建站 浏览量
Spring Boot i18n 国际化实战:从资源文件到多语言接口完整指南

大家好,我是程序员天天困。

想象一个很常见的场景:团队把 App 推向东南亚市场,海外用户点登录失败,屏幕上弹出来的提示却是中文的「用户名或密码错误」。开发在本地测试全绿,翻出代码一看——错误文案直接写死在 Java 类里。要改文案?重新打包部署吧。

说真的,只要业务有出海计划,后端迟早要碰 Spring Boot i18n。这篇文章就把这套东西从头到尾讲清楚。点个收藏,我们开始。

一、i18n 是什么,为什么后端也需要它

i18n(internationalization,国际化):单词 internationalization 首尾字母 i 和 n 之间有 18 个字母,所以简写为 i18n。指的是在产品设计阶段就让代码具备适配多种语言和地区的能力,而不是事后硬改。你可以理解为「给软件预留多语言插槽」。

和它一起出现的还有 l10n(localization,本地化):i18n 是搭好插槽,l10n 是往插槽里填具体某种语言的翻译和格式(日期、货币、数字符号)。

很多人觉得 i18n 是前端的事,后端返回错误码、前端自己映射文案就行。这种做法在纯 App 场景能跑,但一旦对接第三方回调、开放平台 API、服务端推送消息(邮件、短信、Webhook),文案是从后端直接发出去的,前端根本接不住。

我列几个后端必须做 i18n 的典型场景:

  1. 开放 API:第三方开发者调用你的接口,错误信息得是对方能看懂的语言。
  2. 服务端推送:注册邮件、登录验证码、订单状态短信,这些文案由后端拼接。
  3. 统一错误码体系:后端维护错误码和消息模板,多端共用,避免 iOS、Android、Web 各翻译一遍还对不上。
  4. 日志与审计:部分合规场景要求操作日志按用户地区语言留痕。

说白了,只要文案是从你的服务进程里产生并发给用户的,后端就绕不开 i18n。

二、Spring Boot i18n 的三大核心组件

Spring Boot 处理多语言消息就靠三样东西,记住「查字典」这个类比就够了:

组件 角色 字典类比
MessageSource 消息源,加载并管理多语言资源文件 字典本身
LocaleResolver 区域解析器,决定当前请求用哪种语言 判断读者要查哪种语言版本
LocaleChangeInterceptor 语言切换拦截器,从请求参数里切换语言 读者翻字典前指定语言

MessageSource(消息源):Spring 定义的一个接口,负责根据消息键(key)和区域(Locale)解析出对应文案。Spring Boot 通过 MessageSourceAutoConfiguration 自动装配它,你只需要告诉它资源文件在哪、用什么编码。

LocaleResolver(区域解析器):决定「这次请求到底用什么语言」的策略组件。它可以从 HTTP 头、Cookie、Session 甚至固定值里解析出一个 Locale 对象。DispatcherServlet 处理请求时会调用它,把结果存进 LocaleContextHolder,后续整条链路都能取到。

LocaleChangeInterceptor(语言切换拦截器):一个可选的拦截器,允许通过请求参数(比如 ?lang=en_US)动态切换语言。它本质上是调用 LocaleResolver.setLocale() 把新语言写回 Cookie 或 Session。

整个流程其实就是三步:请求进来 → LocaleResolver 判断语言 → MessageSource 按 key + Locale 查文案。没有任何魔法。

三、从零搭建一个多语言项目

光说不练假把式。下面基于 Spring Boot 3.x 搭一个最小可用的多语言后端,用到的代码都是标准写法。

1、准备资源文件

src/main/resources/ 下建一个 i18n/ 目录,放四份资源文件:

src/main/resources/
└── i18n/├── messages.properties        # 默认兜底├── messages_zh_CN.properties  # 简体中文├── messages_en_US.properties  # 英语(美国)└── messages_ja_JP.properties  # 日语(日本)

messages.properties 是默认文件,当请求的语言找不到对应翻译时,会回退到这里。这个文件必须存在,否则启动和运行时都会有警告。

每份文件内容长这样(默认文件建议用英文兜底),key 保持一致,value 按语言填:

messages.properties

user.welcome=Welcome
user.login.fail=Invalid username or password
user.notfound=User not found: {0}

messages_zh_CN.properties

user.welcome=欢迎回来
user.login.fail=用户名或密码错误
user.notfound=用户不存在:{0}

messages_en_US.properties

user.welcome=Welcome back
user.login.fail=Invalid username or password
user.notfound=User not found: {0}

messages_ja_JP.properties

user.welcome=おかえりなさい
user.login.fail=ユーザー名またはパスワードが正しくありません
user.notfound=ユーザーが見つかりません:{0}

{0} 是占位符,运行时可以用参数替换,后面会演示。

2、配置 application.yml

spring:messages:# 资源文件基础名,不要写 .properties 后缀basename: i18n/messagesencoding: UTF-8# 资源文件缓存秒数;开发时设为 0 方便热更新,生产给 3600cache-duration: 3600# 找不到对应语言的资源文件时,不回退到系统 Locale,直接用 messages.propertiesfallback-to-system-locale: false

basename 支持配置多个,用逗号分隔,比如 i18n/messages,i18n/errors。一般项目一个就够。

3、配置 LocaleResolver 和拦截器

新建一个配置类:

@Configuration
public class I18nConfig implements WebMvcConfigurer {// Bean 名必须叫 localeResolver,Spring 按此名查找@Beanpublic LocaleResolver localeResolver() {CookieLocaleResolver resolver = new CookieLocaleResolver("lang");resolver.setCookieMaxAge(Duration.ofDays(30));resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);return resolver;}// 支持通过 ?lang=en_US 切换语言@Beanpublic LocaleChangeInterceptor localeChangeInterceptor() {LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();interceptor.setParamName("lang");return interceptor;}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(localeChangeInterceptor());}
}

这里有个坑:Bean 名必须localeResolver。Spring 在 DispatcherServlet 里按这个名字去容器里找,你要是写成 cookieLocaleResolver(),它找不到就会回退到默认的 AcceptHeaderLocaleResolver,配置直接失效。

4、封装一个消息工具类

直接在业务代码里每次注入 MessageSource 再传三个参数有点啰嗦,封装一下:

@Component
public class MessageSourceUtils {private final MessageSource messageSource;public MessageSourceUtils(MessageSource messageSource) {this.messageSource = messageSource;}/*** 取当前请求 Locale 对应的文案*/public String get(String key, Object... args) {return messageSource.getMessage(key, args, LocaleContextHolder.getLocale());}/*** 指定 Locale 取文案(后台任务、异步线程里用)*/public String get(String key, Locale locale, Object... args) {return messageSource.getMessage(key, args, locale);}
}

LocaleContextHolder 里的 Locale 是 DispatcherServlet 在请求进来时通过 LocaleResolver 设进去的,整条请求链路都能取。但要注意:异步线程里它是空的,因为 LocaleContextHolder 底层用的是 ThreadLocal。异步场景必须显式传 Locale,这也是我留第二个重载方法的原因。

5、在 Controller 里使用

@RestController
@RequestMapping("/api/users")
public class UserController {private final MessageSourceUtils messageSource;public UserController(MessageSourceUtils messageSource) {this.messageSource = messageSource;}@GetMapping("/welcome")public Result<String> welcome() {return Result.ok(messageSource.get("user.welcome"));}@GetMapping("/{id}")public Result<UserVO> getUser(@PathVariable Long id) {UserVO user = userService.getById(id);if (user == null) {// 只抛错误码和参数,文案由全局异常处理器统一翻译throw new BusinessException("user.notfound", id);}return Result.ok(user);}
}

启动项目,默认请求(Cookie 没设置时走 defaultLocale=zh_CN):

curl http://localhost:8080/api/users/welcome

返回:欢迎回来

通过参数切到英文:

curl "http://localhost:8080/api/users/welcome?lang=en_US"

返回:Welcome back

切到日文:

curl "http://localhost:8080/api/users/welcome?lang=ja_JP"

返回:おかえりなさい

?lang=en_US 第一次请求时,拦截器会把 Locale 写进名为 lang 的 Cookie,之后不带参数也会记住语言,30 天有效。

6、全局异常处理里用 i18n

更常见的做法是业务代码只抛错误码,文案翻译交给全局异常处理器:

@RestControllerAdvice
public class GlobalExceptionHandler {private final MessageSource messageSource;public GlobalExceptionHandler(MessageSource messageSource) {this.messageSource = messageSource;}@ExceptionHandler(BusinessException.class)public ResponseEntity<Result<Void>> handleBusiness(BusinessException e) {// 优先用错误码当 key 去资源文件查;查不到用异常自带的默认消息String message = messageSource.getMessage(e.getCode(),e.getArgs(),e.getDefaultMessage(),LocaleContextHolder.getLocale());return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Result.fail(e.getCode(), message));}
}

这样业务层只管抛 new BusinessException("user.notfound", id),文案怎么显示交给统一的处理器。错误码直接作为资源文件的 key,规范且好维护。

四、四种 LocaleResolver 怎么选

Spring 内置了四种 LocaleResolver,各有适用场景,别一上来就抄 Cookie 方案。

实现 语言来源 持久化 适用场景
AcceptHeaderLocaleResolver 请求头 Accept-Language 无(每次按头解析) 纯 API 服务、对接第三方、无登录态
CookieLocaleResolver 指定 Cookie 浏览器端持久 前后端分离、未登录也要记语言
SessionLocaleResolver HttpSession 会话级 传统服务端渲染、登录后定语言
FixedLocaleResolver 写死一个 Locale 永不变 测试、强制单语言的内部系统

我的选型建议很直接:

  • 纯后端 API,不维护用户语言偏好:用默认的 AcceptHeaderLocaleResolver,啥都不用配。客户端(浏览器、App)会自动带 Accept-Language 头。
  • 需要记住用户选择,且未登录也要生效:用 CookieLocaleResolver。这也是最通用的方案。
  • 登录后由用户中心统一下发语言:用 SessionLocaleResolver 或干脆自定义一个从用户信息里读 Locale 的解析器。
  • 想同时支持 Cookie 和 Header 兜底,可以自定义 LocaleResolver,先读 Cookie,Cookie 没有再回退到 Accept-Language 头。

注意 AcceptHeaderLocaleResolver 不支持通过拦截器切换语言——它的 setLocale() 方法会直接抛 UnsupportedOperationException,因为 HTTP 头是客户端发的,服务端改不了。如果你需要 ?lang=xxx 切换,必须换成 Cookie 或 Session 方案。

五、踩坑记录与最佳实践

实际项目里有几个高频坑,挑最容易踩的说。

1)编码问题:properties 文件乱码

这是最经典的坑。Java 9 之前 JDK 读取 .properties 文件默认按 ISO-8859-1 编码,中文只能写成 \uXXXX 转义;Java 9 起 PropertyResourceBundle 默认改成了 UTF-8,但老项目、老服务器上的乱码问题大多源自这。Spring Boot 3.x 要求 JDK 17+,默认已经是 UTF-8,但显式配置一下更稳妥:

spring:messages:encoding: UTF-8

另外,IDEA 里也要把 properties 文件的编码对齐(Settings → Editor → File Encodings → Default encoding for properties files 选 UTF-8;老项目如果还在用 ISO-8859-1,可以勾选 Transparent native-to-ascii conversion,IDEA 会自动在编辑时显示原文、保存时写转义码)。文件本身的编码和 Spring 配置里声明的编码要一致,一头 UTF-8 一头 ISO-8859-1 必乱。

2)key 的命名要规范

别图省事用 msg1msg2 这种名字。推荐按「模块.场景.含义」分层:

user.login.fail=用户名或密码错误
user.register.email.duplicate=该邮箱已被注册
order.pay.timeout=支付超时,请重试
system.error.internal=系统繁忙,请稍后再试

好处是 IDE 里搜 user. 就能把用户模块所有文案找出来,翻译人员也好按文件分工。

3)用占位符而不是字符串拼接

不同语言语序不一样。中文说「用户 123 不存在」,日语可能是「ユーザー 123 が見つかりません」,数字位置不同。用 {0} 占位符由 MessageFormat 去拼,能保证语序正确:

user.notfound=用户不存在:{0}
messageSource.get("user.notfound", id);

千万别自己用 String.format 或字符串 + 拼,翻译的时候语序全乱。

4)找不到 key 怎么办

MessageSource.getMessage() 默认找不到 key 会抛 NoSuchMessageException。生产环境因为漏配一个翻译就 500 不值得。前面全局异常处理器里用的四参版本:

messageSource.getMessage(code, args, defaultMessage, locale);

第三个参数 defaultMessage 就是兜底文案,查不到 key 时返回它,不会抛异常。建议线上统一用这个版本。

可能有人会问:前端已经做了 i18n,后端还有必要做吗?

看你的文案在哪产生。如果所有提示都是前端本地映射,后端只返回错误码,那后端确实可以不做;但邮件、短信、第三方回调、文件导出这些由后端直接产出文案的场景,后端不做就没法多语言。两者不是互斥的,是分工问题。

六、小结

Spring Boot i18n 没有任何黑魔法,本质就是「按 Locale 查 properties 文件」。记住三个要点:

  1. MessageSource 管字典——配置 basename 和编码,资源文件按 messages_{locale}.properties 命名。
  2. LocaleResolver 定语言——纯 API 用 Header,要记住偏好就用 Cookie,别让 Bean 名写错。
  3. 业务层只抛错误码——文案翻译交给全局异常处理器,用默认消息兜底,避免漏配翻译导致 500。

Spring Boot i18n 代码不复杂,真正的工作量在翻译和 key 的规范管理上。建议项目初期就把 i18n 搭好,别等出海了再回头改硬编码字符串——那时候散落各处的文案能改到你怀疑人生。


我是程序员天天困,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:踩过最离谱的 i18n 坑是什么?