这篇文章让你搞懂 SpringMVC 国际化
松哥之前写过 Spring Boot 国际化的问题,不过那一次没讲源码,这次咱们整点源码来深入理解下这个问题。
国际化,也叫 i18n,为啥叫这个名字呢?因为国际化英文是 internationalization ,在 i 和 n 之间有 18 个字母,所以叫 i18n。我们的应用如果做了国际化就可以在不同的语言环境下,方便的进行切换,最常见的就是中文和英文之间的切换,国际化这个功能也是相当的常见。
1.SpringMVC 国际化配置
还是先来说说用法,再来说源码,这样大家不容易犯迷糊。我们先说在 SSM 中如何处理国际化问题。
首先国际化我们可能有两种需求:
- 在页面渲染时实现国际化(这个借助于 Spring 标签实现)
- 在接口中获取国际化匹配后的消息
大致上就是上面这两种场景。接下来松哥通过一个简单的用法来和大家演示下具体玩法。
首先我们在项目的 resources 目录下新建语言文件,language_en_US.properties 和 language_zh-CN.properties,如下图:
内容分别如下:
language_en_US.properties:
login.username=Usernamelogin.password=Password
language_zh-CN.properties:
login.username=用户名login.password=用户密码
这两个分别对应英中文环境。配置文件写好之后,还需要在 SpringMVC 容器中提供一个 ResourceBundleMessageSource 实例去加载这两个实例,如下:
<bean class="org.springframework.context.support.ResourceBundleMessageSource" id="messageSource"> <property name="basename" value="language"/> <property name="defaultEncoding" value="UTF-8"/></bean>
这里配置了文件名 language 和默认的编码格式。
接下来我们新建一个 login.jsp 文件,如下:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %><%@ page contentType="text/html;charset=UTF-8" language="java" %><html><head> <title>Title</title></head><body><spring:message code="login.username"/> <input type="text"> <br><spring:message code="login.password"/> <input type="text"> <br></body></html>
在这个文件中,我们通过 spring:message 标签来引用变量,该标签会根据当前的实际情况,选择合适的语言文件。
接下来我们为 login.jsp 提供一个控制器:
@Controllerpublic class LoginController { @Autowired MessageSource messageSource; @GetMapping("/login") public String login() { String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale()); String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale()); System.out.println("username = " + username); System.out.println("password = " + password); return "login"; }}
控制器中直接返回 login 视图即可。
另外我这还注入了 MessageSource 对象,主要是为了向大家展示如何在处理器中获取国际化后的语言文字。
配置完成后,启动项目进行测试。
默认情况下,系统是根据请求头的中 Accept-Language 字段来判断当前的语言环境的,该这个字段由浏览器自动发送,我们这里为了测试方便,可以使用 POSTMAN 进行测试,然后手动设置 Accept_Language 字段。
首先测试中文环境:
然后测试英文环境:
都没问题,完美!同时观察 IDEA 控制台,也能正确打印出语言文字。
上面这个是基于 AcceptHeaderLocaleResolver 来解析出当前的区域和语言的。
有的时候,我们希望语言环境直接通过请求参数来传递,而不是通过请求头来传递,这个需求我们通过 SessionLocaleResolver 或者 CookieLocaleResolver 都可以实现。
先来看 SessionLocaleResolver。
首先在 SpringMVC 配置文件中提供 SessionLocaleResolver 的实例,同时配置一个拦截器,如下:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> <property name="paramName" value="locale"/> </bean> </mvc:interceptor></mvc:interceptors><bean class="org.springframework.web.servlet.i18n.SessionLocaleResolver" id="localeResolver"></bean>
SessionLocaleResolver 是负责区域解析的,这个没啥好说的。拦截器 LocaleChangeInterceptor 则主要是负责参数解析的,我们在配置拦截器的时候,设置了参数名为 locale(默认即此),也就是说我们将来可以通过 locale 参数来传递当前的环境信息。
配置完成后,我们还是来访问刚才的 login 控制器,如下:
此时我们可以直接通过 locale 参数来控制当前的语言环境,这个 locale 参数就是在前面所配置的 LocaleChangeInterceptor 拦截器中被自动解析的。
如果你不想配置 LocaleChangeInterceptor 拦截器也是可以的,直接自己手动解析 locale 参数然后设置 locale 也行,像下面这样:
@Controllerpublic class LoginController { @Autowired MessageSource messageSource; @GetMapping("/login") public String login(String locale,HttpSession session) { if ("zh-CN".equals(locale)) { session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("zh", "CN")); } else if ("en-US".equals(locale)) { session.setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, new Locale("en", "US")); } String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale()); String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale()); System.out.println("username = " + username); System.out.println("password = " + password); return "login"; }}
SessionLocaleResolver 所实现的功能也可以通过 CookieLocaleResolver 来实现,不同的是前者将解析出来的区域信息保存在 session 中,而后者则保存在 Cookie 中。保存在 session 中,只要 session 没有发生变化,后续就不用再次传递区域语言参数了,保存在 Cookie 中,只要 Cookie 没变,后续也不用再次传递区域语言参数了。
使用 CookieLocaleResolver 的方式很简单,直接在 SpringMVC 中提供 CookieLocaleResolver 的实例即可,如下:
<bean class="org.springframework.web.servlet.i18n.CookieLocaleResolver" id="localeResolver"/>
注意这里也需要使用到 LocaleChangeInterceptor 拦截器,如果不使用该拦截器,则需要自己手动解析并配置语言环境,手动解析并配置的方式如下:
@GetMapping("/login3")public String login3(String locale, HttpServletRequest req, HttpServletResponse resp) { CookieLocaleResolver resolver = new CookieLocaleResolver(); if ("zh-CN".equals(locale)) { resolver.setLocale(req, resp, new Locale("zh", "CN")); } else if ("en-US".equals(locale)) { resolver.setLocale(req, resp, new Locale("en", "US")); } String username = messageSource.getMessage("login.username", null, LocaleContextHolder.getLocale()); String password = messageSource.getMessage("login.password", null, LocaleContextHolder.getLocale()); System.out.println("username = " + username); System.out.println("password = " + password); return "login";}
配置完成后,启动项目进行测试,这次测试的方式跟 SessionLocaleResolver 的测试方式一致,松哥就不再多说了。
除了前面介绍的这几种 LocaleResolver 之外,还有一个 FixedLocaleResolver,因为比较少见,松哥这里就不做过多介绍了。
2.Spring Boot 国际化配置2.1 基本使用
Spring Boot 和 Spring 一脉相承,对于国际化的支持,默认是通过 AcceptHeaderLocaleResolver 解析器来完成的,这个解析器,默认是通过请求头的 Accept-Language 字段来判断当前请求所属的环境的,进而给出合适的响应。
所以在 Spring Boot 中做国际化,这一块我们可以不用配置,直接就开搞。
首先创建一个普通的 Spring Boot 项目,添加 web 依赖即可。项目创建成功后,默认的国际化配置文件放在 resources 目录下,所以我们直接在该目录下创建四个测试文件,如下:
- 我们的 message 文件是直接创建在 resources 目录下的,IDEA 在展示的时候,会多出一个 Resource Bundle,这个大家不用管,千万别手动去创建这个目录。
- messages.properties 这个是默认的配置,其他的则是不同语言环境下的配置,en_US 是英语(美国),zh_CN 是中文简体,zh_TW 是中文繁体(文末附录里边有一个完整的语言简称表格)。
四个文件创建好之后,第一个默认的我们可以先空着,另外三个分别填入以下内容:
messages_zh_CN.properties
user.name=江南一点雨
messages_zh_TW.properties
user.name=江南壹點雨
messages_en_US.properties
user.name=javaboy
配置完成后,我们就可以直接开始使用了。在需要使用值的地方,直接注入 MessageSource 实例即可。
❝
在 Spring 中需要配置的 MessageSource 现在不用配置了,Spring Boot 会通过 org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration 自动帮我们配置一个 MessageSource 实例。
创建一个 HelloController ,内容如下:
@RestControllerpublic class HelloController { @Autowired MessageSource messageSource; @GetMapping("/hello") public String hello() { return messageSource.getMessage("user.name", null, LocaleContextHolder.getLocale()); }}
在 HelloController 中我们可以直接注入 MessageSource 实例,然后调用该实例中的 getMessage 方法去获取变量的值,第一个参数是要获取变量的 key,第二个参数是如果 value 中有占位符,可以从这里传递参数进去,第三个参数传递一个 Locale 实例即可,这相当于当前的语言环境。
接下来我们就可以直接去调用这个接口了。
默认情况下,在接口调用时,通过请求头的 Accept-Language 来配置当前的环境,我这里通过 POSTMAN 来进行测试,结果如下:
小伙伴们看到,我在请求头中设置了 Accept-Language 为 zh-CN,所以拿到的就是简体中文;如果我设置了 zh-TW,就会拿到繁体中文:
是不是很 Easy?
2.2 自定义切换
有的小伙伴觉得切换参数放在请求头里边好像不太方便,那么也可以自定义解析方式。例如参数可以当成普通参数放在地址栏上,通过如下配置可以实现我们的需求。
@Configurationpublic class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); interceptor.setParamName("lang"); registry.addInterceptor(interceptor); } @Bean LocaleResolver localeResolver() { SessionLocaleResolver localeResolver = new SessionLocaleResolver(); localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); return localeResolver; }}
在这段配置中,我们首先提供了一个 SessionLocaleResolver 实例,这个实例会替换掉默认的 AcceptHeaderLocaleResolver,不同于 AcceptHeaderLocaleResolver 通过请求头来判断当前的环境信息,SessionLocaleResolver 将客户端的 Locale 保存到 HttpSession 对象中,并且可以进行修改(这意味着当前环境信息,前端给浏览器发送一次即可记住,只要 session 有效,浏览器就不必再次告诉服务端当前的环境信息)。
另外我们还配置了一个拦截器,这个拦截器会拦截请求中 key 为 lang 的参数(不配置的话是 locale),这个参数则指定了当前的环境信息。
好了,配置完成后,启动项目,访问方式如下:
我们通过在请求中添加 lang 来指定当前环境信息。这个指定只需要一次即可,也就是说,在 session 不变的情况下,下次请求可以不必带上 lang 参数,服务端已经知道当前的环境信息了。
CookieLocaleResolver 也是类似用法,不再赘述。
2.3 其他自定义
默认情况下,我们的配置文件放在 resources 目录下,如果大家想自定义,也是可以的,例如定义在 resources/i18n 目录下:
但是这种定义方式系统就不知道去哪里加载配置文件了,此时还需要 application.properties 中进行额外配置(注意这是一个相对路径):
spring.messages.basename=i18n/messages
另外还有一些编码格式的配置等,内容如下:
spring.messages.cache-duration=3600spring.messages.encoding=UTF-8spring.messages.fallback-to-system-locale=true
spring.messages.cache-duration 表示 messages 文件的缓存失效时间,如果不配置则缓存一直有效。
spring.messages.fallback-to-system-locale 属性则略显神奇,网上竟然看不到一个明确的答案,后来翻了一会源码才看出端倪。
这个属性的作用在 org.springframework.context.support.AbstractResourceBasedMessageSource#getDefaultLocale 方法中生效:
protected Locale getDefaultLocale() { if (this.defaultLocale != null) { return this.defaultLocale; } if (this.fallbackToSystemLocale) { return Locale.getDefault(); } return null;}
从这段代码可以看出,在找不到当前系统对应的资源文件时,如果该属性为 true,则会默认查找当前系统对应的资源文件,否则就返回 null,返回 null 之后,最终又会调用到系统默认的 messages.properties 文件。
3.LocaleResolver
国际化这块主要涉及到的组件是 LocaleResolver,这是一个开放的接口,官方默认提供了四个实现。当前该使用什么环境,主要是通过 LocaleResolver 来进行解析的。
LocaleResolver
public interface LocaleResolver { Locale resolveLocale(HttpServletRequest request); void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);}
这里两个方法:
- resolveLocale:根据当前请求解析器出 Locale 对象。
- 设置 Locale 对象。
我们来看看 LocaleResolver 的继承关系:
虽然中间有几个抽象类,不过最终负责实现的其实就四个:
- AcceptHeaderLocaleResolver:根据请求头中的 Accept-Language 字段来确定当前的区域语言等。
- SessionLocaleResolver:根据请求参数来确定区域语言等,确定后会保存在 Session 中,只要 Session 不变,Locale 对象就一直有效。
- CookieLocaleResolver:根据请求参数来确定区域语言等,确定后会保存在 Cookie 中,只要 Session 不变,Locale 对象就一直有效。
- FixedLocaleResolver:配置时直接提供一个 Locale 对象,以后不能修改。
接下来我们就对这几个类逐一进行分析。
3.1 AcceptHeaderLocaleResolver
AcceptHeaderLocaleResolver 直接实现了 LocaleResolver 接口,我们来看它的 resolveLocale 方法:
@Overridepublic Locale resolveLocale(HttpServletRequest request) { Locale defaultLocale = getDefaultLocale(); if (defaultLocale != null && request.getHeader("Accept-Language") == null) { return defaultLocale; } Locale requestLocale = request.getLocale(); List<Locale> supportedLocales = getSupportedLocales(); if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) { return requestLocale; } Locale supportedLocale = findSupportedLocale(request, supportedLocales); if (supportedLocale != null) { return supportedLocale; } return (defaultLocale != null ? defaultLocale : requestLocale);}
- 首先去获取默认的 Locale 对象。
- 如果存在默认的 Locale 对象,并且请求头中没有设置 Accept-Language 字段,则直接返回默认的 Locale。
- 从 request 中取出当前的 Locale 对象,然后查询出支持的 supportedLocales,如果 supportedLocales 或者 supportedLocales 中包含 requestLocale,则直接返回 requestLocale。
- 如果前面还是没有匹配成功的,则从 request 中取出 locales 集合,然后再去和支持的 locale 进行比对,选择匹配成功的 locale 返回。
- 如果前面都没能返回,则判断 defaultLocale 是否为空,如果不为空,就返回 defaultLocale,否则返回 defaultLocale。
再来看看它的 setLocale 方法,直接抛出异常,意味着通过请求头处理 Locale 是不允许修改的。
@Overridepublic void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) { throw new UnsupportedOperationException( "Cannot change HTTP accept header - use a different locale resolution strategy");}3.2 SessionLocaleResolver
SessionLocaleResolver 的实现多了一个抽象类 AbstractLocaleContextResolver,AbstractLocaleContextResolver 中增加了对 TimeZone 的支持,我们先来看下 AbstractLocaleContextResolver:
public abstract class AbstractLocaleContextResolver extends AbstractLocaleResolver implements LocaleContextResolver { @Nullable private TimeZone defaultTimeZone; public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) { this.defaultTimeZone = defaultTimeZone; } @Nullable public TimeZone getDefaultTimeZone() { return this.defaultTimeZone; } @Override public Locale resolveLocale(HttpServletRequest request) { Locale locale = resolveLocaleContext(request).getLocale(); return (locale != null ? locale : request.getLocale()); } @Override public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) { setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null)); }}
可以看到,多了一个 TimeZone 属性。从请求中解析出 Locale 还是调用了 resolveLocaleContext 方法,该方法在子类中被实现,另外调用 setLocaleContext 方法设置 Locale,该方法的实现也在子类中。
我们来看下它的子类 SessionLocaleResolver:
@Overridepublic Locale resolveLocale(HttpServletRequest request) { Locale locale = (Locale) WebUtils.getSessionAttribute(request, this.localeAttributeName); if (locale == null) { locale = determineDefaultLocale(request); } return locale;}
直接从 Session 中获取 Locale,默认的属性名是 SessionLocaleResolver.class.getName() + ".LOCALE",如果 session 中不存在 Locale 信息,则调用 determineDefaultLocale 方法去加载 Locale,该方法会首先找到 defaultLocale,如果 defaultLocale 不为 null 就直接返回,否则就从 request 中获取 Locale 返回。
再来看 setLocaleContext 方法,就是将解析出来的 Locale 保存起来。
@Overridepublic void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable LocaleContext localeContext) { Locale locale = null; TimeZone timeZone = null; if (localeContext != null) { locale = localeContext.getLocale(); if (localeContext instanceof TimeZoneAwareLocaleContext) { timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); } } WebUtils.setSessionAttribute(request, this.localeAttributeName, locale); WebUtils.setSessionAttribute(request, this.timeZoneAttributeName, timeZone);}
保存到 Session 中即可。大家可以看到,这种保存方式其实和我们前面演示的自己保存代码基本一致,殊途同归。
3.3 FixedLocaleResolver
FixedLocaleResolver 有三个构造方法,无论调用哪一个,都会配置默认的 Locale:
public FixedLocaleResolver() { setDefaultLocale(Locale.getDefault());}public FixedLocaleResolver(Locale locale) { setDefaultLocale(locale);}public FixedLocaleResolver(Locale locale, TimeZone timeZone) { setDefaultLocale(locale); setDefaultTimeZone(timeZone);}
要么自己传 Locale 进来,要么调用 Locale.getDefault() 方法获取默认的 Locale。
再来看 resolveLocale 方法:
@Overridepublic Locale resolveLocale(HttpServletRequest request) { Locale locale = getDefaultLocale(); if (locale == null) { locale = Locale.getDefault(); } return locale;}
这个应该就不用解释了吧。
需要注意的是它的 setLocaleContext 方法,直接抛异常出来,也就意味着 Locale 在后期不能被修改。
@Overridepublic void setLocaleContext( HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable LocaleContext localeContext) { throw new UnsupportedOperationException("Cannot change fixed locale - use a different locale resolution strategy");}3.4 CookieLocaleResolver
CookieLocaleResolver 和 SessionLocaleResolver 比较类似,只不过存储介质变成了 Cookie,其他都差不多,松哥就不再重复介绍了。
4.附录
搜刮了一个语言简称表,分享给各位小伙伴:
语言 简称 简体中文(中国) zh_CN 繁体中文(中国台湾) zh_TW 繁体中文(中国香港) zh_HK 英语(中国香港) en_HK 英语(美国) en_US 英语(英国) en_GB 英语(全球) en_WW 英语(加拿大) en_CA 英语(澳大利亚) en_AU 英语(爱尔兰) en_IE 英语(芬兰) en_FI 芬兰语(芬兰) fi_FI 英语(丹麦) en_DK 丹麦语(丹麦) da_DK 英语(以色列) en_IL 希伯来语(以色列) he_IL 英语(南非) en_ZA 英语(印度) en_IN 英语(挪威) en_NO 英语(新加坡) en_SG 英语(新西兰) en_NZ 英语(印度尼西亚) en_ID 英语(菲律宾) en_PH 英语(泰国) en_TH 英语(马来西亚) en_MY 英语(阿拉伯) en_XA 韩文(韩国) ko_KR 日语(日本) ja_JP 荷兰语(荷兰) nl_NL 荷兰语(比利时) nl_BE 葡萄牙语(葡萄牙) pt_PT 葡萄牙语(巴西) pt_BR 法语(法国) fr_FR 法语(卢森堡) fr_LU 法语(瑞士) fr_CH 法语(比利时) fr_BE 法语(加拿大) fr_CA 西班牙语(拉丁美洲) es_LA 西班牙语(西班牙) es_ES 西班牙语(阿根廷) es_AR 西班牙语(美国) es_US 西班牙语(墨西哥) es_MX 西班牙语(哥伦比亚) es_CO 西班牙语(波多黎各) es_PR 德语(德国) de_DE 德语(奥地利) de_AT 德语(瑞士) de_CH 俄语(俄罗斯) ru_RU 意大利语(意大利) it_IT 希腊语(希腊) el_GR 挪威语(挪威) no_NO 匈牙利语(匈牙利) hu_HU 土耳其语(土耳其) tr_TR 捷克语(捷克共和国) cs_CZ 斯洛文尼亚语 sl_SL 波兰语(波兰) pl_PL 瑞典语(瑞典) sv_SE 西班牙语(智利) es_CL
5.小结
好啦,今天主要和小伙伴们聊了下 SpringMVC 中的国际化问题,以及 LocaleResolver 相关的源码,相信大家对 SpringMVC 的理解应该又更近一步了吧。
评论