1、什么是本地解析器?
Java中有一个表示本地化的一个类java.util.Locale 这个类就是用来表示当前程序的执行所在地,这个类主要描述的是本地的语言、国际信息,比如中国对应的Locale实例就是 语言lang=zh , country=CN 这样一个Locale实例就表示中国本地。
在程序中如何动态感知到当前程序的执行所在地信息是啥呢?这个时候就需要我们使用一个参数来进行动态获取,这就是SpringMVC中LocaleResolver的主要作用,也就是提供了国际化的一个支撑。
2、SpringMVC中LocaleResolver接口定义
public interface LocaleResolver {
/**
* Resolve the current locale via the given request.
* Can return a default locale as fallback in any case.
* @param request the request to resolve the locale for
* @return the current locale (never {@code null})
*/
根据当前请求解析当前请求的本地化信息
Locale resolveLocale(HttpServletRequest request);
/**
* Set the current locale to the given one.
* @param request the request to be used for locale modification
* @param response the response to be used for locale modification
* @param locale the new locale, or {@code null} to clear the locale
* @throws UnsupportedOperationException if the LocaleResolver
* implementation does not support dynamic changing of the locale
*/
设置当前请求、响应的本地化信息,
void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}
3、LocaleResolver接类图
主要的实现有4个:
3.1、AcceptHeaderLocalResovler:通过请求头里面的Accept-Language:zh,en;q=0.9,zh-HK;q=0.8来决定使用具体的Locale实例,其实AcceptHeaderLocalResovler就是使用HttpServletRequest实例中的Locale实例,在进入DispatcherServlet的时候HttpServletRequest实例里面就已经有Locale实例了,可以通过request.getLocale();来获取Locale实例,HttpServletRequest里面的Locale实例就是使用请求头里面的Accept-Language:zh,en;q=0.9,zh-HK;q=0.8来构建的,比如请求头里面的Accept-Language:zh,en;q=0.9,zh-HK;q=0.8 就表示zh的权重是1,en;q=0.9表示en的权重是0.9,zh-HK;q=0.8就表示zh-HK的权重是0.8,那么我么通过request.getLocale();获取到的就是权重最高的zh,然后就是构建一个zh的Locale实例,那么AcceptHeaderLocalResovler在解析Locale的时候就会返回zh的Locale实例。
3.2、CookieLocaleResovler:根据用户在Cookie中设置的某参数来进行确定具体的本地化Locale实例。
3.3、SessionLocaleResovler:根据用户在HttpSession中设置某参数来进行确定具体的本地化Locale实例。
3.4、FixedLocalResovler:使用jdk自带的默认的Locale实例。
4、常用的LocaleResovler的配置方式
4.1、在我们没有配置LocaleResovler的时候SpringMVC会默认为我们配置一个AcceptHeaderLocalResovler实例,还记得之前讲解的九大逐渐初始化的时候说过在DispatcherServlet.properties中SpringMVC为我们提供默认配置的时候。
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
4.2、显示的配置CookieLocaleResovler,当我们配置了自己的LocaleResovler,那么就不会采用默认的,而是使用我们显示配置的LocaleResovler:使用场景就是比如登录完成后将本地化的参数("zh")设置到Cookie中,后续的请求就能携带本地化参数了
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<!--配置指定本地化参数在cookie中的cookieName 名称-->
<property name="cookieName" value="locale"/>
<!--当前设置的name=locale 在浏览器端的最大存活时间-->
<property name="cookieMaxAge" value="100000"/>
<!--当前cookie 在那些请求路径下可见-->
<property name="cookiePath" value="/"/>
</bean>
4.3、显示的配置SessionLocaleResovler,注意HttpSession中存放的不是"zh" 等字符,而是一个Locale实例。
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<!--配置在HttpSession中 本地化参数的key,在SessionLocaleResolver解析的是会使用这个key去HttpSession中获取本地化Locale实例-->
<property name="localeAttributeName" value="locale"/>
</bean>
4.4、显示的配置FixedLocalResovler
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.FixedLocaleResolver"/>
4.5、注意以上配置的spring bean id 一定要等于"localeResovler"。
5、如何使用LocaleResovler
5.1、通用代码:
@RequestMapping("messageSource")
private ModelAndView messageSource(Locale locale){
入参是一个Locale实例,我们使用不同的LocaleResovler,然后来看看这个Locale会有什么变化,
我们在使用一个消息源messageSource通过locale去不同的本地消息文件中获取key=name的值。
String name = messageSource.getMessage("name", null, locale);
ModelAndView modelAndView = new ModelAndView();
//注册成功返回到注册成功的页面(success.jsp进行翻译来的页面)
modelAndView.setViewName("success");
modelAndView.addObject("name", name);
return modelAndView;
}
在messages_zh_CN.properties本地消息文件中配置:name=\u6e29\u5b97\u6e90 注意由于是中文,需要转为ASCAll码
在messages_en_US.properties本地消息文件中配置:name=wenzongyuan
配置一个消息源messageSource:对于这个消息资源绑定不明白的可以看我之前写的博客
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basenames">
<list>
<value>i18/messages</value>
</list>
</property>
</bean>
写一个简单的jsp用来展示结果:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
<title>新增用户</title>
</head>
<body>
<h1>${name} 注册成功!</h1>
</body>
</html>
5.2、测试AcceptHeaderLocalResovler
返回值是中文中配置的name属性
5.3、测试CookieLocaleResovler
添加配置CookieLocaleResovler
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<!--配置指定本地化参数在cookie中的cookieName 名称-->
<property name="cookieName" value="locale"/>
<!--当前设置的name=locale 在浏览器端的最大存活时间-->
<property name="cookieMaxAge" value="100000"/>
<!--当前cookie 在那些请求路径下可见-->
<property name="cookiePath" value="/"/>
</bean>
为力方便测试我们使用postman,可以手动添加cookie:
结果显示拿到了美国的本地化消息文件中的name属性。
5.4、测试SessionLocaleResovler
1、添加SessionLocaleResovler配置:
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<!--配置在HttpSession中 本地化参数的key,在SessionLocaleResolver解析的是会使用这个key去HttpSession中获取本地化Locale实例-->
<property name="localeAttributeName" value="locale"/>
</bean>
2、写一个方法用于向HttpSession中添加Locale实例:
@RequestMapping("setSessionLocale")
private String setSessionLocale(String mark,HttpSession httpSession){
//我们定义一个mark标记,如果等于1 我们就往HttpSession设置 Locale.CHINA实例,否者设置为 Locale.US实例
if (StringUtils.isEmpty(mark) || "1".equals(mark)){
httpSession.setAttribute("locale", Locale.CHINA);
}else {
httpSession.setAttribute("locale", Locale.US);
}
return "register";
}
3、 开始测试,我们访问http://localhost:7070/test/setSessionLocale?mark=1 测试Locale.CHINA
接着访问http://localhost:7070/test/messageSource
4、测试,我们访问http://localhost:7070/test/setSessionLocale?mark=2 测试Locale.US
接着访问http://localhost:7070/test/messageSource
5.5、FixedLocaleResolver我们不在测试,读者自行测试。
6、LocaleChangeInterceptor -->本地化信息修改拦截器
6.1、有的时候我们可能会根据请求中的某参数来设置当前的本地化信息,这个时候我们上面说的几种LocaleResovler都是无法满足我们的需求的,这个时候LocaleChangeInterceptor就登场了。
6.2、使用LocaleChangeInterceptor:
①、配置LocaleChangeInterceptor拦截器:
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor" p:paramName="locale"/>
</mvc:interceptors>
需要配置LocaleChangeInterceptor实例的paramName属性,表示使用请求中的那个参数来进行本地化信息确定,
不配置默认是参数名称是locale。
②、进行测试浏览器中输入http://localhost:7070/test/messageSource?locale=zh_CN
我们发现会报错,因为我们此时配置的LocaleReslover是AcceptHeaderLocaleResolver类型,AcceptHeaderLocaleResolver是不允许设置本地化信息的,源码体现如下:
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
throw new UnsupportedOperationException(
"Cannot change HTTP accept header - use a different locale resolution strategy");
}
既然AcceptHeaderLocaleResolver不然设置,那么我们就换一个LocaleReslover,我们换成CookieLocaleResolver试试,在此输入http://localhost:7070/test/messageSource?locale=en_US,结果如下:
我们再次输入http://localhost:7070/test/messageSource?locale=zh_CN
从此我们可以看出来LocaleChangeInterceptor跟CookieLocaleResolver可以配套使用。
LocaleChangeInterceptor跟SessionLocaleResovler也是可以配套使用的。
LocaleChangeInterceptor跟FixedLocaleResolver也是不能配套使用的。
7、LocaleResolver接口在整个请求中的工作原理分析
7.1、在分析RequestMappingHandlerAdapter的原理的时候,我们着重分析了参数解析器HandlerMethodArgumentResolver我们提到过支撑Locale类型入参的参数解析器就是ServletRequestMethodArgumentResolver参数解析器,我们来查看他的支撑入参类型的方法是实现:
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
"java.time.ZoneId".equals(paramType.getName()));
}
接下来我们查看ServletRequestMethodArgumentResolver的入参解析方法:我们只看如果入参类型是Locale的实现片段:
else if (Locale.class == paramType) {
return RequestContextUtils.getLocale(request);
}
重点方法是RequestContextUtils.getLocale(request)其实现如下:
public static Locale getLocale(HttpServletRequest request) {
使用请求实例获取一个本地信息解析器localeResolver
LocaleResolver localeResolver = getLocaleResolver(request);
然后使用解析器根据请求实例解析一个本地信息Locale实例返回,
如果没有解析到那就使用请求里面的Locale实例返回。
return (localeResolver != null ? localeResolver.resolveLocale(request) : request.getLocale());
}
获取本地信息解析器的实现:
public static LocaleResolver getLocaleResolver(HttpServletRequest request) {
实现方式就是从请求实例里面根据key获取到,那么问题来了,什么时候设置的呢???
return (LocaleResolver)
request.getAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE);
}
源码里面抛出个问题就是:我们配置的localeResolver是什么时候设置到请求实例里面的呢????
纵观源码我们找到了在DispatcherServlet中确实设置了,源码体现如下:
DispatcherServlet的doService方法中:
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
这里就是将我们配置的localeResolver设置到请求实例中。
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
try {
doDispatch(request, response);
}
8、LocaleChangeInterceptor原理分析
8.1、LocaleChangeInterceptor是一个标注的SpringMVC的拦截器,其实现了HandlerInterceptorAdapter抽象类,复写了拦截器的前置方法preHandle,这个方法里面使用配置的请求入参进行了本地信息实例的构建,然后进行设置。
8.2、LocaleChangeInterceptor拦截器的前置方法实现:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
1、获取请求中配置的本地化信息确认的入参。
String newLocale = request.getParameter(getParamName());
if (newLocale != null) {
2、判断当前的请求方式是否是当前拦截器支持的请求方式,这个是可配置的。
if (checkHttpMethod(request.getMethod())) {
3、看见没还是一样的,使用RequestContextUtils获取到本地信息解析器。
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException(
"No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
4、使用入参去构建一个Locale实例,然后设置到本地信息解
析器中,设置以后,当再次使用localeResolver去解析
本地信息实例的时候,就能拿到当前的设置的本地信息实
例了。
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
}
catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
}
else {
throw ex;
}
}
}
}
// Proceed in any case.
return true;
}
以上就是SpringMVC的本地化信息处理的工作原理,在我们的应用需要支持国际化的时候,这个就是我们主要使用的一个方式。