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

25.3 框架内处理流程拦截与HandlerInterceptor

前面已经讲述了, HandlerMapping返回的用于处理具体Web请求的Handler对象,是通过一个HandlerExecutionChain对象进行封装的 (这在HandlerMapping的接口定义上可以看出来),我们却一直没有对这个HandlerExecutionChain做进一步的解释,现在是彻底揭开这个谜团的时候了。

说白了, HandlerExecutionChain就是一个数据载体,它包含了两方面的数据,一个就是用于处理Web请求的Handler,另一个则是一组随同Handler一起返回的HandlerInterceptor。 这组HandlerInterceptor可以在Handler的执行前后对处理流程进行拦截操作。

HandlerInterceptor定义了如下三个拦截方法:

public interface HandlerInterceptor {
	boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
	void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception;
	void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}

下面是对这三个拦截方法的简单说明。

  • boolean preHandle(…)。 该拦截方法将在相应的HandlerAdaptor调用具体的Handler处理Web请求之前执行。如果想在此之前阻断后继处理流程,preHandle(..)方法将是最合适也是我们唯一的选择。preHandle(..)通过boolean返回值表明是否继续执行后继处理流程,如下所述。

    • true表明允许后继处理流程继续执行。如果当前HandlerInterceptor位于所在HandlerInterceptor链之前或者中间位置,那么后继HandlerInterceptor的preHandle(..)将继续执行。如果HandlerInterceptor是所在HandlerInterceptor链的最后一个,那么处理Web请求的Handler将允许执行。

    • false表明preHandle(..)方法不允许后继流程的继续执行,包括HandlerInterceptor链中的其他HandlerInterceptor以及其后的Handler。在这种情况下,通常认为preHandle(..)方法内部已经自行处理掉了当前的Web请求。当然,通过抛出相应的异常的方式,也可以达到与返回false同样的阻断效果。

一般来说,preHandle(..)将是我们使用最多的拦截方法。我们也可以在这里进行一些必要条件检查,如果没能通过检查,通过preHandle(..)可以阻断后继处理流程的执行。

  • void postHandle(…)。 该拦截方法的 执行时机为HandlerAdaptor调用具体的Handler处理完Web请求之后,并且在视图的解析和渲染之前。 通过该方法我们可以获取Handler执行后的结果,即ModelAndView。我们可以在原处理结果的基础上对其进行进一步的后处理,比如添加新的统一的模型数据,或者对ModelAndView中的数据进行变更等。postHandle(..)返回类型为void,不可以阻断后继处理流程。

  • void afterCompletion(…)。 在框架内 整个处理流程结束之后,或者说视图都渲染完了的时候 ,不管是否发生异常,afterCompletion(..)拦截方法将被执行。如果处理是异常结束的话,我们可以在该方法中获得异常(Exception)的引用并对其进行统一处理。另外,如果Web请求处理过程中有相应资源需要清理的话,也可以在这里完成。不用说也知道,afterCompletion(..)的返回值为void,并且到它执行的时候,处理流程已经是尾声了,根本没有阻断执行流程的必要。

我想,对于HandlerInterceptor三个拦截方法的说明已经很明了了。如果要对HandlerInterceptor的拦截位置有一个更加感性的认识的话,不妨回头看一下我们出发前的地图(图25-2)。

25.3.1 可用的HandlerInterceptor实现

做任何事情之前我们都会先去找一下有没有现成的“锤子”。对于HandlerInterceptor来说,情况同样如此。在实现自定义的HandlerInterceptor之前,我们先看一下Spring MVC都准备了哪些现成的HandlerInterceptor实现。

实际上,通过查看HandlerInterceptor的继承层次,我们可以发现四五个HandlerInterceptor实现类:UserRoleAuthorizationInterceptor、WebContentInterceptor、LocaleChangeInterceptor以及ThemeChangeInterceptor等。不过,鉴于在稍后介绍LocalResolver和ThemeResolver的时候会再次接触LocaleChangeInterceptor和ThemeChangeInterceptor,我们先将它们放置一边,重点看一下UserRoleAuthorizationInterceptor和WebContentInterceptor这两个可用的HandlerInterceptor实现类。

org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor。 UserRoleAuthorizationInterceptor允许我们通过HttpServletRequest的isUserInRole(..)方法,使用指定的一组用户角色(UserRoles)对当前请求进行验证。如果验证通不过,UserRoleAuthorizationInterceptor将默认返回HTTP的403状态码,即forbidden。我们可以通过覆写handleNotAuthorized(..)方法改变这种默认行为,比如将请求导向一个信息页面。UserRoleAuthorization- Interceptor的使用极其简单,只需要指定验证用的一组用户角色(UserRoles)即可,如下所示:

<bean id="userRolesAuthHandlerInterceptor" class="org.springframework.Web.servlet.handler.UserRoleAuthorizationInterceptor">
	<property name="authorizedRoles">
		<list>
			<value>Admin</value>
		</list>
	</property>
</bean>

UserRoleAuthorizationInterceptor将循环遍历这组指定的用户角色(UserRoles)对当前请求进行验证。

org.springframework.web.servlet.mvC.WebContentInterceptor。 WebContentInterceptor对处理流程的拦截主要做如下几件事情。

  • 检查请求方法类型是否在支持方法之列。 如果当前请求的方法类型超出我们通过setSupportedMethods(..)方法指定的范围,那么WebContentInterceptor将抛出HttpRequestMethodNotSupportedException从而阻断后继处理流程。这通常用于进一步限定请求的方法类型,比如,我们可以通过setSupportedMethods(..)方法设置supportedMethods只为POST一种,不支持GET或者其他请求方法类型。

  • 检查必要的Session实例。 如果我们设置requireSession属性为true,同时又发现当前请求不能返回一个已经存在的Session实例,WebContentInterceptor将抛出HttpSessionRequiredException阻断后继处理流程。

  • 检查缓存时间并通过设置相应HTTP头(Header)的方式控制缓存行为。 WebContentInterceptor允许我们通过setCacheSeconds(..)方法设置请求内容的缓存时间。它将通过设置用于缓存管理的HTTP头(HTTPHeader)的形式,对请求内容对应的缓存行为进行管理。我们可以通过useCacheControlHeader或者useExpiresHeader属性,进一步明确是使用的HTTP 1.1的Cache-Control指令还是HTTP 1.0的Expires指令。

通常,WebContentInterceptor使用方式如下所示:

<bean id="WebContentInterceptor" class="org.springframework.Web.servlet.mvc.WebContentInterceptor" p:cacheSeconds="30" p:supportedMethod="POST">
</bean>

除此之外,我们还可以通过setCacheMappings(..)方法,进一步明确指定不同请求与其缓存时间之间的细粒度的映射关系。

注意:UserRoleAuthorizationInterceptor和WebContentInterceptor都是只在preHandle(..)拦截方法中实现了相应的拦截逻辑。我想,你应该已经从它们能够“阻断后继处理流程”的功能上看出这一点。

25.3.2 自定义实现HandlerInterceptor

Spring为我们提供了现成的HandlerInterceptor固然不错,但这并不足以满足广大群众的各种需求。单就HandlerInterceptor作为一个扩展点而存在的意义来讲,如果拦截Web请求处理逻辑的需求就那么几种的话,完全没有必要设置这么一个角色。而实际上,我们所要面对的系统和场景却是繁杂多变的,所以,大部分时间,我们不得不根据应用的需求提供我们的自定义HandlerInterceptor实现类。

在给出我们的自定义HandlerInterceptor之前,按照惯例,我们还得先把该HandlerInterceptor所要处理的业务场景说清楚。FX系统宣称全天24小时都可交易,但实际上,每天都会有少量的时间用于做rollover操作,以便对系统状态进行调整。而这段时间是不能为顾客提供交易服务的。所以,我们要对顾客的交易行为进行限制,当市场处于关闭状态的时候,将不予处理用户的请求。为了达到该目的,可以从程序逻辑上,或者系统管理上,进行多方面考虑。但既然我们现在处于HandlerInterceptor的“领地”,就把这块儿工作交给HandlerInterceptor做如何?

我们的自定义HandlerInterceptor实现类有一个工作, 如果发现现在市场是处于关闭状态,则将当前Web请求转向一个表明系统暂停交易画面,否则允许请求的处理继续进行 ,如下方代码清单所示的原型实现。

public class MarketAccessInterceptor extends HandlerInterceptorAdapter {
	private static final Log 1ogger = LogFactory.getLog(MarketAccessInterceptor.class);

	private String marketClosedPageUrl;
	private IMarketStatusMonitor marketStatusMonitor;

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		if(getMarketStatusMonitor().isMarketClosed()) {
			InternalResourceView view = new InternalResourceView();
			view.setUrl(getMarketClosedPageUrl());
			view.render(null, request, response);
			if(logger.isInfoEnabled()) {
				logger.info("since market has been close," +
				"we will redirect the user request to market-close information page.");
	      	}
			return false;
    	}
		return true;
  	}
	// getter和setter方法定义...
}

IMarketStatusMonitor提供了检查市场状态所需要的服务,至于是让它通过检查数据库状态还是检查其他数据源来完成这项使命,那就不是我们的MarketAccessInterceptor所要关心的啦!

提示:我们的MarketAccessInterceptor并没有直接实现HandlerInterceptor接口,而是继承自HandlerInterceptorAdapter类。不知道你是否已经明了其中缘由?实际上,如果你足够细心的话,就会发现这是Java平台API设计上的一个“光荣传统”。对于那些需要经常被扩展,而又包含多个方法需要实现的接口声明,通常情况下,使用者并非每次都希望提供所有接口方法的实现逻辑。为了避免在实现这些接口的时候,每次都要去实现接口定义中的所有方法,对应API的设计者通常都会提供一个XXXAdaptor类专门用于子类化的需要,避免实现所有接口方法的烦琐。

比如,在AWT/Swing库中的各种事件监听器(EventListener)接口定义,通常都会定义多个事件回调方法。java.awt.event.MouseListener接口就定义了如下5个事件回调方法。

void mouseClicked(MouseEvente)。

void mousePressed(MouseEvent e)

void mouseReleased(MouseEvent e)。

void mouseEntered(MouseEvent e)。

void mouseExited(MouseEvent e)。

但通常情况下,我们可能只希望处理鼠标点击等一两个动作,那么每次直接实现MouseListener接口则比较烦琐。MouseAdapter将帮助我们减轻子类化时候的“压力”。我们直接继承MouseAdapter,然后覆写需要实现的那个方法即可。我们还可以在其他地方遇到这种提供XXXAdaptor的API设计方式。如果某一天我们也遇到类似的情况的话,说不定也可以借鉴一下哦。只不过,不要将它与Adaptor模式(Adaptor Patterm)里的那个Adaptor相混淆。

有了MarketAccessInterceptor自定义实现类,我们就可以将其添加到WebApplicationContext使用了,如下所示:

<bean id="marketAccessInterceptor" class="cn.spring21.simplefx.interceptor.MarketAccessInterceptor">
	<property name="marketClosedPageUrl" value="/WEB-INF/jsp/marketCloseNotification.jsp"/>
	<property name="marketStatusMonitor" ref="marketStatusMonitor"/>
</bean>

marketClosedPageUrl属性指定市场关闭后请求将被转向的信息页面路径。 因为我们在MarketAccessInterceptor内部通过InternalResourceView来渲染视图,所以指定的路径指向.jsp形式的视图模板。不过,如果想要MarketAccessInterceptor拥有更好的扩展性,改为直接注入相应的视图也无妨。

25.3.3 HandlerInterceptor寻根

我们已经知道了HandlerInterceptor的作用,也知道了Spring MVC都提供了哪些常用的HandlerInterceptor实现,甚至,也了解了如何自定义一个HandlerInterceptor,却还不知道到底应该如何将HandlerInterceptor纳入应用程序才能使其工作。

HandlerInterceptor和Handler实际上“本是同根生”。如果我们从HandlerInterceptor所处的位置溯源而上(按照HandlerInterceptor→HandlerExecutionChain→HandlerMapping的顺序),则会发现HandlerMapping是其最终的发源地,AbstractHandlerMapping作为几乎所有HandlerMapping实现类的父类,提供了setInterceptors(..)方法以接受一组指定的HandlerInterceptor实例。所以,要使我们的HandlerInterceptor发挥作用,只要将它添加到相应的HandlerMapping即可,如下所示:

<bean id="handlerMapping" class="org.springframework.Web.servlet.handler.BeanNameUr1HandlerMapping">
	<property name="interceptors">
		<list>
			<ref bean="userRolesAuthHandlerInterceptor"/>
			<ref bean="WebContentInterceptor"/>
			<ref bean="marketAccessInterceptor"/>
		</list>
	</property>
</bean>

这些指定的HandlerInterceptor将随同处理具体Web请求的Handler一起返回(以HandlerExecutionChain的形式),并对Web请求的处理流程进行拦截。

在讲解HandlerMapping的时候提到过,我们可以在同一Spring MVC应用程序中指定多个拥有不同优先级的HandlerMapping,每一HandlerMapping管理自己所负责的一组Web请求处理的映射关系。在HandlerInterceptor结合HandlerMapping的这一特性(即ChainingOfHandlerMapping)之后,就可以细化Web请求处理流程的拦截范围。 我们可以只赋予某一HandlerMapping相应的HandlerInterceptor实例,这样,该HandlerInterceptor就可以只拦截这一HandlerMapping所管理的那一组处理Web请求的Handler。 相对于只有一个HandlerMapping的情况,这样的处理方式让HandlerInterceptor的使用更加地灵活。

25.3.4 HandlerInterceptor之外的选择

在Spring MVC中,并非只有HandlerInterceptor才能对Web请求的处理流程进行拦截并做相应的处理。既然Spring MVC同样基于Servlet API构建,那么,Servlet提供的规范设施自然也可以在使用Spring MVC的Web应用程序中使用。 所以,能够提供拦截能力的Servlet标准组件Filter,就成为了使用HanlderInterceptor之外的选择。 HandlerInterceptor和ServletFilter的共同特点是都可以用于Web请求处理流程的拦截。但在某些方面二者也存在一些差别,如图25-3所示。

image-20220711214010833

HandlerInterceptor位于DispatcherServlet之后 ,指定给HandlerMapping的它,可以对HandlerMapping所管理的多组映射处理关系进行拦截。最主要的是,HandlerInterceptor拥有更细粒度的拦截点。我们可以在Handler执行之前,Handler执行之后,以及整个DispatcherServlet内部处理流程完成时点插入必要的拦截逻辑。通过结合HandlerMapping的Chaining特性,我们可以对不同HandlerMapping管理的多组不同的Handler,应用不同的HandlerInterceptor进行处理流程的拦截处理,总之,HandlerInterceptor带给我们的是- 种具有高灵活性的细粒度的请求处理流程拦截方案。

与HandlerInterceptor不同,Filter提供的拦截方式更像是一种对Web应用程序的“宏观调控”。作为Servlet规范的标准组件,Filter通常被映射到Java Web应用程序中的某个servlet,或者一组符合某种URL匹配模式的访问资源上。所以,从Spring MVC应用的结构上看, Filter位于DispatcherServlet之前。 如果把Filter和HandlerInterceptor看作同一类型的拦截器,Filter将比HandlerInterceptor拥有更高的执行优先级。不过,二者提供的拦截功能所加诸于上的目标对象却完全是不同级别: Fiter序列在servlet层面对DispatcherServlet进行拦截,而HandlerInterceptor则位于DispatcherServlet内部,对Handler的执行进行拦截。 Filter的应用位置注定了它不能够提供细粒度的拦截时点, 所以,通常情况下,使用Filter对于Web应用程序中的一些普遍关注点进行统一处理是比较适合的,一旦需要细化处理流程的拦截逻辑,可以再转而求助于Handlerinterceptor。

Filter是servlet标准组件,需要在web.xml中配置,这就意味着,其生命周期管理更多是由Web容器进行管理的。如果我们的Filter在实现期间需要某些服务的支持,尤其是当前Spring MVC应用的WebApplicationContext中的某些服务的支持,我们不得不采用某种过度耦合的绑定机制或者查找方式来获取这些服务的支持。为了能够让Filter的实现更加无拘无束,尽情享用依赖注入所带来的乐趣,Spring MVC引入了org.springframework.web.filter.DelegatingFilterProxy以改变Filter的现状。

顾名思义, DelegatingFilterProxy的作用是作为一个Filter的Proxy对象,当真正需要执行拦截操作的时候,它将把具体的工作委派给它所对应的一个Filter委派对象。 在物理结构上,DelegatingFilterProxy位于web.xml中承担Filter的原始使命,而它所委派的那个Filter对象,也就是做实际工作的那个家伙,却可以置身于WebApplicationContext中,充分享受Spring的IoC容器所提供的各项服务。图25-4演示了DelegatingFilterProxy与其对应的Filter实例之间的存在关系。

image-20220711214351823

在DelegatingFilterProxy和其Fiter委派对象的关系中,基本上所有“脏活儿”(dirty work)都是由DelegatingFilterProxy来做的,包括从绑定到ServletContext的WebApplicationContext中获取其将使用的Filter委派对象,然后读取原始的Filter配置信息,并设置给委派对象使用等。只有在这些准备工作都完成之后,我们才能使用真正依赖的那个Filter对象。这是不是让我们感觉又回到了IoC之前的时代了呢?我们的最终目的只是想要一个直接可用的Filter实例而已。不过,DelegatingFilterProxy做了这些事情,倒是能够解脱实际使用的那个Filter实现类,也算值得吧!有了DelegatingFilterProxy的付出,我们自己的Filter实现过得就舒服多了。

下方代码清单给出了一个享受IoC服务的Filter实现类。

public class AnyFilter implements Filter {
	private IAuthorityService authorityService;
	private IAnySerivce anyServiceYouWant;

	public void init(FilterConfig config) throws ServletException {
		//如果需要的话,做必要的初始化工作
  	}

	public void doFilter(ServletReguest request,ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
		//使用注入的服务实现相应的逻辑
			filterChain.doFilter(request, response);
  	}

	public void destroy() {
		//可能必要的对象销毁逻辑
  	}

	public IAuthorityService getAuthorityService() {
		return authorityService;
	}

	public void setAuthorityService(IAuthorityService authorityService) {
		this.authorityService = authorityService;
	}

	public IAnySerivce getAnyServiceYouWant() {
		return anyServiceYouWant;
  	}

	public void setAnyServiceYouWant(IAnySerivce anyServiceYouWant) {
		this.anyServiceYouWant = anyServiceYouWant;
  	}
}

只要将我们自己的Filter实现添加到WebApplicationContext管理,该Filter实现类就可获得所需要的依赖注入,如下所示:

<bean id="anyCustomFilter" class="..AnyFilter">
  <property name="authorityService" ref=".."/>
  <property name="anyServiceYouWant" ref=".."/>
</bean>

不过,要想让这个原来本该在web.xml中配置生效,而现在却在WebApplicationContext中的Filter发挥作用,我们需要让它与DelegatingFilterProxy发生关系才行。 **而让WebApplicationContext中的Filter实例成为DelegatingFilterProxy的代理对象的默认关系标志,就是容器中Filter实例的beanName,只需要保证其beanName与DelegatingFilterProxy所对应的<filter- name>的值相同,就能确定二者之间的委派关系。**下方代码清单给出的是对应的DelegatingFilterProxy在web.xml中的配置内容。

<filter>
	<filter-name>anyCustomPilter</filter-name>
	<filter-class>
		org.springframework.Web.filter.DelegatingFilterProxy
	</filter-class>
</filter>

<filter-mapping>
	<filter-name>anyCustomFilter</filter-name>
	<url-pattern>..</url-pattern>
	<!--或者 <servlet-name>..</servlet-name>-->
</filter-mapping>

DelegatingFilterProxy将根据<filter- name>的值到WebApplicationContext中抓取对应的Fiter实例作为其委派对象来使用,在这里也就是“anyCustomerFilter”所对应的Filter实例。

在使用DelegatingFilterProxy的场景中,具体Filter实例的生命周期默认是由WebApplicationContext容器进行管理的。也就是说,Filter的标准生命周期管理方法init(..)destroy(),对于WebApplicationContext容器来说没有特殊的意义。DelegatingFilterProxy默认不负责调用具体Filter实例的这两个方法。如果我们希望改变这一场景中默认的生命周期管理方式,即 希望由默认的WebApplicationContext容器管理转向原始的Web容器管理, 那么,我们可以通过将DelegatingFilterProxy的targetFilterLifecycle属性值从默认的false设置为true以完成这一转变,如下所示:

<filter>
	<filter-name>anyCustomFilter</filter-name>
	<filter-class>
		org.springframework.Web.filter.DelegatingFilterProxy
	</filter-class>
	<init-param>
		<param--name>targetFilterLifecycle</param-name>
		<param-value>true</param-value>
		</init-param>
</filter>

DelegatingFilterProxy从WebApplicationContext中获取到Filter委派对象实例之后,将负责调用该委派对象的init(..)初始化方法,销毁的操作也是同样道理。

注意:除DelegatingFilterProxy之外,Spring MVC还在org.springframework.web.filter包中提供了多个Filter实现类,我们可以在实际开发中根据情况选用它们。关于这些Filter类的详细情况,可参考Spring框架提供的Javadoc文档。