第25章 认识更多Spring MVC家族成员(四)

第25章 认识更多Spring MVC家族成员

25.4 框架内的异常处理与HandlerExceptionResolver

在介绍下一位出场选手之前,我们先来简单回顾一下之前讲述的两种Handler类型的定义,即Controller和ThrowawaryController,如下所示:

public interface Controller {
    
    
	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

public interface ThrowawayController {
    
    
	ModelAndView execute() throws Exception;
}

在Effictive Java一书中,作者对异常处理提出了如下的一段陈述:

总是要单独的声明被检查的异常,并且利用Javadoc的@throws标记准确地记录下每个异常被抛出的条件。如果一个方法可能会抛出多个异常类,那么不要使用“快捷方式”,即声明它会抛出这些异常类的某个基类。作为一个极端的例子,永远不要声明一个方法“thtows Exception”,或者更差的做法,“throws Throwable”。这样的声明没有为你的客户提供关于“这个方法能够抛出哪些异常”的任何指导信息,而且大大地妨碍了该方法的使用,因为它实际上掩盖了在同样的执行环境中该方法可能会抛出的任何其他异常。

对于上面的Handler处理方法定义来说,直接抛出异常的做法看起来直接违反了这段描述所倡导的异常处理最佳实践标准,而且框架开发者也承认这一点。不过,让我们换一个角度再来看这样的接口设计。

作为框架类的Handler,其应用的场景可能千差万别,而且在处理各个场景的Web请求的过程中,Handler自身或者Handler所依赖的各种业务对象所可能抛出的checked exception"也是不一而足。试想,如何让Handler来预知那些可能抛出的异常类型呢?就好像牛顿当年只能感慨“我能计算出天体的运行轨迹,却难以预料到…”一样,如果让Handler还去走最佳实践的那条路,显然后果也好不到哪里去。而且,应该根本就达不到最初所设想的目的。所以,框架实现者可能不得不“退而求其次”,转而throwsException。而且,这并非尽是坏处,现在的Handler接口不会对所有可能抛出的异常类型做任何的限制。

虽然最为顶层的Handler接口定义直接throws Exception,但如果愿意,我们依然可以通过覆写Handler的子类来进一步限定处理方法可能拋出的异常类型,例如:

public class ExceptionRaisingController extends AbstractController {
    
    
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest arg0, HttpServletResponse arg1) throws TradeRateExpireException {
    
    
		throw new TradeRateExpireException("test exception");
  	}
}

对于类似的子类实现,使用它们的客户端代码同样可以明确所要处理的具体异常情况。

当然,Handler接口定义能够如此设计和实现,其背后最强大的支持者当属即将登场的HandlerExceptionResolver。是它提供的框架内统一的异常处理方式,让throws Exception看起来更加“理直气壮”。

org.springframework.web.servlet.HandlerExceptionResolver对异常的处理范围仅限于Handler查找以及Handler执行期间,也就是图25-2中矩形所圈定的范围。HandlerExceptionResolver和Handler的关系最不一般,它们就好像双子座两兄弟一样,如果Handler执行过程中没有任何异常,将以ModelAndView的形式返回后继流程要用的视图和模型数据信息,而一旦出现异常情况,HandlerExceptionResolver将接手异常情况的处理,处理完成后,将同样以ModelAndView的形式返回后继处理流程要使用的视图和模型数据信息。只不过,HandlerExceptionResolver所返回的ModelAndView中所包含的信息是错误信息页面和相关异常的信息。试想一下,不是兄弟,HandlerExceptionResolver为什么要专门替Handler收拾各种烂摊子呢?

我们已经了解了Handler,下面来详细见识一下HandlerExceptionResolver吧!其定义如下:

public interface HandlerExceptionResolver {
    
    
	ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
}

HandlerExceptionResolver仅定义了一个resolveException(..)方法,负责处理与相关Handler所关联的某种异常(如参数Exception ex所示),并异常处理完成后,将处理结果以ModelAndView的形式返回。这当然包括将要跳转到的错误信息页面,以及该页面所要显示的必要信息。至于HandlerExceptionResolver所返回的ModelAndView的后继处理,与Handler处理后返回的ModelAndView应该说是“殊途同归”了。如果没记错的话,下面DispatcherServlet将寻求ViewResolver和View对这些返回的信息进行处理。

从HandlerExceptionResolver的“家谱”上看,它在Spring MVC框架中有点儿“人丁不旺”的意思。因为框架只提供了org.springframework.web.servlet.handler.SimpleMappingExceptionResolver这一个可用的实现类。不过,通常情况下,SimpleMappingExceptionResolver已经足够完成主要使命了。

SimpleMappingExceptionResolver使用Properties来管理具体异常类型与所要转向的错误页面之间的映射对应关系。大部分情况下,通过Properties类型的exceptionMappings属性指定具体的映射关系,将是使用SimpleMappingExceptionResolver进行异常处理所要做的主要的工作。

下方代码清单给出的正是SimpleMappingExceptionResolver的使用配置示例。

<bean name="handlerExResolver" class="org.springframework.Web.servlet.handler.SimpleMappingExceptionResolver">
	<property name="defaultErrorView" value="defaultErrorViewName"/>
	<property name="exceptionMappings">
		<props>
			<prop key="TradeRateExpireException">tradeRateExpireErrorPage</prop>
      ...
			<prop key="java.lang.Exception">..</prop>
		</props>
	</property>
</bean>

SimpleMappingExceptionResolver内部将遍历exceptionMappings的所有元素,寻找其中与当前抛出的异常类型“最接近”的映射项,并将其对应的值作为错误信息页面的逻辑视图名,然后封装到ModelAndView中返回以供后继处理流程使用。

小心:SimpleMappingExceptionResolver在将当前抛出的异常类型与exceptionMappings中相应映射进行匹配的时候,采用的不是类型匹配,而是根据类名进行匹配,更进一步说,是使用类名字符串的局部匹配原则。打个比方吧!假设我们有如下映射关系:

Exception <-> page1
TradeRateExpireException <-> page2
exceptions.TradeRateExpireException <-> page3

如果当前抛出的异常类型是a.b.c.exceptions.TradeRateExpireException,那么最终匹配结果可能不是我们想要的page2,而是page1。为什么会造成这种情况呢?SimpleMappingExceptionResolver使用String的indexOf(..)方法来判断当前抛出异常类型的类名a.b.c.exceptions.TradeRateExpireException中是否存在exceptionMappings所指定的各项映射中的键值。存在的话,则首先认为存在的映射项为匹配结果,并且同时限定查找的depth的上下限。如果余下的其他映射项的匹配不足以进一步缩小depth的上下限,那么最初的匹配结果将作为最终结果。在我们的映射中,对Exception的匹配已经将depth的上下限限定在了[0,0],所以,即使下面有更加合适的匹配结果,也不能够进一步缩小该depth上下限范围,那么Exception对应的page1就作为结果返回。而实际上,我们想要的是TradeRateExpireException,甚至于最差也应该是exceptions.TradeRateExpireException。显然SimpleMappingExceptionResolver这种处理方式并不合适。不过,如果你要使用SimpleMappingExceptionResolver的话,则需要了解该实现类在这部分逻辑实现上的“陷阱”,以免预料之外的情况发生了,却找不到原因“。为了避免SimpleMappingExceptionResolver的这种indexOf(..)匹配方式可能造成的“误解”,建议在指定映射中的异常名称的时候,全部使用全限定类名(Full Qualified Class Name)。这样,就会进一步强制Simp1eMappingExceptionResolver去做类名的完全匹配,从而最大限度地避免以上问题的发生。

除了通过exceptionMappings属性定制SimpleMappingExceptionResolver的异常处理行为,SimpleMappingExceptionResolver还定义了其他几个属性,以进一步定制其功能,主要为如下几个。

  • defaultErrorView。用于指定一个默认的错误信息页面对应的逻辑视图名。当无法通过exceptionMappings查找到可用的视图名的时候,defaultErrorView所指定的默认视图名将被返回。

  • defaultStatusCode。可以指定异常情况下默认返回给客户端的HTTP状态码。比如,我们可以通过defaultStatusCode设定默认的HTTP状态码为500(SC_INTERNAL_SERVER_ERROR)或者404(SC_NOT__FOUND)。当然,该属性是可选的。

  • exceptionAttribute。如果我们想要在错误信息页面中对抛出的异常进行访问,那么可以设定exceptionAttribute的值,然后以设定后的值作为键,在错误信息页面中获取抛出的异常实例。当然,即使不对其进行设定,我们依然可以在错误信息页面中使用exceptionAttribute的默认值“exception”获取相应的异常实例。另外,如果不想将异常公开给客户端页面的话,则可以将exceptionAttribute的值设为null。

  • mappedHandlers和mappedHandlerClasses。如果不为mappedHandlers和mappedHandlerClasses指定属性的值,那么SimpleMappingExceptionResolver将捕获和处理所有的Handler抛出的异常。可以通过mappedHandlers或者mappedHandlerClasses属性来限定这种默认行为,比如,只指定对某几个Handler或者某几种Handler类型所抛出的异常进行处理。

有关SimpleMappingExceptionResolver的各个属性的详细情况可以参考该类的Javadoc。而最后我们要提一下SimpleMappingExceptionResolver的order属性,该属性能够带给SimpleMappingExceptionResolver什么特性,我想大家应该可以猜个“八九不离十”了。在Spring MVC框架中,我们可以按照优先级顺序指定多个HandlerMapping以及ViewResolver的实例来帮助我们细化相应关注点的处理,而HandlerExceptionResolver则是框架内第三个拥有这种能力的“人”!也就是说,如果我们在DispatcherServlet的WebApplicationContext中指定多个HandlerExceptionResolver实例的话,DispatcherServlet将根据它们的优先级顺序选取合适的实例进行异常处理。当然啦,优先级的控制方式依然是通过ordered接口来进行,如下方代码清单所示。

<bean name="handlerExResolver1" class="org.springframework.Web,servlet.handler.SimpleMappingExceptionResolver">
	<property name="order" value="1"/>
  	...
<bean>
  
<bean name="handlerExResolver2" class="org.springframework.Web.servlet.handler.SimpleMappingExceptionResolver">
	<property name="order" value="2"/>
<bean>

名称依然不重要,Dispatcherservlet将依然通过类型来获取和使用容器中所指定的多个HandlerExceptionResolver实例。

在使用多个HandlerExceptionResolver实例的时候,有个问题需要注意:如果通过mappedHandlers或者mappedHandlerClasses属性为SimpleMappingExceptionResolver指定一组目标Handler,那么,SimpleMappingExceptionResolver的exceptionMappings和defaultErrorView将只属于这一组指定的目标Handler。如果这之外的Handler抛出异常,那么当前SimpleMappingExceptionResolver将把“控制权”转给下一个。

可是如果没有指定mappedHandlers或者mappedHandlerClasses,那么exceptionMappings和defaultErrorView将应用于所有的Handler。这个时候,本来可能不想让当前SimpleMappingExceptionResolver处理的异常类型被抛出,而当前SimpleMappingExceptionResolver自身又找不到合适的处理匹配结果,那么它就会以defaultErrorView作为最后的救命稻草,返回以defaultErrorView作为视图的ModelAndView。如果当前HandlerExceptionResolver具有较高的优先级,那么即使较低优先级的HandlerExceptionResolver中exceptionMappings有对应当前抛出异常类型的处理映射关系,也绝对没有机会“崭露头脚”了。

25.5 国际化视图与LocalResolver

网络拉近了人与人之间的距离。即使相距千里,人们也可以通过网络互相了解对方的信息和文化。但是,不管怎么说,在“地球村”没有统一的“官方语言”之前,不同地区的不同语言依然是人们能够互相交流的一道障碍。所以,现在的Web应用程序尤其是企业级的应用,都会提供国际化的信息支持,以便可以根据访问者的Locale信息为他们提供相应语言的信息内容。为用户提供国际化视图支持自然成为Spring MVC框架不可或缺的一部分。

在ViewResolver根据逻辑视图名解析视图的时候,ViewResolver的resolveViewName(viewName, locale)方法除了接受要解析的逻辑视图名作为参数之外,还同时接受一个Locale类型对象。这样,ViewResolver就可以根据Locale的不同而返回针对不同Locale的视图实例。到此为止,好像没有必要再往下看了,ViewResolver的设计已经足以完成国际化视图支持的使命了,不是吗?难道ResourceBundleViewResolver不就是很好的例证吗?实际上,从ViewResolver这个层次上来讲,情况确实如此。但是,我们可曾想过,ViewResolver所接受的Iocale实例是从何而来的呢?如何获取用户所对应的Locale呢?只有揭开这一谜团,才能将SpringMVC框架内对国际化视图的支持讲述完整。

可以有多种方式获取用户通过浏览器提交的Web请求所对应的Locale值,比如,根据HTTP的Accept-Language协议头进行解析,或者读取用户浏览器端存储的相应Cookie值等。鉴于有如此多不同的处理方式,Spring MVC使用org.springframework.web.servlet.LocaleResolver接口定义对各种可能的Locale值的获取/解析方式进行统一的策略抽象。该接口定义如下:

public interface LocaleResolver {
    
    
  Locale resolveLocale(HttpServletRequest request);
  void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale);
}

作为策略接口,LocaleResolver主要完成两个工作:第一,由Locale resolveLocale(request)方法负贵根据当前Locale解析策略获取当前请求对应的Locale值;第二,如果当前策略支持Locale的更改,那么可以通过setLocale(..)方法对当前策略默认取得的Locale值进行变更。

25.5.1 可用的LocaleResolver

根据通常的Locale获取策略,Spring MVC为LocaleResolver提供了相应的可用实现类,如下所述。

  • FixedlocaleResolver。最简单的LocaleResolver实现类。一旦指定给FixedLocaleResolver一个Locale值,FixeaLocaleResolver将一直持有并返回这个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。

25.5.2 LocaleResolver的足迹

要在Spring MVC应用中使用相应的LocaleResolver对Locale进行解析和设置,只需要将相应实现类添加到DispatherServlet的WebApplicationContext中。在合适的时机,该LocaleResolver将被使用。我们以sessionLocaleResolver为例,给出对应的配置内容如下:

<bean id="localeResolver" class="org.springframework.Web.servlet.i18n.SessionLocaleResolver" p:defaultLocale="zh_CN">
</bean>

在把要使用的LocaleResolver实现类添加到容器的过程中需要注意,名称“localeResolver”是必须的。因为DispatcherServlet在初始化的时候,将按照该指定名称到WebApplicationContext中去查找可用的LocaleResolver实例。如果找不到,DispatcherServlet将使用默认的FixedLocaleResolver。

如果只使用LocaleResolver,那么将相应实现类添加到WebApplicationContext之后我们就可以收手了。不过,要是想进一步了解添加到WebApplicationContext的LocaleResolver实例都可以在Web请求处理过程中的哪些时间点发挥作用,LocaleResolver走过的几个点还是需要知道一下的。

  • 在DispatcherServlet要处理接收到Web请求之前,它会将其在初始化的时候获取的LocaleResolver实例,以及通过该LocaleResolver解析后的Locale值,以LocaleContext的形式绑定到当前线程。这样,在需要的时候,后继处理流程就可以通过绑定的LocaleContext获得当前Locale值以及使用的LocaleResolver。

  • 流程按照我们之前所描述的顺序执行,在ViewResolver行动之前,DispatcherServlet将使用初始化时获取到的LocaleResolver进行Locale的解析,然后,ViewResolver就可以使用该LocaleResolver所返回的Locale进行视图查找了。到这里,我们就明白了ViewResolver是如何获得客户端请求对应的Locale值了。

  • ViewResolver从“宏观上”解决了对应不同Locale的视图选取问题。不过,如果我们要在具体的视图内访问Locale信息该怎么办?原则上来说,我们是通过org.springframework.web.servlet.support.RequestContext访问必要的国际化信息,包括当前请求对应的Locale、应用使用的LocaleResolver,以及MessageSource中的国际化信息。如果使用JSP作为视图技术,那么直接使用Spring MVC提供的自定义Tag就可以,比如<spring:message code=".."/>,因为它底层就是通过RequestContext完成相应功能的。但是,如果使用Velocity/Freemarker之类的视图技术,我们就不得不直接使用Request-Context来完成这些相关信息的访问了。而通过设置AbstractView的requestContextAttribute属性可以让我们在这些视图中获取到RequestContext的支持。

  • 最后一步是清理的工作,DispatcherServlet将恢复以LocaleContext形式绑定到当前线程的Locale相关信息。现在,跟着LocaleResolver的足迹走过一遍之后,各位是否已经理解了LocaleResolver的存在价值了呢?

25.5.3 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对用户请求进行拦截即可,如下方代码清单所示。

<bean id="handlerMapping" class="org.springframework.Web.servlet.handler.BeanNameUrlHandlerMapping">
	<property name="interceptors">
		<list>
			<ref bean="marketAccessInterceptor"/>
			<ref bean="localeChangeInterceptor"/>
		</list>
	</property>
</bean>
  
<bean id="1ocaleChangeInterceptor" clasg="org.springframework.Web.servlet.i18n.LocaleChangeInterceptor">
	<property name="paramName" value="lang"/>
</bean>

<bean id="localeResolver" class="org.springframework.Web.servlet.i18n.CookieLocaleResolver" p:defaultLocale="en_US">
</bean>

如果我们以类似http://host:port/simplefx/anyAction.do?lang=zh_CN,或者http://host:port/simplefx/anyAction.do?lang=n1这样的请求形式发送处理请求,那么所期望的视图页面就会展现在眼前了。因为我们配置LocaleChangeInterceptor时使用lang作为标志参数。如果你认为使用默认标志参数即可,那么不要对LocaleChangeInterceptor的paramName属性做任何配置。这样,http://host:port/simplefx/anyAction.do?ocale=zh_CN形式的请求将完成同样的功能。

猜你喜欢

转载自blog.csdn.net/qq_34626094/article/details/125857020
今日推荐