Spring MVC更多家族成员--国际化视图与LocalResolver---10

2022-08-23 11:04:41 浏览数 (1)

Spring MVC更多家族成员--国际化视图与LocalResolver---10

  • 引言
    • 可用的LocaleResolver
    • LocaleResolver的足迹
      • LocaleResolver在初始化流程中的使用
        • processRequest处理请求的核心方法
        • DispatcherServlet的doservice方法
        • 小结
      • 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方法实现
代码语言:javascript复制
	@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方法首次给出了实现
代码语言:javascript复制
    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
代码语言:javascript复制
	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
代码语言:javascript复制
	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
代码语言:javascript复制
	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>

0 人点赞