Spring MVC更多家族成员--国际化视图与LocalResolver---10
- 引言
- 可用的LocaleResolver
- LocaleResolver的足迹
- LocaleResolver在初始化流程中的使用
- processRequest处理请求的核心方法
- DispatcherServlet的doservice方法
- 小结
- LocaleResolver后继使用时机
- 体会
- LocaleResolver在初始化流程中的使用
- Locale的变更与LocaleChangeHandler
引言
网络拉近了人与人之间的距离。即使相距千里,人们也可以通过网络互相了解对方的信息和文化。但是,不管怎么说,在“地球村”没有统一的“官方语言”之前,不同地区的不同语言依然是人们能够互相交流的一道障碍。所以,现在的Web应用程序尤其是企业级的应用,都会提供国际化的信息支持,以便可以根据访问者的Locale信息为他们提供相应语言的信息内容。为用户提供国际化视图支持自然成为Spring MVC框架不可或缺的一部分。
在ViewResolver根据逻辑视图名解析视图的时候,ViewResolver的resolveviewName(viewName,locale)方法除了接受要解析的逻辑视图名作为参数之外,还同时接受一个Locale类型对象。这样,ViewResolver就可以根据Locale的不同而返回针对不同Locale的视图实例。
到此为止,好像没有必要再往下看了,ViewResolver的设计已经足以完成国际化视图支持的使命了,不是吗?
难道ResourceBundleviewResolver不就是很好的例证吗?
实际上,从ViewResolver这个层次上来讲,情况确实如此。
但是,我们可曾想过,ViewResolver所接受的Locale实例是从何而来的呢?如何获取用户所对应的Localel呢?
只有揭开这一谜团,才能将Spring MVC框架内对国际化视图的支持讲述完整。
可以有多种方式获取用户通过浏览器提交的Web请求所对应的Locale值,比如,根据HTTP的Accept-Language协议头进行解析,或者读取用户浏览器端存储的相应Cookie值等。鉴于有如此多不同的处理方式,Spring MVC使LocaleResolver接口定义对各种可能的Locale值的获取/解析方式进行统一的策略抽象。
该接口定义如下:
代码语言:javascript复制public interface LocaleResolver {
Locale resolveLocale(HttpServletRequest request);
void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);
}
作为策略接口,LocaleResolver.主要完成两个工作:
- 第一,由resolveLocale(request)方法负责根据当前Locale解析策略获取当前请求对应的Locale值
- 第二,如果当前策略支持Locale的更改,那么可以通过setLocale方法对当前策略默认取得的Locale值进行变更。
可用的LocaleResolver
根据通常的Locale获取策略,Spring MVC为LocaleResolver提供了相应的可用实现类,如下所述。
-
FixedLocaleResolver
。最简单的LocaleResolver实现类。一旦指定给FixedLocaleResolver一个Locale值,FixedLocaleResolver将一直持有并返回这个Locale保持不变。因为该FixedLocaleResolver的策略是保持一个Locale值不变,所以不能通过setLocale更改FixedLocaleResolver默认返回的Locale值。 -
AcceptHeaderLocaleResolver
。用户通过客户端浏览器提交Web请求之后,HTTP的Accept-Language协议头(HTTP Header)将随同Web请求一同发送给服务器端进行处理。AcceptHeaderLocaleResolver的策略就是根据Accept-Language协议头来解析并返回当前Web请求对应的Locale值。既然我们无法更改Accept-Language协议头,那么AcceptHeaderLocaleResolver与FixedLocaleResolver一样无法更改默认策略返回的Locale值。 -
SessionLocaleResolver
。SessionLocaleResolver将根据指定键值从Session中获取相应的Locale。初始的时候,我们可以为其指定一个默认的Locale值。如果SessionLocaleResolver既无法从Session获取可用的Locale值,又没有初始化的默认Locale,那么它将采用AcceptHeaderLocaleResolver的策略获取Web请求对应的Locale值。因为SessionLocaleResolver是以Session进行Locale管理,所以我们可以对SessionLocaleResolver默认所返回的Locale值进行变更。 -
CookieLocaleResolver
。如果客户端浏览器没有禁止使用Cookie的话,我们也可以使用 Cookie来管理Locale信息。CookieLocaleResolver通过读取客户端的指定Cookie获取相应的 Locale值,当然,我们在初始之初就可以为CookieLocaleResolver指定一个默认返回的Locale值。当CookieLocaleResolver无法从客户端的Cookie获取相应的Locale的时候,它可以转而 返回这个初始化时候指定的默认Locale值。如果以上尝试均告失败,那么CookieLocaleResolver也就不得不与SessionLocaleResolver那样,转而使用AcceptHeaderLocaleResolver的策略来获取Locale值了。只要客户端浏览器不禁止Cookie的使用,我们就可以对Cookie中的数据进行更新。所以CookieLocaleResolver支持通过setLocale方法更改默 认返回的Locale值。
以上实现类全部位于org.springframework.web.servlet.i18n
包中。我们可以根据当前Web应用程序的需要选择使用其中任何一个,有关各个实现类的API细节,不妨参照相应类的Javadoc。
LocaleResolver的足迹
要在Spring MVC应用中使用相应的LocaleResolver对Locale进行解析和设置,只需要将相 应实现类添加到DispatherServlet的webApplicationContext中。在合适的时机,该LocaleResolver将被使用。我们以SessionLocaleResolver为例,给出对应的配置内容如下:
代码语言:javascript复制 <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<property name="defaultLocale" value="zh_CN"/>
</bean>
在把要使用的LocaleResolver实现类添加到容器的过程中需要注意,名称“localeResolver” 是必须的。因为DispatcherServlet在初始化的时候,将按照该指定名称到webApplicationContext 中去查找可用的LocaleResolver实例。如果找不到,DispatcherServlet将使用默认的LocaleResolver。
代码语言:javascript复制 private void initLocaleResolver(ApplicationContext context) {
try {
this.localeResolver = context.getBean(LOCALE_RESOLVER_BEAN_NAME, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Using LocaleResolver [" this.localeResolver "]");
}
}
catch (NoSuchBeanDefinitionException ex) {
// We need to use the default.
this.localeResolver = getDefaultStrategy(context, LocaleResolver.class);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate LocaleResolver with name '" LOCALE_RESOLVER_BEAN_NAME
"': using default [" this.localeResolver "]");
}
}
}
如果只使用LocaleResolver,那么将相应实现类添加到webApplicationContext之后我们 就可以收手了。不过,要是想进一步了解添加到webApplicationContext的LocaleResolver实 例都可以在Web请求处理过程中的哪些时间点发挥作用,LocaleResolver走过的几个点还是需要知道一下的。
LocaleResolver在初始化流程中的使用
可以看到DispatcherServlet的继承体系,并且在请求到来的时候,最终会调用到Servlet的service方法,因此我们先从service方法开始追溯:
- 由于FrameworkServlet覆写了父类service方法的实现,因此请求到来的时候,首先调用到的是FrameworkServlet覆写的service方法实现
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
//当请求放为PATCH或者请求方法为空的时候,调用processRequest处理请求--该方法是DispathcerServlet处理请求的核心方法
if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
processRequest(request, response);
}
else {
//调用父类service方法实现--这里调用到的是HttpServlet的service方法实现
super.service(request, response);
}
}
- HttpServlet#对service方法首次给出了实现
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
...
doGet(req, resp);
...
} else if (method.equals(METHOD_HEAD)) {
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//如果当前请求方法是不被支持的,那么就响应对应的错误
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
HttpServlet相关doGet,doPost等方法的实现,这里最终都调用的是FrameworkServlet的实现:
而FrameworkServlet对于这些doGet,doPost方法的实现,最终都是调用的processRequest方法的实现。
processRequest方法是我们关注的核心,该方法中完成了对LocaleContext的绑定工作:
processRequest处理请求的核心方法
代码语言:javascript复制 protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
//尝试从LocaleContextHolder中取出当前线程先前绑定的LocaleContext---》ThreadLocal
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
//为当前请求构建一个LocaleContext---buildLocaleContext调用的是DispathcerServlet覆写的方法
//利用LocaleContextResolver解析当前request得到一个LocaleContext实现
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
//将上面得到的localeContext绑定到LocaleContextHolder上
initContextHolders(request, localeContext, requestAttributes);
try {
//调用DispathcerServlet的doService方法
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
//将相关contextHolder还原到初始模样---为啥要取出previousLocaleContext,就是为了当前请求处理完毕后,再还原会去
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
- buildLocaleContext
protected LocaleContext buildLocaleContext(final HttpServletRequest request) {
LocaleResolver lr = this.localeResolver;
if (lr instanceof LocaleContextResolver) {
return ((LocaleContextResolver) lr).resolveLocaleContext(request);
}
else {
return () -> (lr != null ? lr.resolveLocale(request) : request.getLocale());
}
}
调用DispathcerServlet内部的LocaleContextResolver对当前request进行解析
- initContextHolders
private void initContextHolders(HttpServletRequest request,
@Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
if (localeContext != null) {
LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
}
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
}
}
将上面得到的localeContext绑定到LocaleContextHolder上
- resetContextHolders
private void resetContextHolders(HttpServletRequest request,
@Nullable LocaleContext prevLocaleContext, @Nullable RequestAttributes previousAttributes) {
LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable);
RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable);
}
还原相关ContextHolder的状态
DispatcherServlet的doservice方法
代码语言:javascript复制@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
...
// Make framework objects available to handlers and view objects.
//在这里将很多工具都放在了request对象的属性集合中,相当于暴露给了handler和view视图对象使用
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
...
doDispatch(request, response);
...
}
小结
在DispatcherServlet要处理接收到Web请求之前,它会将其在初始化的时候获取的LocaleResolver实例,以及通过该LocaleResolver解析后的Locale值,以LocaleContext的形式绑定到当前线程。这样,在需要的时候,后继处理流程就可以通过绑定的LocaleContext获得当前Locale值以及使用的LocaleResolver。
LocaleResolver后继使用时机
流程按照我们之前所描述的顺序执行,在ViewResolver行动之前,DispatcherServlet将使用初始化、
ViewResolver从“宏观上”解决了对应不同Locale的视图选取问题。
不过,如果我们要在具体的视图内访问Locale信息该怎么办?
原则上来说,我们是通过RequestContext访问必要的国际化信息,包括当前请求对应的 Locale、应用使用的LocaleResolver,以及MessageSource中的国际化信息。
如果使用 JSP作为视图技术,那么直接使用Spring MVC提供的自定义Tag就可以,比如<spring:messagecode=". .”/>,因为它底层就是通过RequestContext完成相应功能的。但是, 如果使用velocity/Freemarker之类的视图技术,我们就不得不直接使用RequestContext来完成这些相关信息的访问了。而通过设置AbstractView的requestContextAttribute属性可以让我们在这些视图中获取到RequestContext的支持。
最后一步是清理的工作,DispatcherServlet将恢复以LocaleContext形式绑定到当前线程 的Locale相关信息。
现在,跟着LocaleResolver的足迹走过一遍之后,各位是否已经理解了LocaleResolver的存在价值了呢?
体会
当我们需要在一个工作处理流中任意节点都可以获取某个模型对象,那么有下面两种方法:
- 将模型对象放入ThreadLocal中,与当前线程绑定。
- 如果在当前工作流中存在某个对象的生命周期与当前工作流一致,例如: Web请求处理流程中的Request对象,那么我们可以将模型对象与当前Request对象相绑定。
并且通常会将整个工作流中需要用的模型对象,都交给一个Context上下文对象保存,对应上下文对象的生命周期和对应的工作流一致,例如: 会将请求处理工作流中需要用的对象都放入RequestContext中。
Locale的变更与LocaleChangeHandler
当访问各种支持国际化信息页面的网站的时候,即使本地的默认Locale使得服务器返回的是英文的信息页面,我们依然可以点击页面中的相应链接更改这一结果,比如,点击“中文版”切换到中文信息页面。在基于Spring MVC的Web应用中,我们要如何实现这一功能呢?
我们已经介绍了4种LocaleResolver的策略实现,为FixedLocaleResolver,AcceptHeaderLocaleResolver,SessionLocaleResolver和CookieLocaleResolver。前两种实现显然不支持Locale的变更,所以,如果要实现根据用户选择来切换Locale这样的功能需求,我们只能选择SessionLocaleResolver或者CookieLocaleResolve。在这样的前提下,我们再寻求下一步的解决 方案。
国际化信息页面的选择是由ViewResolver所接受的Locale决定的。要让用户能够变更到其他语言内容的信息页面,我们只要根据用户提交的请求内容变更Locale值即可。
在介绍HandlerInterceptor的时候,我们提到LocaleChangeInterceptor,而这里就是它的“用武之地”了。
LocaleChangeInterceptor的工作原理十分简单,它根据某一个请求参数获取要切换到的Locale 信息(该参数默认名称为“locale”),然后通过相应LocaleResolver实现类的setLocale方法, 使用新获取的Locale信息替换掉所使用的LocaleResolver默认策略所返回的Locale值。
要根据用户请求进行面向不同Locale的视图切换,我们只要配置一个LocaleChangeInterceptor对用户请求进行拦 截即可,该拦截器的核心preHandle方法如下:
代码语言:javascript复制 @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
//getParamName()返回值默认为locale
String newLocale = request.getParameter(getParamName());
if (newLocale != null) {
//只有指定的HttpMethod才能被允许切换locale
if (checkHttpMethod(request.getMethod())) {
LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException(
"No LocaleResolver found: not in a DispatcherServlet request?");
}
try {
localeResolver.setLocale(request, response, parseLocaleValue(newLocale));
}
catch (IllegalArgumentException ex) {
if (isIgnoreInvalidLocale()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring invalid locale value [" newLocale "]: " ex.getMessage());
}
}
else {
throw ex;
}
}
}
}
// Proceed in any case.
return true;
}
如果要使用该拦截器,那么具体配置如下:
代码语言:javascript复制 <bean id="handlerMapping" class="com.example.AnnoHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
</bean>
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
<property name="defaultLocale" value="zh_CN"/>
</bean>