Spring MVC各组件近距离接触--上--02

2022-08-23 10:58:01 浏览数 (1)

Spring MVC各组件近距离接触--上--02

  • 忙碌的协调人HandlerMapping
    • 可用的HandlerMapping
      • BeanNameUrlHandlerMapping
      • SimpleUrlHandlerMapping
    • HandlerMapping的执行序列(chain of handlermapping)
  • 亲密伙伴Controller
    • AbstractController
      • MultiActionController
        • MethodNameResolver
          • InternalPathMethodNameResolver
          • PropertiesMethodNameResolver
          • ParameterMethodNameResolver
        • MultiActionController使用演示
        • MultiActionController源码剖析
  • 小结

忙碌的协调人HandlerMapping

HandlerMapping帮助DispathcerServlet进行Web请求的URL到具体处理类的匹配。

之所以被称为HandlerMapping是因为,在Spring MVC中,并不只局限于使用org.springframework.web.servlet.mvc.Controller作为DispatcherSevlet的次级控制器来进行具体的Web请求的处理。

实际上,在稍后介绍HandlerAdaptor的时候,就会知道,我们也可以使用其他类型的次级控制器,包括Spring MVC提供的除了Controller之外的次级控制器类型,或者第三方Web开发框架中的Page controller组件,而所有这些次级控制器类型,在Spring MVC中都被称为Handler。

因此HandlerMapping要处理的其实就是Web请求到相应Handler之间的映射关系。

HandlerMapping的定义如下:

代码语言:javascript复制
public interface HandlerMapping {


	String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName()   ".bestMatchingHandler";

	@Deprecated
	String LOOKUP_PATH = HandlerMapping.class.getName()   ".lookupPath";

	String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName()   ".pathWithinHandlerMapping";

	String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName()   ".bestMatchingPattern";

	String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName()   ".introspectTypeLevelMapping";

	String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName()   ".uriTemplateVariables";

	String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName()   ".matrixVariables";

	String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName()   ".producibleMediaTypes";



	default boolean usesPathPatterns() {
		return false;
	}

   
	@Nullable
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

}
  • getHandler方法为什么返回HandlerExecutionChain

HandlerMapping支持请求映射过程中添加对应的拦截器,这就是我们熟悉的并经常在Spring mvc中使用的拦截器链,这也就是解释了,为什么getHandler方法返回的是一个HandlerExecutionChain,因为该HandlerExecutionChain中会包含可以应用到当前handler上的一堆拦截器还有handler自身。

当DispathcerServlet要调用该handler时,会先去调用拦截器链中每个拦截器的preHandler方法,如果都返回true,再去调用对应的hanler。

tomcat在调用某个servlet之前,也会去调用该servlet相关的过滤器链,然后最终再去调用servlet的service方法 对于spring mvc的DispathcerServlet来说,要先等到tomcat的相关过滤器链执行完毕,再去执行DispathcherServlet,然后DispathcerServlet再将请求分发给具体Handler之前,还需要先调用当前handler关联的一堆拦截器

  • 上面给出的一堆ATTRIBUTE,被各个HandlerMapping子实现类所支持,对应的子实现类,会取出自身支持的属性,然后根据结果做相关事情(这个下面会讲)

可用的HandlerMapping

Spring MVC默认提供了多个HandlerMapping的实现:

我们重点关注下面几个实现类:

  • BeanNameUrlHandlerMapping: 用url最后一个资源名作为beanName去匹配对应的handler,例如: http://host:port/hhh.do会去当前容器内寻找名为/hhh.do的Controller定义。
  • SimpleUrlHandlerMapping: 该实现类进一步接触了请求URL与Handler的beanName之间的耦合,并且支持更灵活的映射表达方式
  • ControllerClassNameHandlerMapping: 我们将在后面章节再进行介绍。
  • DefaultAnnotationHandlerMapping: Spring 2.5之后的Spring MVC引入了基于注解的配置方式。

BeanNameUrlHandlerMapping

BeanNameUrlHandlerMapping类的定义如下:

代码语言:javascript复制
public class BeanNameUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping {
    //当前handler可以映射到哪些URLS上去
	@Override
	protected String[] determineUrlsForHandler(String beanName) {
		List<String> urls = new ArrayList<>();
		//如果当前Handler的beanName以/开头,那么对应handler就可以映射到该请求上
		if (beanName.startsWith("/")) {
			urls.add(beanName);
		}
		//获取当前bean的别名数组
		String[] aliases = obtainApplicationContext().getAliases(beanName);
		//如果别名中也有/开头的,那么也加入URLS集合
		for (String alias : aliases) {
			if (alias.startsWith("/")) {
				urls.add(alias);
			}
		}
		//返回的是当前handler可以处理的URLS集合
		return StringUtils.toStringArray(urls);
	}
}

这里不需要去看看其父类中的实现,只需要根据BeanNameUrlHandlerMapping实现的determineUrlsForHandler方法即可推断出,该HandlerMapping的具体映射规则。

  • 即用以"/“开头的beanName或者以”/"开头的别名,作为当前handler映射到的URLS数组

所以,如果要使用BeanNameUrlHandlerMapping,只需要在对应[servletName]-servlet.xml配置文件中,注入该bean即可: (当然,不注入的话,也会默认使用BeanNameUrlHandlerMapping)

代码语言:javascript复制
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
    
    <bean name="/hhh.do" class="com.example.controller.HelloController">
        <property name="helloService" ref="helloService"/>
        <property name="viewName" value="hello"/>
    </bean>

SimpleUrlHandlerMapping

使用BeanNameUrlHandlerMapping进行Web请求到具体Handler的映射管理,需要我们保证视图模板中的请求路径,必须与容器中对应的Handler的beanName一致。

BeanNameUrlHandlerMapping的固定映射模式,并没有对映射做个过程做过多的关注,而是简单的直接匹配。

SimpleUrlHandlerMapping比BeanNameUrlHandlerMapping做的工作多一些,它可以使视图一方和handler一方自由活动,最后由SimpleUrlHandlerMapping进行统筹。

  • SimpleUrlHandlerMapping基本使用如下:
代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.do">helloController</prop>
            </props>
        </property>
    </bean>
    
    <bean name="helloController" class="com.example.controller.HelloController">
        <property name="helloService" ref="helloService"/>
        <property name="viewName" value="hello"/>
    </bean>

</beans>

这里所有请求后面要加.do是因为在web.xml中配置的DispatcherServlet拦截的路径是 * .do

现在控制器Controller可以起任何名字,视图中的链接也可以独立提供,只需要通过SimpleUrlHandlerMapping指定一下二者的对应关系即可。

SimpleUrlHandlerMapping 的源码也非常的简单:

代码语言:javascript复制
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
    //存放controller控制器和其对应映射到的请求路径
	private final Map<String, Object> urlMap = new LinkedHashMap<>();
    
    //下面两个方法对应xml中往urlMap中注入属性的两种不同方式
	public void setMappings(Properties mappings) {
		CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
	}

	public void setUrlMap(Map<String, ?> urlMap) {
		this.urlMap.putAll(urlMap);
	}
	
	public Map<String, ?> getUrlMap() {
		return this.urlMap;
	}


=   //该方法会在父类中被调用,因为父类实现了ApplicationContextAware接口
   //初始化时,会去调用setApplicationContext方法,其中会调用 initApplicationContext方法
	@Override
	public void initApplicationContext() throws BeansException {
		super.initApplicationContext();
		//注册好handler的相关映射
		registerHandlers(this.urlMap);
	}


	protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
		if (urlMap.isEmpty()) {
			logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
		}
		else {
		//这里注意是处理配置文件中,给出请求路径时,没加/的情况,和空格处理
			urlMap.forEach((url, handler) -> {
				// Prepend with slash if not already present.
				if (!url.startsWith("/")) {
					url = "/"   url;
				}
				// Remove whitespace from handler bean name.
				if (handler instanceof String) {
					handler = ((String) handler).trim();
				}
				//调用父类方法进行注册---注册到父类的handlerMap映射集合中去
				registerHandler(url, handler);
			});
		}
	}

}
  • 读取xml配置文件,初始化SimpleUrlHandlerMapping 时,会将我们在配置文件中设置好的请求映射放入urlMap集合中去
  • 然后再初始化initApplicationContext时,又通过registerHandler,将相关映射加入父类的handlerMap映射集合中去
  • 因为,每次请求来时,是去父类的handlerMap映射集合查找

SimpleUrlHandlerMapping还可以使用类似于ANT路径形式的模式匹配,这样我们就可以通过各种表达式,将一组或多组拥有某种相似特征的Web处理请求映射给相应的Handler处理。

代码语言:javascript复制
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/**/*hello.do">helloController</prop>
            </props>
        </property>
    </bean>

    <bean name="helloController" class="com.example.controller.HelloController">
        <property name="helloService" ref="helloService"/>
        <property name="viewName" value="hello"/>
    </bean>

当然,其实使用BeanNameUrlMapping也可以使用ANT路径匹配,只不过这里要使用ANT路径的地方,是对应控制器的beanName

代码语言:javascript复制
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
    
    <bean name="/h*h.do" class="com.example.controller.HelloController">
        <property name="helloService" ref="helloService"/>
        <property name="viewName" value="hello"/>
    </bean>

为什么,可以玩呢?

  • 其实,这和他们的父类AbstractUrlHandlerMapping中查找某个请求映射到的handler的lookupHandler方法有很大关系
  • lookupHandler是查找请求映射handler的通用父类逻辑
代码语言:javascript复制
	@Nullable
	protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
		// Direct match?
		//直接匹配--去handlerMap中,直接根据urlPath去进行精确查找
		Object handler = this.handlerMap.get(urlPath);
		if (handler != null) {
		//如果精确查找到了,那么判断此时handlerMap中找到的这个handler是不是一个字符串
		//为什么会是一个字符串呢?
		//因为,如果该handler是一个多例类型,那么每次都需要new一个新的实例,因此handlerMap保存的是该handler的beanName
		//每次通过getBean重新创建该handler
			// Bean name or resolved handler?
			if (handler instanceof String) {
				String handlerName = (String) handler;
				handler = obtainApplicationContext().getBean(handlerName);
			}
			validateHandler(handler, request);
			//以当前handler构建一个空的HandlerExecutionChain,暂时还未加入相关拦截器
			return buildPathExposingHandler(handler, urlPath, urlPath, null);
		}
        
        //采用ant模式进行路径匹配
		// Pattern match?
		List<String> matchingPatterns = new ArrayList<>();
		for (String registeredPattern : this.handlerMap.keySet()) {
		//getPathMatcher返回的就是AntPathMatcher---对注册好的请求路径与当前请求路径进行ant匹配
			if (getPathMatcher().match(registeredPattern, urlPath)) {
			//如果满足条件,则加入集合
				matchingPatterns.add(registeredPattern);
			}
			//useTrailingSlashMatch默认为false
			//即不会把"/users"当做"/users/"
			else if (useTrailingSlashMatch()) {
			//如果注册的映射路径不以/结尾,这里加上/,再进行ant匹配,判断是否满足
			//such as "/users" also matches to "/users/"
				if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern   "/", urlPath)) { 
					matchingPatterns.add(registeredPattern   "/");
				}
			}
		}

        //如果上面ant匹配存在多个matchingPattern,那么下面会挑选出一个最佳匹配
        //遵循的是精确优先
		String bestMatch = null;
		Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
		if (!matchingPatterns.isEmpty()) {
			matchingPatterns.sort(patternComparator);
			if (logger.isDebugEnabled()) {
				logger.debug("Matching patterns for request ["   urlPath   "] are "   matchingPatterns);
			}
			bestMatch = matchingPatterns.get(0);
		}
		//判断ant匹配是否可以得到一个结果
		if (bestMatch != null) {
		//如果得到了,那么拿到对应的handler
			handler = this.handlerMap.get(bestMatch);
			//如果handler为空,那么尝试取出掉bestMatch最后的/,再次进行查找匹配
			if (handler == null) {
				if (bestMatch.endsWith("/")) {
					handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
				}
				//如果还是没找到,则抛出异常
				if (handler == null) {
					throw new IllegalStateException(
							"Could not find handler for best pattern match ["   bestMatch   "]");
				}
			}
			// Bean name or resolved handler?
			//处理多例的情况
			if (handler instanceof String) {
				String handlerName = (String) handler;
				handler = obtainApplicationContext().getBean(handlerName);
			}
			validateHandler(handler, request);
			String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);

			// There might be multiple 'best patterns', let's make sure we have the correct URI template variables
			// for all of them
			Map<String, String> uriTemplateVariables = new LinkedHashMap<>();
			for (String matchingPattern : matchingPatterns) {
				if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
					Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
					Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
					uriTemplateVariables.putAll(decodedVars);
				}
			}
			if (logger.isDebugEnabled()) {
				logger.debug("URI Template variables for request ["   urlPath   "] are "   uriTemplateVariables);
			}
			return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
		}

		// No handler found...
		return null;
	}

先精确查找,再模糊匹配,如果存在多个匹配结果,那么精确优先。


HandlerMapping的执行序列(chain of handlermapping)

在基于Spirng MVC的Web应用程序中,我们可以为DispathcerServlet提供多个HandlerMapping,DispatcherServlet在选用HandlerMapping的过程中,会先对HandlerMapping集合按照优先级进行排序,然后按照依次询问每个HandlerMapping。

initHandlerMappings方法中会对HandlerMappings集合进行排序,并且该方法是在onRefresh方法中被调用的

如果某一个HandlerMapping能够返回可用的Handler,则DispathcerServlet使用当前Handler进行Web请求的处理,而不再询问其他HandlerMapping。

  • 下面给出的是DispathcerServlet中获取某个请求对应handler的方法
代码语言:javascript复制
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	//handlerMappings集合中存放的是当前捕获到可用的handlerMapping集合
		if (this.handlerMappings != null) {
		//按照优先级顺序依次遍历
			for (HandlerMapping hm : this.handlerMappings) {
				if (logger.isTraceEnabled()) {
					logger.trace(
							"Testing handler map ["   hm   "] in DispatcherServlet with name '"   getServletName()   "'");
				}
				//如果某个handlerMapping返回了值,那么直接退出循环
				HandlerExecutionChain handler = hm.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

HandlerMapping的优先级规定遵循了Spring框架内一贯的Oredered接口所规定的语义,数值越小,优先级越大,具体配置实例如下:

代码语言:javascript复制
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
        <property name="order" value="1"/>
    </bean>

亲密伙伴Controller

Controller是Spring MVC框架支持的用于处理具体Web请求的Handler类型之一。

要实现一个具体的Controller,我们可以直接实现Controller接口:

代码语言:javascript复制
@FunctionalInterface
public interface Controller {
	@Nullable
	ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
  • handleRequest方法解释: 处理请求并返回一个ModelAndView,DispathcerServlet会通过ModelAndView来进行渲染工作,如果返回null值,表示Controller内部自己完成了渲染,不需要DispathcerServlet来进行渲染

直接实现Controller接口当然没有问题,但是这需要我们去处理很多的通用细节问题,例如: 请求参数的抽取,请求编码的设定,国际化信息处理,Session数据的管理等。

而实际上,这些关注点是所有Controller都需要的,我们就应该想办法将这些通用的逻辑进行复用,这就是Spring MVC提供了一套Controller实现体系的原因。它帮助我们更好地处理了Web请求过程中的某些通用关注点。

Controller的继承体系如下:

  • 自由挥洒派的Controller: 对于该派而言,其提供的就是接近原生Servlet API服务,我们从HttpServletRequest中获取请求参数,然后验证,调用业务层逻辑,最终返回一个ModelAndView。甚至,可以直接通过HttpServletResponse输出视图。虽然很自由,但是需要处理的细节也很多,因此大家可以根据使用场景,自行判断是否需要。

Spring 4开始,对原有的Controller架构进行了大调整,大家上面看到的这些类基本都被移除了,Spring 4开始的Controller架构如下,我们讲3,再看4

  • 规范操作派的Controller: 以BASECommandController为首的规范操作派,对Web处理过程中某些通用逻辑进行了进一步的规范化封装处理,规范化的方面主要包括:
    • 自动抽取请求参数并绑定到指定的Command对象
    • 提供了统一的数据验证方式,BaseCommandController及其子类可以接收一组Validator以进行数据验证
    • 规范化了表单的请求处理流程,并对简单的多页面表单请求处理提供支持

AbstractController

AbstractControlle是整个Controller继承体系的起源,该类通过模板方法模式帮助我们解决了如下几个通用关注点:

  • 管理当前Controller所支持的请求方法类型(GET/POST)
  • 是否需要session,如果需要尝试获取,但是不会选择创建,如果不存在,则抛出异常
  • 管理页面的缓存设置,即是否允许浏览器缓存当前页面
  • 管理执行流程在会话上的同步,session mutex

我们要做的,就是在AbstractController所公开的handleRequestInternal模板方法中实现具体Wbe请求处理过程中的其他逻辑。

DispatcherServlet会去调用AbstractController实现Controller接口的handleReqeuest方法,而AbstractController为了做一些准备工作,因此需要在handleReqeuest提前做好,然后将真正请求处理过程再通过抽象方法handleRequestInternal暴露给子类去实现。

  • handleRequest是AbstractController模板方法模式的体现
代码语言:javascript复制
	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
			throws Exception {

		// Delegate to WebContentGenerator for checking and preparing.
		//做上面讲的通用关注点处理--由父类WebContentGenerator完成的
		checkAndPrepare(request, response, this instanceof LastModified);
      
		// Execute handleRequestInternal in synchronized block if required.
		//是否需要开启session同步锁机制
		if (this.synchronizeOnSession) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				Object mutex = WebUtils.getSessionMutex(session);
				synchronized (mutex) {
					return handleRequestInternal(request, response);
				}
			}
		}
        
        //进入真正处理请求的方法逻辑
		return handleRequestInternal(request, response);
	}
  • WebContentGenerator父类实现的通用关注点分离方法:
代码语言:javascript复制
	protected final void checkAndPrepare(
			HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
			throws ServletException {

		// Check whether we should support the request method.
		//是否支持当前请求方法
		String method = request.getMethod();
		if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
			throw new HttpRequestMethodNotSupportedException(
					method, StringUtils.toStringArray(this.supportedMethods));
		}

		// Check whether a session is required.
		//是否需要session
		if (this.requireSession) {
			if (request.getSession(false) == null) {
				throw new HttpSessionRequiredException("Pre-existing session required but none found");
			}
		}

		// Do declarative cache control.
		// Revalidate if the controller supports last-modified.
		//是否需要设置缓存相关响应头
		applyCacheSeconds(response, cacheSeconds, lastModified);
	}

可以看到AbstractController并没有做很多事情,因此如果我们实现AbstractController来完成业务逻辑编写,会发现还是需要关注参数抽取,数据验证等细节。


MultiActionController

该类在Spring 4中已经被移除了

MultiActionController用于对一组逻辑上相近的Web请求进行封装,例如针对同一个对象的CRUD进行封装。

而不需要像AbstaractController那样为每一个请求单独实现一个继承AbstaractController的处理类。

MultiActionController提供了以下功能:

  • 请求参数到Command对象的绑定。
  • 通过Validator的数据验证。
  • 细化的异常处理方法。

为了在MultiActionController中处理多个Web请求,我们需要定义多个Web请求处理方法,分别对应每个Web请求的处理。

这些Web请求处理方法可以定义在MultiActionController的子类中,也可以定义在某一个将来可以指定给MultiActionController的委派对象(delegate)内,但Web请求的处理方法的签名必须符合一定的要求:

代码语言:javascript复制
public (ModelAndView | Map | String | void) actionName
(HttpServletRequest request, HttpServletResponse response, [,HttpSession] [,AnyObject]);

Web请求处理方法的名称可以取任何有意义的名字,但是前面两个方法参数是必须的,第三个参数是可选的,可以是HttpSession类型也可以是Object类型,如果是Object类型,则表明对应的是Command对象,那么MultiActionController就会帮我们绑定数据并执行数据验证了。

最后一个参数可以是我们自定义的对象,只需要给其中对应的属性提供get和set方法,MultiActionController便会在参数绑定时,去请求参数中尝试将同名属性绑定到对应的对象属性上面去

方法的返回值有三种类型,分别对应如下语义:

  • 返回ModelAndView表示正常的Web处理方法,后继的ViewResolver和View处理流程,依照之前的DispathcerServlet的流程进行。
  • 返回Map表明只返回了模型数据,而没有返回逻辑视图名。这时,将寻求默认的视图名。这个工作由org.springframework.web.servlet.RequestToViewNameTranslator负责,该类能按照某种规则提供一个默认的逻辑视图名。
  • 返回void,则表明既没有返回模型数据,也没有返回逻辑视图名。这时,我们认为,当前Web请求处理方法自行处理掉了视图的渲染和输出。
  • 另外,Spring 2.5中可以返回String ,代表逻辑视图名,没有相关的模型数据。

现在还有一个问题: MultiActionController如何知道将当前请求映射到哪个方法上呢?

MethodNameResolver决定当前Web请求交给哪个方法来处理,其定义如下:

代码语言:javascript复制
public interface MethodNameResolver {
	String getHandlerMethodName(HttpServletRequest request) throws NoSuchRequestHandlingMethodException;
}

有了MethodNameResolver 之后,当请求到来时,MultiActionController只需要询问一下MethodNameResolver 需要调用哪个方法即可:

代码语言:javascript复制
	protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		try {
			String methodName = this.methodNameResolver.getHandlerMethodName(request);
			return invokeNamedMethod(methodName, request, response);
		}
		catch (NoSuchRequestHandlingMethodException ex) {
			return handleNoSuchRequestHandlingMethod(ex, request, response);
		}
	}

MethodNameResolver 为MultiActionController提供了灵活的Web请求到对应处理方法的映射策略,包括根据Web请求的URL进行映射,或者根据某个参数值进行映射等。

我们下面来看看Spring为我们提供好了哪些MethodNameResolver 实现:


MethodNameResolver

MethodNameResolver本身是一个策略接口,Spring默认提供了三个实现类:

AbstractUrlMethodNameResolver是基于请求URL到具体处理方法的抽象解析器父类,该类中有两个重要的方法:

代码语言:javascript复制
//根据请求获得对应handlerMethod的方法名
	public final String getHandlerMethodName(HttpServletRequest request)
			throws NoSuchRequestHandlingMethodException {
        //查询出当前请求对应的URL路径 
		String urlPath = this.urlPathHelper.getLookupPathForRequest(request);
		//调用子类实现的抽象方法,来获取到URL具体映射到的方法名
		String name = getHandlerMethodNameForUrlPath(urlPath);
		if (name == null) {
			throw new NoSuchRequestHandlingMethodException(urlPath, request.getMethod(), request.getParameterMap());
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Returning handler method name '"   name   "' for lookup path: "   urlPath);
		}
		return name;
	}


	protected abstract String getHandlerMethodNameForUrlPath(String urlPath);

InternalPathMethodNameResolver

如果没有为MultiActionController指定任何MethodNameResolver,那么InternalPathMethodNameResolver将作为默认的MethodNameResolver,以进行Web请求与具体处理方法间的映射解析。

InternalPathMethodNameResolver将提取URL最后一个/之后的部分并去除扩展名,作为要返回的方法名称,比如:

  • /dhy/xpy/list.do ----> list
  • /dhy/xpy/update.do ----> update
  • /dhy/xpy/delete.do ----> delete
  • /dhy/xpy/create.do ----> create

我们还可以通过prefix和suffix来为上面截取得到的methodName加上前后缀,作为最终会去查询得到的方法名:

代码语言:javascript复制
    <bean id="internalPathMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver">
        <property name="prefix" value="dhy_"/>
        <property name="suffix" value="_xpy"/>
    </bean>

如果加上了前后缀限制,那么上面的请求URL最终映射得到的方法名如下:

  • /dhy/xpy/list.do ----> dhy_list_xpy
  • /dhy/xpy/update.do ----> dhy_update_xpy
  • /dhy/xpy/delete.do ----> dhy_delete_xpy
  • /dhy/xpy/create.do ----> dhy_create_xpy

InternalPathMethodNameResolver的源码实现也非常简单:

代码语言:javascript复制
public class InternalPathMethodNameResolver extends AbstractUrlMethodNameResolver {

	private String prefix = "";

	private String suffix = "";

	private final Map<String, String> methodNameCache = new ConcurrentHashMap<String, String>(16);

    //....省略get和set方法
     
    //根据URL路径去获取到对应的handlerName 
	@Override
	protected String getHandlerMethodNameForUrlPath(String urlPath) {
		//首选查询缓存
		String methodName = this.methodNameCache.get(urlPath);
		if (methodName == null) {
		    //提取URL最后一个/之后的部分并去除扩展名,作为要返回的方法名称
			methodName = extractHandlerMethodNameFromUrlPath(urlPath);
			//加上前后缀
			methodName = postProcessHandlerMethodName(methodName);
			this.methodNameCache.put(urlPath, methodName);
		}
		//返回
		return methodName;
	}


	protected String extractHandlerMethodNameFromUrlPath(String uri) {
		return WebUtils.extractFilenameFromUrlPath(uri);
	}

	protected String postProcessHandlerMethodName(String methodName) {
		return getPrefix()   methodName   getSuffix();
	}
}

PropertiesMethodNameResolver

PropertiesMethodNameResolver与InternalPathMethodNameResolver唯一的相同点在于,他们都是基于请求的URL进行映射(因为都继承了AbstractUrlMethodNameResolver)。

但它比InternalPathMethodNameResolver灵活,如果HandlerMapping与MethodNameResolver都是处理映射这一点来看,InternalPathMethodNameResolver相当于BeanNameURLHandlerMapping,而PropertiesMethodNameResolver相当于SimpleUrlHandlerMapping.

PropertiesMethodNameResolver可以指定完全匹配的映射关系,或者使用ANT形式的路径匹配模式所表达的映射关系。

例如:

代码语言:javascript复制
    <bean id="propertiesMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
        <property name="mappings">
            <value>
                /list.do=list
                /update=update
                /list*=list
            </value>
        </property>
    </bean>

PropertiesMethodNameResolver的源码也非常简单:

代码语言:javascript复制
public class PropertiesMethodNameResolver extends AbstractUrlMethodNameResolver
		implements InitializingBean {

	private Properties mappings;

	private PathMatcher pathMatcher = new AntPathMatcher();

    ... 

	@Override
	protected String getHandlerMethodNameForUrlPath(String urlPath) {
	//先进行精确匹配
		String methodName = this.mappings.getProperty(urlPath);
		if (methodName != null) {
			return methodName;
		}
   //如果精确匹配没找到,再进行模糊匹配		
		Enumeration propNames = this.mappings.propertyNames();
		while (propNames.hasMoreElements()) {
			String registeredPath = (String) propNames.nextElement();
			//这里按顺序挨个匹配,当遇到第一个匹配成功,则直接返回
			//因此不会进行最佳匹配
			if (this.pathMatcher.match(registeredPath, urlPath)) {
				return (String) this.mappings.get(registeredPath);
			}
		}
		return null;
	}

}

ParameterMethodNameResolver

ParameterMethodNameResolver允许我们根据请求中的某个参数的值作为映射的方法名,也允许我们使用请求中的一组参数来处理映射的方法名称.

  • 下面是两种策略的详细情况

(1) 根据请求中的某个参数的值作为映射后的方法名。在Web请求提交之后,我们可以附带一个参数,专门指定由MultiActionController中的那个方法来处理当前请求

ParameterMethodNameResolver默认检测的参数名称为action,我们也可以通过setParamName方法来修改默认的参数名称。

代码语言:javascript复制
     <bean class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
         <property name="paramName" value="methodName"/>
     </bean>

那么,HTTP GET形式发送的URL看起来就类似于: http://host:port//dhy/xpy?methodName=list.

(2) 根据请求中的一组参数作为映射后的方法名,在某个页面中存在多种行为选择的时候,可以让每一种行为对应一个参数。

我们通过methodParamNames属性为ParameterMethodNameResolver指定一组要检测的参数名。

ParameterMethodNameResolver将以指定的一组参数名作为基准,对Web请求中的参数进行检测。

如果发现存在其中某个参数,则将当前Web请求映射到与参数相同名称的处理方法。

我们还可以通过指定defaultMethodName,来提供一个兜底方法,该方法会在请求无法找到合适处理方法的时候,作为默认方法进行处理

代码语言:javascript复制
     <bean class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
         <property name="paramName" value="methodName"/>
         <property name="methodParamNames" value="list,update,delete"/>
         <property name="defaultMethodName" value="list"/>
     </bean>

如果像上面一样,同时指定paramName和methodParamNames,那么谁的优先级高呢?

最简单的策略是楼一眼ParameterMethodNameResolver的源码:

代码语言:javascript复制
public class ParameterMethodNameResolver implements MethodNameResolver {

	/**
	 默认的映射请求参数名
	 */
	public static final String DEFAULT_PARAM_NAME = "action";

	private String paramName = DEFAULT_PARAM_NAME;

	private String[] methodParamNames;
     
    //逻辑映射--下面源码中会体现出它的作用 
	private Properties logicalMappings;

	private String defaultMethodName;
    
    ...

  
	public String getHandlerMethodName(HttpServletRequest request) throws NoSuchRequestHandlingMethodException {
		String methodName = null;

		//首先处理methodParamNames 
		if (this.methodParamNames != null) {
			for (String candidate : this.methodParamNames) {
			//按顺序挨个匹配,如果有一个匹配上了,直接返回
				if (WebUtils.hasSubmitParameter(request, candidate)) {
				//这里是直接将methodParamNames数组中当前元素作为方法名
					methodName = candidate;
					if (logger.isDebugEnabled()) {
						logger.debug("Determined handler method '"   methodName  
								"' based on existence of explicit request parameter of same name");
					}
					break;
				}
			}
		}


		//如果methodParamNames没有匹配上,那么再对paramName进行处理
		if (methodName == null && this.paramName != null) {
		//这里是取出请求参数中的值作为方法名
			methodName = request.getParameter(this.paramName);
			if (methodName != null) {
				if (logger.isDebugEnabled()) {
					logger.debug("Determined handler method '"   methodName  
							"' based on value of request parameter '"   this.paramName   "'");
				}
			}
		}

        //如果上面经过上面处理后,匹配上了,那么下面会把当前methodName作为一个逻辑值,去logicalMappings找到其真正的值
        //前提是设置了logicalMappings ---相当于又提供了一层映射,感觉没啥用
		if (methodName != null && this.logicalMappings != null) {
			// Resolve logical name into real method name, if appropriate.
			String originalName = methodName;
			//第二个参数是如果找不到,默认值是啥
			methodName = this.logicalMappings.getProperty(methodName, methodName);
			if (logger.isDebugEnabled()) {
				logger.debug("Resolved method name '"   originalName   "' to handler method '"   methodName   "'");
			}
		}
       
       //防止上面得到的methodName 是空串
		if (methodName != null && !StringUtils.hasText(methodName)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Method name '"   methodName   "' is empty: treating it as no method name found");
			}
			methodName = null;
		}
        
        //如果一番折腾后,没得到,那么使用默认的defaultMethodName
		if (methodName == null) {
			if (this.defaultMethodName != null) {
				// No specific method resolved: use default method.
				methodName = this.defaultMethodName;
				if (logger.isDebugEnabled()) {
					logger.debug("Falling back to default handler method '"   this.defaultMethodName   "'");
				}
			}
			else {
				// If resolution failed completely, throw an exception.
				throw new NoSuchRequestHandlingMethodException(request);
			}
		}

		return methodName;
	}

}

MultiActionController使用演示

这里我们实现一个Stu的CRUD:

  • 先准备一个首页,当我们点击首页跳转按钮时,会跳转到学生管理界面,可以进行增删改查
代码语言:javascript复制
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<br>
<h1>大忽悠首页</h1></br></br></br></br>

<a href="/stu">点击跳转到学生信息管理界面</a>

</body>
</html>
  • 准备相关增删改查界面
  • 构建MultiActionController,使用MultiActionController进行一组Web请求的处理有两种实现方式:
    • 继承MultiActionController
    • 为MultiActionController提供一个委派对象

为MultiActionController提供一个委派对象的好处在于,委派对象不需要继承任何父类或者接口。因此这里采用委派对象。

并且使用ParamterMethodNameResolver对请求方法进行映射,具体来说,就是为methodParamNames指定insert,update,delete作为要映射的参数,同时以list作为默认的方法:

代码语言:javascript复制
     <bean id="parameterMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
         <property name="methodParamNames" value="insert,update,delete"/>
         <property name="defaultMethodName" value="list"/>
     </bean>

这样的话,我们的委派对象就需要实现list,create,update,delete对应名称的方法:

代码语言:javascript复制
@Data
public class StuController{
    private StuService stuService;
    private String listViewName;
    private String updateViewName;
    private String insertViewName;
    private String deleteViewName;

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView(listViewName,"stuInfoList",stuService.list());
    }

    public ModelAndView insert(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView(insertViewName);
    }

    public ModelAndView update(HttpServletRequest request, HttpServletResponse response){
        stuService.update(Integer.valueOf(request.getParameter("sno")),request.getParameter("sname"));
        return new ModelAndView(updateViewName);
    }

    public ModelAndView delete(HttpServletRequest request, HttpServletResponse response, Stu stu){
        stuService.delete(stu.getNum());
        return new ModelAndView(deleteViewName);
    }
}
  • 最后就是完整的配置文件了—记住是[servletName]-servlet.xml配置文件,service要在application.xml中进入注入
代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

     <bean id="parameterMethodNameResolver" class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver">
         <property name="methodParamNames" value="insert,update,delete"/>
         <property name="defaultMethodName" value="list"/>
     </bean>

    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/**/stu">multiActionController</prop>
            </props>
        </property>
    </bean>

    <bean id="multiActionController" class="org.springframework.web.servlet.mvc.multiaction.MultiActionController">
              <property name="delegate" ref="stuController"/>
              <property name="methodNameResolver" ref="parameterMethodNameResolver"/>
    </bean>

    <bean id="stuController" class="com.example.controller.StuController">
        <property name="stuService" ref="stuService"/>
        <property name="listViewName" value="list"/>
        <property name="updateViewName" value="update"/>
        <property name="insertViewName" value="insert"/>
        <property name="deleteViewName" value="delete"/>
    </bean>
</beans>

通过multiActionController的delegate属性,我们指定了它要使用的委派对象,multiActionController内部将使用反射机制调用相应MethodNameResolver所返回的处理方法。

指定的逻辑视图名可以添加相应的前缀(prefix),比如: redirect:viewName,将以重定向的形式跳转到相应的视图,有关视图的转发和重定向将在后面ViewResolver和View部分进行介绍。

注意,需要将web.xml中DispathcerServlet的拦截路径修改为/

代码语言:javascript复制
   <servlet>
       <servlet-name>controller</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <load-on-startup>2</load-on-startup>
   </servlet>


    <servlet-mapping>
        <servlet-name>controller</servlet-name>
<!--   拦截除*.jsp以外的所有请求     -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
  • 完整流程和下面这幅图画的一样

MultiActionController源码剖析
  • 首先是MultiActionController对代理对象的设置,有三种方式
代码语言:javascript复制
   //无参构造,那么代理对象就是自身,即我们采用的方法是继承MultiActionController类
	public MultiActionController() {
		this.delegate = this;
		//将当前类包括其父类内部所有public方法进行注册
		registerHandlerMethods(this.delegate);
	}
    
    //有参构造传入代理对象
   	public MultiActionController(Object delegate) {
		setDelegate(delegate);
	}
    
    //配置文件中指定属性注入
    public final void setDelegate(Object delegate) {
		Assert.notNull(delegate, "Delegate must not be null");
		this.delegate = delegate;
		//注册代理对象内部的方法
		registerHandlerMethods(this.delegate);
		// There must be SOME handler methods.
		if (this.handlerMethodMap.isEmpty()) {
			throw new IllegalStateException("No handler methods in class ["   this.delegate.getClass()   "]");
		}
	} 
  • 注册代理对象内部方法的具体细节
代码语言:javascript复制
	private void registerHandlerMethods(Object delegate) {
	    //清空
		this.handlerMethodMap.clear();
		this.lastModifiedMethodMap.clear();
		this.exceptionHandlerMap.clear();

		// Look at all methods in the subclass, trying to find
		// methods that are validators according to our criteria
		//先获取代理类及其父类中所有的public包括protected方法
		Method[] methods = delegate.getClass().getMethods();
		for (Method method : methods) {
			// 判断该方法是否是用于异常兜底处理方法
			if (isExceptionHandlerMethod(method)) {
			//如果是的话,就进行注册
				registerExceptionHandlerMethod(method);
			}
			//否则判断是否是普通处理方法
			else if (isHandlerMethod(method)) {
			//是的话就注册
				registerHandlerMethod(method);
				registerLastModifiedMethodIfExists(delegate, method);
			}
		}
	}

上面提到了异常兜底处理方法,下面简单介绍一下使用

  • 如果代理对象中提供了如下格式的方法签名,则会被当做异常兜底处理方法加入异常方法映射集合中去,当代理对象方法执行出现异常时,会去寻找对应能处理该异常的异常兜底方法来处理异常
代码语言:javascript复制
//方法名任意,最后一个参数表示当前方法用来处理哪种类型的异常
public ModelAndView anyMeaningfulName
(HttpServletRequest request, HttpServletResponse response, ExceptionClass exception);
  • 对上面举出的例子进行简单的修改
代码语言:javascript复制
    public ModelAndView list(HttpServletRequest request, HttpServletResponse response) throws IllegalArgumentException {
        if(true){
            throw new IllegalArgumentException("测试兜底异常捕获");
        }
        return new ModelAndView(listViewName,"stuInfoList",stuService.list());
    }

    public ModelAndView catchIllegalArgumentException(HttpServletRequest request, HttpServletResponse response,IllegalArgumentException exception){
        return new ModelAndView("error","ex",exception.getMessage());
    }

  • 下面先来看看异常方法的判定和注册过程
代码语言:javascript复制
	private boolean isExceptionHandlerMethod(Method method) {
	//首先需要满足普通handlerMethod的格式
		return (isHandlerMethod(method) &&
		//方法参数正好等于三个
				method.getParameterTypes().length == 3 &&
	   //最后一个参数必须继承至Throwable
				Throwable.class.isAssignableFrom(method.getParameterTypes()[2]));
	}
代码语言:javascript复制
	private void registerExceptionHandlerMethod(Method method) {
		//加入exceptionHandlerMap,key是异常类型,val是对应异常兜底方法
		this.exceptionHandlerMap.put(method.getParameterTypes()[2], method);
		if (logger.isDebugEnabled()) {
			logger.debug("Found exception handler method ["   method   "]");
		}
	}
  • 再来看看普通handlerMethod的判定和注册过程
代码语言:javascript复制
	private boolean isHandlerMethod(Method method) {
	//首先判断返回结果是否是下面规定的四种之一
		Class returnType = method.getReturnType();
		if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) ||
				void.class.equals(returnType)) {
				//再对参数值进行判断
			Class[] parameterTypes = method.getParameterTypes();
			//必须大于等于2,并且前面两个参数类型固定
			return (parameterTypes.length >= 2 &&
					HttpServletRequest.class.equals(parameterTypes[0]) &&
					HttpServletResponse.class.equals(parameterTypes[1]) &&
					//下面两个条件不能同时满足,否则说明是继承至Controller的方法
					!("handleRequest".equals(method.getName()) && parameterTypes.length == 2));
		}
		return false;
	}
代码语言:javascript复制
	private void registerHandlerMethod(Method method) {
		if (logger.isDebugEnabled()) {
			logger.debug("Found action method ["   method   "]");
		}
		//加入handlerMethodMap集合,key是方法名,val是对应的方法
		this.handlerMethodMap.put(method.getName(), method);
	}

  • 上面在MultiActionController初始化过程中,完成了相关方法的搜集和整理,下面进入正式请求接收处理过程

因为继承了AbstractController,因此需要重写handleRequestInternal方法

代码语言:javascript复制
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
			throws Exception {
		try {
		    //方法解析器解析当前请求得到对应的方法名
			String methodName = this.methodNameResolver.getHandlerMethodName(request);
			//去调用代理对象的methodName 
			return invokeNamedMethod(methodName, request, response);
		}
		catch (NoSuchRequestHandlingMethodException ex) {
			return handleNoSuchRequestHandlingMethod(ex, request, response);
		}
	}
  • 下面进入了代理类目标方法调用的过程
代码语言:javascript复制
	protected final ModelAndView invokeNamedMethod(
			String methodName, HttpServletRequest request, HttpServletResponse response) throws Exception {
       //根据methodName从handlerMethodMap中得到对应的方法
		Method method = this.handlerMethodMap.get(methodName);
		//找不到,抛出异常
		if (method == null) {
			throw new NoSuchRequestHandlingMethodException(methodName, getClass());
		}

         //参数解析
		try {
		   //首先获取到当前方法的参数列表
			Class[] paramTypes = method.getParameterTypes();
		  //params参数数组大小为4,说明参数个数最多只有四个
			List<Object> params = new ArrayList<Object>(4);
	     //	前面两个参数的位置和类型固定死了	
			params.add(request);
			params.add(response);

        //如果参数个数大于等于3个,并且第三个参数为HttpSession,那么设置进去
			if (paramTypes.length >= 3 && paramTypes[2].equals(HttpSession.class)) {
				HttpSession session = request.getSession(false);
				if (session == null) {
					throw new HttpSessionRequiredException(
							"Pre-existing session required for handler method '"   methodName   "'");
				}
				params.add(session);
			}

			// 如果参数类型大于等于3个,并且最后一个参数不是HttpSession类型,这里可能是三个或者四个参数,
         //如果是四个参数,那么第三个参数必须是HttpSession类型
			if (paramTypes.length >= 3 &&
					!paramTypes[paramTypes.length - 1].equals(HttpSession.class)) {
			   //实例化该对象,这里要求我们给出的自定义对象,必须要有无参构造才行
			   //因为这里是用无参构造实例化的对象		
				Object command = newCommandObject(paramTypes[paramTypes.length - 1]);
				//加入参数集合
				params.add(command);
				//进行参数绑定
				bind(request, command);
			}
            //调用代理对象的目标方法
			Object returnValue = method.invoke(this.delegate, params.toArray(new Object[params.size()]));
			//将返回值通通进行包装
			return massageReturnValueIfNecessary(returnValue);
		}
		//如果出现异常,那么需要对异常进行处理
		catch (InvocationTargetException ex) {
			// The handler method threw an exception.
			return handleException(request, response, ex.getTargetException());
		}
		catch (Exception ex) {
			// The binding process threw an exception.
			return handleException(request, response, ex);
		}
	}

  • 下面再对bind参数绑定过程进行简单的介绍
代码语言:javascript复制
	protected void bind(HttpServletRequest request, Object command) throws Exception {
		logger.debug("Binding request parameters onto MultiActionController command");
		//创建请求数据绑定器
		ServletRequestDataBinder binder = createBinder(request, command);
		//进行数据绑定
		binder.bind(request);
		//validators进行校验
		if (this.validators != null) {
			for (Validator validator : this.validators) {
				if (validator.supports(command.getClass())) {
					ValidationUtils.invokeValidator(validator, command, binder.getBindingResult());
				}
			}
		}
		//对数据校验过程发生的异常进行处理
		binder.closeNoCatch();
	}

数据绑定过程简单理解就是按照请求参数名和对象属性名进行匹配,如果匹配成功,就设置进去。

validators校验这里暂时不展开,后面会讲。


  • massageReturnValueIfNecessary对返回值进行处理
  • 如果返回ModelAndView,那么直接返回即可
  • 如果返回map,则将map作为附加属性返回,视图名会通过RequestToViewNameTranslator 选择默认的视图名
  • 如果返回String,则作为视图名
  • 如果返回null,则表示具体的渲染逻辑在controller内部完成
代码语言:javascript复制
	private ModelAndView massageReturnValueIfNecessary(Object returnValue) {
		if (returnValue instanceof ModelAndView) {
			return (ModelAndView) returnValue;
		}
		else if (returnValue instanceof Map) {
			return new ModelAndView().addAllObjects((Map) returnValue);
		}
		else if (returnValue instanceof String) {
			return new ModelAndView((String) returnValue);
		}
		else {
			// Either returned null or was 'void' return.
			// We'll assume that the handle method already wrote the response.
			return null;
		}
	}

  • 如果代理对象目标方法调用过程中出现异常了呢?
代码语言:javascript复制
	private ModelAndView handleException(HttpServletRequest request, HttpServletResponse response, Throwable ex)
			throws Exception {
        //根据异常类型,去exceptionHandlerMap中取出对应处理该异常的方法
		Method handler = getExceptionHandler(ex);
		//如果不为空
		if (handler != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Invoking exception handler ["   handler   "] for exception: "   ex);
			}
			try {
			//则调用对应异常兜底方法,然后处理其返回值
				Object returnValue = handler.invoke(this.delegate, request, response, ex);
				return massageReturnValueIfNecessary(returnValue);
			}
			catch (InvocationTargetException ex2) {
				logger.error("Original exception overridden by exception handling failure", ex);
				ReflectionUtils.rethrowException(ex2.getTargetException());
			}
			catch (Exception ex2) {
				logger.error("Failed to invoke exception handler method", ex2);
			}
		}
		else {
		//如果代理类目标方法执行过程抛出了异常,但是我们没有指定相关异常兜底方法,则抛出该异常
			// If we get here, there was no custom handler or we couldn't invoke it.
			ReflectionUtils.rethrowException(ex);
		}
		throw new IllegalStateException("Should never get here");
	}

getExceptionHandler: 根据异常类型去exceptionHandlerMap获取对应的兜底方法

代码语言:javascript复制
	protected Method getExceptionHandler(Throwable exception) {
		Class exceptionClass = exception.getClass();
		if (logger.isDebugEnabled()) {
			logger.debug("Trying to find handler for exception class ["   exceptionClass.getName()   "]");
		}
		Method handler = this.exceptionHandlerMap.get(exceptionClass);
		//如果当前异常类型没匹配上,那么就调用抛出异常的父类型不断去匹配
		while (handler == null && !exceptionClass.equals(Throwable.class)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Trying to find handler for exception superclass ["   exceptionClass.getName()   "]");
			}
			exceptionClass = exceptionClass.getSuperclass();
			handler = this.exceptionHandlerMap.get(exceptionClass);
		}
		return handler;
	}

小结

本节主要对HandlerMapping和Controller相关类进行了简单介绍,下节,我们将继续探寻controller的更多细节实现,和整个DispathcerServlet执行流程

0 人点赞