Spring Web源码之核心组件(拦截器与异常处理)

Posted 木兮君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Web源码之核心组件(拦截器与异常处理)相关的知识,希望对你有一定的参考价值。

前言

上篇博文Spring Web源码之MVC主体结构主要讲了spring mvc整体结构以及主要核心流程,其实细心的小伙伴可以看出其实小编还遗留了一些没有讲到,比方说拦截器与异常处理器,今天小编带大家主要拦截器以及异常处理器。
在介绍拦截器与异常处理器之前,想问大家一个问题,拦截器和过滤器的区别,为什么有了拦截器还需要过滤器,小编看起来他们功能都是差不多的,其实过滤器针对servlet进行过滤,而拦截器是针对我们mvc中的handler进行拦截,上篇也有看到在执行handler的业务流程之前可以对其进行拦截。这是小编的一些自己的理解。好了进入正题。

核心组件

拦截器

HandlerInterceptor,大家看到Interceptor就差不多可以理解为拦击器了,大部分开源框架基本都有他是身影,那对于这样一个拦截器接口基本有三个方法:

  1. 前置拦截:调用方法之前的进行拦截。
  2. 后置拦截:调用方法之后进行拦截。
  3. 完成处理:在处理业务的时候发生错误了,那后置拦截就不会被处理,但是完成处理就会被处理,相当于finally中的逻辑

这些方法其实和动态代理有很大的相似,尤其是在做监控的时候,我们需要方法运行时间啊参数啊,报错后的截取啊等等。
接下来咱们来看看拦截的类封装以及拦截的流程:

容小编稍作解释:

  • 首先是拦截器的方法如同小编上面所讲有三个接口,然后他被封装在执行器链中的List中
  • 在获取hanler的时候其实是通过HandlerMapping找到的HandlerExecutionChain
  • 之后获取适配器然后执行handler里面的方法时则会先调用HandlerExecutionChain中的前置后置方法,其实就是List中所有HandlerInterceptor的所有前置后置方法
  • 然后从调用handler方法,创建视图以及渲染视图的时候,任意一个报错都会进入完成处理拦截中,不报错也会执行。

如果看过上篇博客中的源代码,再看这幅图的时候就比较简单了。

拦截器代码演示

拦截器有两种使用方式分别为:1、配置拦截器映射 2、直接注入到HandlerMapping里面去
配置拦截器映射(HandlerMapping映射相似):
spring-mvc.xml

<bean id="myInterceptor" class="com.lecture.mvc.MyInterceptor"/>
    <!--    第一种拦截配置-->
    <bean class="org.springframework.web.servlet.handler.MappedInterceptor">
        <constructor-arg index="0" type="java.lang.String[]">
            <list>
                <value>/requestHandler</value>
            </list>
        </constructor-arg>
        <constructor-arg index="1" type="org.springframework.web.servlet.HandlerInterceptor"
                         ref="myInterceptor"/>
    </bean>
    <!--第二种拦截配置-->
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/requestHandler"/>
            <ref bean="myInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>

这里第二种配置其实是第一种配置的简化,其实质还是第一种配置。

这里小编一直有个不明白MappedInterceptor为什么会这样设计,他既是一个映射,里面包含了HandlerInterceptor又实现了HandlerInterceptor。看源码的时候其实他是去遍历的时候用到,感觉设计并不是很好。还有就是HandlerExecutionChain会有两个HandlerInterceptor的封装,设置的时候先设置到list中然后转到数组中去,spring mvc很多地方都会这么用。一个数组一个list有可能有多例或并发吧,不是很清楚

直接注入到HandlerMapping
spring-mvc.xml

<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello">httpRequestHandlerTest</prop>
            </props>
        </property>
        <property name="interceptors" ref="myInterceptor"/>
    </bean>

第二种的配置比较少用,但是如果和第一种放在一起使用,那拦截器就会有一样的myInterceptor两次(看了源码之后就会明白)。其实第一种方法到最后也是放入到handlerMapping中去,当然中间进过一系列操作,那小编接着往下说

拦截器机制

拦截器是如何封装进去的,先看一下其流程图:

源码阅读
dispatchServlet中的getHandler最终会调用到org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception 
        Object handler = this.getHandlerInternal(request);
        if (handler == null) 
            handler = this.getDefaultHandler();
        

        if (handler == null) 
            return null;
         else 
            if (handler instanceof String) 
                String handlerName = (String)handler;
                handler = this.getApplicationContext().getBean(handlerName);
            
			//这里将handler以及Interecptor合在一起
            HandlerExecutionChain executionChain = this.getHandlerExecutionChain(handler, request);
            if (CorsUtils.isCorsRequest(request)) 
                CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
                CorsConfiguration handlerConfig = this.getCorsConfiguration(handler, request);
                CorsConfiguration config = globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig;
                executionChain = this.getCorsHandlerExecutionChain(request, executionChain, config);
            

            return executionChain;
        
    
	protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) 
		HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
				(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
		//url路径
		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
		//遍历所有的HandlerInterceptor 
		for (HandlerInterceptor interceptor : this.adaptedInterceptors) 
			//如果是MappedInterceptor 拦截器映射则进行下一步判断
			if (interceptor instanceof MappedInterceptor) 
				//是否和拦截器所配置的路径表达式想匹配,如果匹配则加入拦截器链中,这边拦截器强转了一次
				MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
				if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) 
					chain.addInterceptor(mappedInterceptor.getInterceptor());
				
			
			else 
				//直接添加到chain,这里的话就是第二种配置方法 直接注入的
				//加入用两种方式配置则一样的interceptor有两个
				chain.addInterceptor(interceptor);
			
		
		return chain;
	

上面源码就是封装HandlerExecutionChain 的过程,当然还差所有拦截器的初始化过程,方法如下,

org.springframework.web.servlet.handler.AbstractHandlerMapping#initApplicationContext
代码比较简单小编就不继续贴出源码了,无法就是Interceptor以及在ioc容器里面然后根据不同的Interceptor,添加到里面去。

protected void initApplicationContext() throws BeansException 
		extendInterceptors(this.interceptors);
		detectMappedInterceptors(this.adaptedInterceptors);
		initInterceptors();
	

说完了拦截器,接下来进入异常处理

异常处理器

异常处理只能是在获取Handler,获取Handler适配器以及调用的过程中抛出异常,我们将跳转到哪个页面。异常处理可以配置异常处理的映射表,让不同的异常跳对应的视图界面,说到这儿我们先看一下异常处理的流程以及对应异常处理类:

这里稍微说明一点,当出现异常的时候,后置拦截是不会被执行的。

异常处理器代码演示

异常处理器有两种使用方式分别为:1、基于映射配置 2、基于注解
基于映射配置
就如上图一样,不同的异常对应不同的视图即可
spring-mvc.xml 下面是将IOException和IllegalArgumentException配置

 <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
            <property name="defaultErrorView" value="error"/>
        <property name="exceptionMappings" >
            <props>
                <prop key="java.io.IOException">IoError</prop>
                <prop key="java.lang.IllegalArgumentException">IllegalError</prop>
            </props>
        </property>
    </bean>

基于注解
跟映射配置不同的是上面是交给了HandlerExceptionResolver来帮忙封装创建的,而这个是咱们自己来封装的,具体看下面类:

@Controller
@ControllerAdvice
public class MethodHandler 

    @ExceptionHandler(value = IllegalArgumentException.class, IOException.class)
    public ModelAndView handleException(Exception ex) 
        ModelAndView mv = new ModelAndView();
        mv.addObject("exception", new RuntimeException("这个是由@ExceptionHandler捕捉的异常"));
        mv.setViewName("error");
        return mv;
    

    @RequestMapping("/method/hello")
    @ResponseBody
    public String hello(HttpServletRequest request) 
        if (request.getParameter("illegalError") != null) 
            throw new IllegalArgumentException("参数错误");
        
        if (request.getParameter("otherError") != null) 
            throw new RuntimeException("其它错误");
        
        return "hello method";
    


这里@ControllerAdvice主要是捕获所有Controller的异常,如果不加只对本类有效,这里调用hello方法的时候就会被上面异常处理器捕获到,然后返回响应的页面。
源码阅读
首先在DispatchServlet初始化过程中会initHandlerExceptionResolvers

private void initHandlerExceptionResolvers(ApplicationContext context) 
		this.handlerExceptionResolvers = null;
		//一般这里都为true,意思为是否开启侦查所有的HandlerExceptionResolvers
		if (this.detectAllHandlerExceptionResolvers) 
			// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
			//在Ioc容器中找到对应HandlerExceptionResolver类,不为空则加进去
			Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
					.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
			if (!matchingBeans.isEmpty()) 
				this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
				// We keep HandlerExceptionResolvers in sorted order.
				AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
			
		
		else 
			try 
				//从IOC容器中拿name为handlerExceptionResolver的HandlerExceptionResolver 
				HandlerExceptionResolver her =
						context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
				this.handlerExceptionResolvers = Collections.singletonList(her);
			
			catch (NoSuchBeanDefinitionException ex) 
				// Ignore, no HandlerExceptionResolver is fine too.
			
		

		// Ensure we have at least some HandlerExceptionResolvers, by registering
		// default HandlerExceptionResolvers if no other resolvers are found.
		if (this.handlerExceptionResolvers == null) 
			//都为空则从配置中查找
			this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
			if (logger.isTraceEnabled()) 
				logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
						"': using default strategies from DispatcherServlet.properties");
			
		
	

初始化完之后又调用到doDispatch方法了,进入到创建异常视图processDispatchResult方法

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception 

		boolean errorView = false;
		//如果说异常不等于空
		if (exception != null) 
			//异常为ModelAndViewDefiningException,则为modelAndView直接返回
			//一般情况不会到这儿
			if (exception instanceof ModelAndViewDefiningException) 
				logger.debug("ModelAndViewDefiningException encountered", exception);
				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
			
			else 
				//大部分情况都是这个
				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
				mv = processHandlerException(request, response, handler, exception);
				errorView = (mv != null);
			
		

		// Did the handler return a view to render?
		if (mv != null && !mv.wasCleared()) 
			render(mv, request, response);
			if (errorView) 
				WebUtils.clearErrorRequestAttributes(request);
			
		
		else 
			if (logger.isTraceEnabled()) 
				logger.trace("No view rendering, null ModelAndView returned.");
			
		

		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) 
			// Concurrent handling started during a forward
			return;
		

		if (mappedHandler != null) 
			mappedHandler.triggerAfterCompletion(request, response, null);
		
	

处理异常视图,所谓处理异常就是遍历handlerExceptionResolvers,如果可以处理就处理了,然后拿到异常的视图然后渲染。否则就报错了。

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
			@Nullable Object handler, Exception ex) throws Exception 

		// Success and error responses may use different content types
		request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

		// Check registered HandlerExceptionResolvers...
		ModelAndView exMv = null;
		if (this.handlerExceptionResolvers != null) 
			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) 
				exMv = resolver.resolveException(request, response, handler, ex);
				if (exMv != null) 
					break;
				
			
		
		if (exMv != null) 
			if (exMv.isEmpty()) 
				request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
				return null;
			
			// We might still need view name translation for a plain error model...
			if (!exMv.hasView()) 
				String defaultViewName = getDefaultViewName(request);
				if (defaultViewName != null) 
					exMv.setViewName(defaultViewName);
				
			
			if (logger.isTraceEnabled()) 
				logger.trace("Using resolved error view: " + exMv, ex);
			
			if (logger.isDebugEnabled()) 
				logger.debug("Using resolved error view: " + exMv);
			
			WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
			return exMv;
		

		throw ex;
	

整体的代码还是比较容易懂的。

总结

今天小编主要是分享了spring mvc中的拦截器与异常处理器,还是那句话虽然很少配置了,但是期望小编能懂里面的原理,并且了解过里面的源码,做到心中有数,而并不是百度或原项目中copy完事。下次为大家带来视图解析(虽然好像已经不太用了),再接再厉加油!

以上是关于Spring Web源码之核心组件(拦截器与异常处理)的主要内容,如果未能解决你的问题,请参考以下文章

Spring Web源码之核心组件(拦截器与异常处理)

Spring Web源码之核心组件(拦截器与异常处理)

(34)java Spring Cloud+Spring boot+mybatis企业快速开发架构之SpringCloud-Zuul过滤器介绍及使用(传递数据拦截请求和异常处理)

Spring 中拦截器与过滤器的区别

Springboot 系列Spring Boot web 开发之拦截器和三大组件

Spring Web源码之映射体系