20_1 Spring MVC web系列 - MVC工作原理

20_1 Spring MVC web系列 - MVC工作原理

关于Spring MVC的使用可以查看这里。这里对Spring MVC的执行流程做描述。

一、Spring MVC核心架构

image.png

如上图所示是Spring MVC核心架构的具体流程步骤:

  1. 用户发起请求-->DispatcherServlet,前端控制器(DispatcherServlet)收到请求后自己不处理,而是委托给其他的解析器进行处理。而DispatcherServlet作为统一访问点,进行全局的流程控制
  2. DispatcherServlet-->HandlerMapping,前端控制器请求处理映射器(HandlerMapping)去查找Handler。HandlerMapping会把请求映射为HandlerExecutionChain对象,通过这种策略,可以很容易添加新的映射策略。
    1. HandlerExecutionChain包含一个Handler处理器(页面控制器)对象和多个HandlerInterceptor拦截器对象
  3. DispatcherServlet-->HandlerAdapter,前端控制器请求处理器适配器(HandlerAdapter)。HandlerAdapter会把处理器包装为适配器,从而支持多种类型处理器。即适配器设计模式的应用,很容易支持很多类型的处理器。
  4. HandlerAdapter-->处理器功能处理方法的调用,HandlerAdapter去执行相应的Handler(也就是Controller)。HandlerAdapter将会根据适配的结果调用真正的处理器的功能处理方法,完成功能处理;并返回一个ModelAndView对象(包含模型数据、逻辑视图名)
  5. ModelAndView的逻辑视图名-->ViewResolver,ViewResolver将把逻辑视图名解析为具体的View,通过这种策略,很容易更换其他视图技术
  6. View-->渲染,View会根据传过来的Model模型数据进行渲染,此处的Model实际上是一个Map数据结构,因此很容易支持其他视图技术
  7. 返回控制权给DispatcherServlet,由DispatcherServlet返回响应给用户,到此一个流程结束

流程中出现的各个组件的功能说明如下:

  • 前端控制器(DispatcherServlet):其作用是接收用户请求,然后给用户反馈结果。它的作用相当于一个转发器或中央处理器,控制整个流程的执行,对各个组件进行统一调度,以降低组件之间的耦合性,有利于组件之间的拓展
  • 处理器映射器(HandlerMapping):其作用是根据请求的URL路径,通过注解或者XML配置,寻找匹配的处理器(Handler)信息
  • 处理器适配器(HandlerAdapter):其作用是根据映射器找到的处理器(Handler)信息,按照特定规则执行相关的处理器(Handler)
  • 处理器(Handler):其作用是执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装至ModelAndView对象中
  • 视图解析器(ViewResolver):其作用是进行解析操作,通过ModelAndView对象中的View信息将逻辑视图名解析成真正的视图View(如通过一个JSP路径返回一个真正的JSP页面、或者通过一个Html路径返回一个真正的Html页面)
  • 视图(View):其本身是一个接口,实现类支持不通的View类型(JSP、FreeMarker、Excel等)

二、Spring MVC工作原理源码

2.1 从HttpServlet的doXxx()方法说起

image.png
我们知道HttpServlet提供了一系列的doXxx()方法,来接收处理。Spring MVC是Spring 提供的一个基于MVC来处理web请求的模块框架。那它自然和HttpServlet就有关系了,在Spring MVC中主要是由DispatcherServlet来做控制处理。

  • HttpServletBean是HttpServlet的子类,主要做一些初始化的工作,将web.xml中配置的参数设置到Servlet中。比如servlet标签的子标签init-param标签中配置的参数。
  • FrameworkServlet是HttpServlet的子类,重写了一系列doXxx方法,并调用内部processRequest()方法。将Servlet与Spring容器上下文关联。其实也就是初始化FrameworkServlet的属性webApplicationContext,这个属性代表SpringMVC上下文,它有个父类上下文,既web.xml中配置的ContextLoaderListener监听器初始化的容器上下文
  • FrameworkServlet的processRequest()方法里,会调用抽象方法doService()方法
  • DispatcherServlet是FrameworkServlet的子类,实现了doService()方法,这个方法就是前端控制器DispatcherServlet的入口。初始化各个功能的实现类。比如异常处理、视图处理、请求映射处理等。

2.2 DispatcherServlet调用流程

image.png
整个流程由DispatcherServlet前端控制器来控制,起点在DispatcherServlet的doService方法。
工作流程步骤如下:

  1. doService方法里调用doDispatch方法。doService方法是进行分发的入口,在该方法里实质去做分发的是doDispatch方法
  2. doDispatch-->getHandler。doDispatch方法里会调用getHandler方法,来获取HandlerExecutionChain
  3. DispatcherServlet里的getHandler里,会通过HandlerMapping的getHandler方法获取HandlerExecutionChain
  4. doDispatch-->getHandlerAdapter。doDispatch方法里会调用getHandlerAdapter方法,来获取HandlerAdapter
  5. doDispatch里调用HandlerExecutionChain的applyPreHandle方法
  6. 在applyPreHandle方法里会遍历拦截器,并执行拦截器的preHandle方法
  7. doDispatch里调用HandlerAdapter的handle方法,获取ModelAndView
  8. 在HandlerAdapter的handle方法里,会去调用Handler,也就是Controller得到ModelAndView
  9. doDispatch里调用HandlerExecutionChain的applyPostHandle方法
  10. 在applyPostHandle方法方法里会遍历拦截器,并执行拦截器的postHandle方法
  11. doDispatch里调用processDispatchResult,完成最后的渲染和拦截器的结束方法afterCompletion
  12. 在processDispatchResult方法里调用render方法,进行视图渲染

三、Spring MVC的工作机制

有了上面的基础印象,那我们就知道SpringMVC框架其实围绕的都是 DispatcherServlet 来工作。它就是一个另类的Servlet,我们知道Servlet是可以拦截到HTTP发送过来的请求。

在Spring MVC中,Servlet在初始化的时候即调用init方法的时候,Spring MVC会根据配置,来获取配置信息,从而来获得URI和处理器Handler之间的映射关系,而这个URI是统一资源标识符。为了更加灵活的操作和增强某些我们所需要的功能,这时候,SpringMVC还会给处理器加入拦截器。

3.1 url和controller的对应关系的建立

SpringMVC的容器初始化的时候,会建立所有url和controller的对应关系。在ApplicationObjectSupport里:
ApplicationObjectSupport的setApplicationContext方法

@Override
	public final void setApplicationContext(@Nullable ApplicationContext context) throws BeansException {
		if (context == null && !isContextRequired()) {
			// Reset internal context state.
			this.applicationContext = null;
			this.messageSourceAccessor = null;
		}
		else if (this.applicationContext == null) {
			// Initialize with passed-in context.
			if (!requiredContextClass().isInstance(context)) {
				throw new ApplicationContextException(
						"Invalid application context: needs to be of type [" + requiredContextClass().getName() + "]");
			}
			this.applicationContext = context;
			this.messageSourceAccessor = new MessageSourceAccessor(context);
			initApplicationContext(context);
		}
		else {
			// Ignore reinitialization if same context passed in.
			if (this.applicationContext != context) {
				throw new ApplicationContextException(
						"Cannot reinitialize with different application context: current one is [" +
						this.applicationContext + "], passed-in one is [" + context + "]");
			}
		}
	}
  • 这个方法里关键的就是initApplicationContext这个方法。这个方法的实际实现是由子类AbstractDetectingUrlHandlerMapping实现了该方法。

AbstractDetectingUrlHandlerMapping的detectHandlers方法

	protected void detectHandlers() throws BeansException {
		ApplicationContext applicationContext = obtainApplicationContext();
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for URL mappings in application context: " + applicationContext);
		}
		String[] beanNames = (this.detectHandlersInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
				applicationContext.getBeanNamesForType(Object.class));

		// 采取任何bean的名字,我们可以确定url。.
		for (String beanName : beanNames) {
			String[] urls = determineUrlsForHandler(beanName);
			if (!ObjectUtils.isEmpty(urls)) {
				// URL路径发现:我们认为这是一个处理程序 这时候就要保存urls和beanName的对应关系,
				registerHandler(urls, beanName);
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug("Rejected bean name '" + beanName + "': no URL paths identified");
				}
			}
		}
	}

而我们在使用SpringMVC的Controller里面的注解解析 Url 的时候,通过的是什么类?什么方法呢?就是接下来的这个方法determineUrlsForHandler。这个方法也是一个抽象方法,真正的实现是由子类BeanNameUrlHandlerMapping来实现的。
BeanNameUrlHandlerMapping的determineUrlsForHandler

	/**
	 * Checks name and aliases of the given bean for URLs, starting with "/".
	 */
	@Override
	protected String[] determineUrlsForHandler(String beanName) {
		List<String> urls = new ArrayList<>();
		if (beanName.startsWith("/")) {
			urls.add(beanName);
		}
		String[] aliases = obtainApplicationContext().getAliases(beanName);
		for (String alias : aliases) {
			if (alias.startsWith("/")) {
				urls.add(alias);
			}
		}
		return StringUtils.toStringArray(urls);
	}

在我们日常写 CRUD 的建立Controller的时候,在上面总是习惯的@RequestMapping注解,里面写我们从前端的ajax或者其他方式请求过来的路径,通过这个方法来进行Controller和url之间的对应关系。这时候关系完成了,接下来就是是根据url去找Controller了。

posted @ 2020-09-10 20:33  在线打工者  阅读(187)  评论(0编辑  收藏  举报