乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

国际化

本章是 Spring MVC 教程中"高级特性与最佳实践"章节的基础设施内容。在讲解拦截器、跨域、缓存等高级机制之前,先理解国际化(i18n)——它解决的是"同一套系统如何服务不同语言用户"的问题。Spring MVC 的国际化支持不是孤立的翻译工具,而是与 LocaleResolver、ViewResolver 紧密协作的完整链路。


定义与作用

国际化(Internationalization)缩写为 i18n——因为单词首字母 I 和末字母 n 之间有 18 个字母。它指的是软件系统在不修改源代码的前提下,支持多种语言、多种地区格式的能力。与国际化相对的是本地化(Localization,L10n),指针对特定地区进行的具体适配工作。

在 Web 应用中,国际化通常涉及:

  • 界面文本:按钮文字、提示信息、菜单标签的多语言切换
  • 日期格式:中文环境用 2024年3月15日,美国环境用 03/15/2024
  • 数字格式:德国用逗号作为小数点(1,5),美国用点号(1.5)
  • 货币符号:人民币 ¥、美元 $、欧元 €

Spring MVC 的国际化机制由两个核心组件协作完成:

  1. LocaleResolver:从客户端请求中解析出当前用户的语言环境(Locale)。Locale 包含语言代码(如 zh)和地区代码(如 CN),组合成 zh_CN。

  2. MessageSource:根据 Locale 加载对应的多语言资源文件,提供"按 key 取翻译"的服务。Controller 和视图模板通过 key 获取当前语言下的实际文本。

生活类比:跨国酒店的前台

想象一家跨国连锁酒店:

  • LocaleResolver 是前台接待员:客人进门时,接待员通过观察(Accept-Header)、询问(Session 记录)或查看房卡(Cookie)判断客人来自哪个国家——中国、美国还是日本?
  • MessageSource 是多语言服务手册:酒店准备了一本手册,同一个服务编号在不同语言页有不同翻译。WELCOME_MSG 在中文页是"欢迎光临",在英文页是"Welcome",在日文页是"ようこそ"。
  • Controller 和 View 是服务员:服务员不需要学会所有语言,只需要记住服务编号(key),然后根据接待员判断的客人国籍(Locale),从手册对应页查找翻译。

这个类比的关键在于:判断语言环境(LocaleResolver)和提供翻译(MessageSource)是独立的两件事,通过 key 串联起来。


核心原理

国际化解析流程

流程解读:

  1. 请求进入:浏览器发送 HTTP 请求,可能携带 Accept-Language: zh-CN,en-US;q=0.9 头,或 URL 参数 ?lang=zh_CN。
  2. Locale 解析:DispatcherServlet 调用 LocaleResolver 从请求中提取 Locale。不同的 LocaleResolver 采用不同策略(见下表)。
  3. 消息获取:Controller 或视图模板通过 MessageSource,传入 key 和 Locale,获取对应语言的文本。
  4. 资源加载:MessageSource 按命名规范找到 messages_zh_CN.properties,返回 "welcome.title=员工管理系统" 中的值。
  5. 响应渲染:视图模板将国际化文本填充到页面,返回给浏览器。

常见 LocaleResolver 实现对比

实现类解析策略适用场景优点缺点
AcceptHeaderLocaleResolver读取 HTTP 请求头 Accept-Language默认策略,适合无状态 REST API无需额外配置,浏览器自动发送用户无法手动切换语言
SessionLocaleResolver从 HttpSession 中读取 SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME需要用户手动切换语言的 Web 应用切换后整个会话保持同一语言会话过期后语言设置丢失
CookieLocaleResolver从 Cookie 中读取语言偏好希望语言偏好长期保存的站点关闭浏览器后再打开,语言设置仍在用户可能禁用 Cookie
FixedLocaleResolver固定使用系统默认 Locale单语言系统,强制禁用国际化配置最简单完全无国际化能力

选择建议:

  • 如果系统只服务国内用户,用 AcceptHeaderLocaleResolver(默认即可,无需配置)
  • 如果系统有语言切换按钮(如中英文切换),用 CookieLocaleResolver,让选择持久化
  • 如果系统同时有 Web 页面和 REST API,可以配置两个 DispatcherServlet,分别用不同的 LocaleResolver

资源文件命名规范与结构

Spring MVC 的 MessageSource 默认加载 classpath 下名为 messages 开头的 properties 文件:

src/main/resources/
├── messages.properties              # 默认语言(兜底)
├── messages_zh_CN.properties        # 简体中文(中国大陆)
├── messages_zh_TW.properties        # 繁体中文(中国台湾)
├── messages_en_US.properties        # 美式英语
├── messages_en_GB.properties        # 英式英语
└── messages_ja_JP.properties        # 日语

命名规则:messages_语言代码_国家代码.properties

文件语言代码国家代码匹配的 Locale
messages.properties——所有未匹配的 Locale 的兜底
messages_zh_CN.propertieszhCNnew Locale("zh", "CN")
messages_en_US.propertiesenUSnew Locale("en", "US")
messages_ja.propertiesja—new Locale("ja")(只指定语言)

资源文件内容示例:

# messages.properties(默认/英文兜底)
app.title=Employee Management System
app.welcome=Welcome, {0}
button.save=Save
button.delete=Delete
error.notfound=Resource not found
# messages_zh_CN.properties(简体中文)
app.title=员工管理系统
app.welcome=欢迎,{0}
button.save=保存
button.delete=删除
error.notfound=资源不存在

占位符说明:{0}、{1} 等是消息格式化占位符,运行时由 MessageSource 替换为实际参数。例如 getMessage("app.welcome", new Object[]{"张三"}, locale) 返回 "欢迎,张三"。


完整示例

场景

飞翔科技的员工管理系统需要支持中英文双语切换,供国内总部和海外分公司共同使用。CTO 大翔要求:

  1. 页面右上角有语言切换按钮(中文 / English)
  2. 切换后整个会话保持所选语言
  3. 所有界面文本、错误提示、按钮标签都要国际化
  4. 日期和数字格式随语言环境自动变化

架构师白歌选择 CookieLocaleResolver 作为 Locale 持久化方案。小崔负责配置和 Controller,黄俪负责 Thymeleaf 模板中的国际化标签,李眉负责资源文件的翻译校对。

小崔的配置实现

// WebConfig.java
package com.feixiang.web.config;

import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
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.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

import java.util.Locale;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    /**
     * 配置 MessageSource:加载 classpath 下的 messages*.properties
     */
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");           // 基础名 messages,自动匹配后缀
        messageSource.setDefaultEncoding("UTF-8");       // 资源文件使用 UTF-8 编码
        messageSource.setFallbackToSystemLocale(false);  // 不匹配时不回退到系统默认语言
        messageSource.setUseCodeAsDefaultMessage(true);    // key 找不到时返回 key 本身,而非抛异常
        return messageSource;
    }

    /**
     * 配置 LocaleResolver:使用 Cookie 持久化语言选择
     */
    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver resolver = new CookieLocaleResolver();
        resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);  // 默认简体中文
        resolver.setCookieName("feixiang_lang");               // Cookie 名称
        resolver.setCookieMaxAge(3600 * 24 * 30);             // Cookie 有效期 30 天
        return resolver;
    }

    /**
     * 注册 LocaleChangeInterceptor:拦截 ?lang=xx 参数切换语言
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
        interceptor.setParamName("lang");  // URL 参数名:?lang=en_US
        registry.addInterceptor(interceptor);
    }
}

配置分析:

  • ResourceBundleMessageSource 是 MessageSource 的标准实现,自动加载 messages*.properties
  • setBasename("messages") 表示加载 messages.properties、messages_zh_CN.properties 等所有以 messages 开头的资源文件
  • CookieLocaleResolver 把用户选择的语言存入 Cookie,30 天内再次访问自动恢复
  • LocaleChangeInterceptor 拦截 ?lang=en_US 这类 URL 参数,自动修改当前 Locale 并写入 Cookie

资源文件

# messages.properties(英文兜底)
app.title=Employee Management System
nav.home=Home
nav.employees=Employees
nav.departments=Departments
button.search=Search
button.reset=Reset
button.export=Export
msg.welcome=Welcome to {0}
error.access.denied=Access denied. Please contact administrator.
# messages_zh_CN.properties(简体中文)
app.title=员工管理系统
nav.home=首页
nav.employees=员工管理
nav.departments=部门管理
button.search=查询
button.reset=重置
button.export=导出
msg.welcome=欢迎使用{0}
error.access.denied=权限不足,请联系管理员。

小崔的 Controller

// EmployeeController.java
package com.feixiang.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Locale;

@Controller
public class EmployeeController {

    @Autowired
    private MessageSource messageSource;

    @GetMapping("/employees")
    public String list(Model model, Locale locale) {
        // 在 Controller 中获取国际化消息
        String title = messageSource.getMessage("app.title", null, locale);
        String welcome = messageSource.getMessage("msg.welcome", 
                new Object[]{title}, locale);
        
        model.addAttribute("welcomeMsg", welcome);
        return "employeeList";
    }
}

注意:Spring MVC 会自动将 Locale 注入到 Controller 方法的参数中——这是 LocaleResolver 与 DispatcherServlet 协作的结果。

黄俪的 Thymeleaf 模板

<!-- employeeList.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title th:text="#{app.title}">Employee Management</title>
</head>
<body>
    <!-- 语言切换按钮 -->
    <div class="lang-switcher">
        <a href="?lang=zh_CN">中文</a>
        <a href="?lang=en_US">English</a>
    </div>

    <h1 th:text="${welcomeMsg}">Welcome</h1>

    <!-- 导航菜单 -->
    <nav>
        <a th:href="@{/}" th:text="#{nav.home}">Home</a>
        <a th:href="@{/employees}" th:text="#{nav.employees}">Employees</a>
        <a th:href="@{/departments}" th:text="#{nav.departments}">Departments</a>
    </nav>

    <!-- 操作按钮 -->
    <div class="actions">
        <button th:text="#{button.search}">Search</button>
        <button th:text="#{button.reset}">Reset</button>
        <button th:text="#{button.export}">Export</button>
    </div>

    <!-- 错误提示 -->
    <div th:if="${error}" class="error" th:text="#{error.access.denied}">
        Access denied
    </div>
</body>
</html>

Thymeleaf 国际化语法:

  • th:text="#{app.title}":从 MessageSource 读取 key 为 app.title 的消息
  • th:text="${welcomeMsg}":读取 Controller 放入 Model 的属性(已在 Controller 中完成国际化)
  • ?lang=zh_CN 和 ?lang=en_US:点击后 LocaleChangeInterceptor 拦截,修改 Cookie 中的语言设置,页面刷新后自动加载对应资源文件

李眉的部署检查

# 检查资源文件是否正确打包到 classpath
cd target/classes
ls messages*.properties
# 应输出:
# messages.properties
# messages_zh_CN.properties

李眉的常见问题排查:

  • 中文乱码 → 检查 messageSource.setDefaultEncoding("UTF-8") 是否配置
  • 切换语言无效 → 检查 Cookie 是否被浏览器禁用,或 LocaleChangeInterceptor 是否注册
  • key 找不到 → 检查 setUseCodeAsDefaultMessage(true),若设为 false 会抛 NoSuchMessageException

易错场景与面试考点

误区一:资源文件使用 ISO-8859-1 编码存储中文

错误做法:在旧版 Spring 或某些 IDE 默认设置中,properties 文件用 ISO-8859-1 编码,中文需要写成 Unicode 转义序列:

# 错误:手动转义,维护困难
app.title=\u5458\u5DE5\u7BA1\u7406\u7CFB\u7EDF

纠正:Spring 5 和 Spring Boot 2.x 支持 UTF-8 编码的 properties 文件:

messageSource.setDefaultEncoding("UTF-8");

然后直接用 UTF-8 保存文件:

# 正确:直接写中文
app.title=员工管理系统

注意:如果项目需要兼容旧版 Spring(4.x 及以下),仍需使用 native2ascii 工具转换,或改用 YAML 格式的资源文件(Spring Boot 支持 messages.yml)。

误区二:LocaleResolver 和 LocaleChangeInterceptor 混用不当

错误配置:

@Bean
public LocaleResolver localeResolver() {
    return new AcceptHeaderLocaleResolver();  // 只读请求头
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LocaleChangeInterceptor());  // 试图通过 URL 参数切换
}

问题:AcceptHeaderLocaleResolver 不支持 setLocale() 方法,LocaleChangeInterceptor 调用时会抛 UnsupportedOperationException。

纠正:如果要用 ?lang=xx 切换语言,必须使用支持写操作的 LocaleResolver:

@Bean
public LocaleResolver localeResolver() {
    return new CookieLocaleResolver();  // ✅ 支持 setLocale()
}

误区三:在 REST API 中错误地使用 SessionLocaleResolver

错误场景:前后端分离项目中,前端每次请求都是独立的 AJAX 调用,没有会话概念。此时使用 SessionLocaleResolver 会导致语言切换只在某个 AJAX 请求中生效,下次请求又恢复默认。

纠正:前后端分离项目应使用 AcceptHeaderLocaleResolver(前端在请求头中携带 Accept-Language),或让前端在每次请求中携带语言参数,由自定义 LocaleResolver 解析。

面试高频:Spring MVC 国际化的核心组件是什么?它们如何协作?

标准回答:

Spring MVC 国际化由 LocaleResolver 和 MessageSource 两个核心组件协作完成:

  1. LocaleResolver 负责"判断用户用什么语言"。它从 HTTP 请求中提取 Locale 信息——可能是请求头(AcceptHeaderLocaleResolver)、Session(SessionLocaleResolver)、Cookie(CookieLocaleResolver)或 URL 参数(配合 LocaleChangeInterceptor)。

  2. MessageSource 负责"按 key 取对应语言的文本"。它管理一组 messages_*.properties 资源文件,根据传入的 key 和 Locale,返回匹配的翻译文本。

  3. 协作流程:DispatcherServlet 收到请求后,先调用 LocaleResolver 获取 Locale;Controller 或视图模板通过 #{key} 或 messageSource.getMessage(key, args, locale) 获取翻译;MessageSource 根据 Locale 加载对应的 properties 文件,返回实际文本。


小结

国际化(i18n)不是简单的"翻译界面文字",而是一套从请求中识别语言环境、按 key 加载对应资源、在 Controller 和视图中统一使用的完整机制。Spring MVC 通过 LocaleResolver 解析语言偏好,通过 MessageSource 管理多语言资源文件,两者在 DispatcherServlet 的调度下无缝协作。

实现国际化时,必须注意三个关键点:选择合适的 LocaleResolver(AcceptHeader / Session / Cookie)、资源文件用 UTF-8 编码、兜底文件 messages.properties 必须存在。同时要避免在前后端分离项目中误用 SessionLocaleResolver,以及 AcceptHeaderLocaleResolver 与 LocaleChangeInterceptor 的不兼容配置。

本章与全局的关系:本章讲解了 Spring MVC 的多语言基础设施。下一章"拦截器机制"将深入讲解 LocaleChangeInterceptor 的工作原理——它本质上就是一个 HandlerInterceptor,在请求到达 Controller 之前修改当前 Locale。

上一页
MockMvc测试
下一页
最佳实践