从源码入手分析SpringMVC的原理

栏目: Java · 发布时间: 5年前

内容简介:SpringMVC作为控制层框架,具体的作用就不在此赘述了,本文主要针对其处理请求流程的原理来做一次较为细致的讲解首先来看经典的MVC的三层架构,下面是一个模拟请求的调用和返回,我们把重点放在中间的控制层:SpringMVC就是用于管理这一层,负责将视图层发来的请求发送给下一层,然后再将结果返回,听起来很简单,但是真的这么简单吗?接下来,我们就来渐进式的分析,来研究其工作的原理

SpringMVC作为控制层框架,具体的作用就不在此赘述了,本文主要针对其处理请求流程的原理来做一次较为细致的讲解

首先来看经典的MVC的三层架构,下面是一个模拟请求的调用和返回,我们把重点放在中间的控制层:

从源码入手分析SpringMVC的原理

SpringMVC就是用于管理这一层,负责将视图层发来的请求发送给下一层,然后再将结果返回,听起来很简单,但是真的这么简单吗?接下来,我们就来渐进式的分析,来研究其工作的原理

准备工作

首先我们选用的是springboot的2.1.4发布版,查看一下依赖树,发现springmvc的版本是5.1.6的版本,正好可以和现在网络上大部分针对3.x版本的源码讲解做一个比较

从源码入手分析SpringMVC的原理

这里为了不做多余的功夫,一切从简,就只添加了以下的类:

从源码入手分析SpringMVC的原理

具体的类信息如下

@RestController
public class DemoController {

    private final DemoService demoService;

    public DemoController(DemoService demoService) {
        this.demoService = demoService;
    }

    @RequestMapping("hi")
    public String sayHi() {
        return demoService.getInfo();
    }
}
复制代码
@Service
public class DemoService {

    public String getInfo() {
        return "Hello World...";
    }
}
复制代码

我们启动测试一下:

从源码入手分析SpringMVC的原理

一切正常,接下来我们就要开始着手分析

工作原理

从刚才的测试我们会发现,我们发送了一个请求(url)给springmvc,然后springmvc就执行了我们定义的sayHi()方法,接着我们的浏览器就收到了响应,这个过程究竟是怎么实现的呢?别急,我们在sayHi()方法处打上断点,进入调试模式

我们依然发起 localhost:8080/hi 这个请求,程序停在了断点位置,同时也得到了一条调用链。我们沿着调用链从启动位置向上找,我们忽略所有core包下的内容,只找web包下的类,发现请求经过HttpServlet层层封装,最终交给了一个叫DispatcherServlet的类来处理

DispatcherServlet -- doService()

这个DispatcherServlet内部有一个doService方法,我们就以此作为起点,来分析这个请求的过程是怎么样的

protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
		logRequest(request);

		// 保留属性快照,以便恢复
		Map<String, Object> attributesSnapshot = null;
		if (WebUtils.isIncludeRequest(request)) {
			attributesSnapshot = new HashMap<>();
			Enumeration<?> attrNames = request.getAttributeNames();
			while (attrNames.hasMoreElements()) {
				String attrName = (String) attrNames.nextElement();
				if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
					attributesSnapshot.put(attrName, request.getAttribute(attrName));
				}
			}
		}

		// 设置属性,使之可供处理程序和视图对象使用
		request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
		request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
		request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
		request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

		if (this.flashMapManager != null) {
			FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
			if (inputFlashMap != null) {
				request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
			}
			request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
			request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
		}

		try {
			// 执行具体的分发操作
			doDispatch(request, response);
		}
		finally {
			if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
				// 如果是include请求,通过之间的快照来恢复属性
				if (attributesSnapshot != null) {
					restoreAttributesAfterInclude(request, attributesSnapshot);
				}
			}
		}
	}
复制代码

整个操作具体可分为4个部分:

  • 保存快照
  • 设置属性
  • 执行操作
  • 恢复属性(如果是include请求)

这里先解释一下什么是include请求,include请求指在一个Servlet请求中包含了另一个请求,清楚了这一点,我们接着看处理流程,核心方法就是一个doDispatch方法,我们直接继续进入该方法

DispatcherServlet -- doDispatch()

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;
		// 异步请求处理的接口
		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			// 模型与视图
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				// 将请求转化为多部分请求,如果没设置多部分解析,就使用原有的请求
				processedRequest = checkMultipart(request);
				// true表示开启了多部分解析
				multipartRequestParsed = (processedRequest != request);

				// 来确定具体的处理程序,返回结果是一个执行链
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// 来确定处理器适配器
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// 检查最后修改的标头
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					// 根据上次修改的时间戳来判断资源是否已被修改,如果资源未被修改,则直接返回,浏览器将使用缓存
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				// 执行前置拦截方法
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// 触发具体的逻辑方法
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				// 返回true表示正在进行并发处理,应保持响应打开状态
				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				// 执行后置处理
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			// 处理最后的结果
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			// 处理多部分请求
			if (asyncManager.isConcurrentHandlingStarted()) {
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}
复制代码

这个方法就是处理具体的分发流程,也就是说将我们的url对应到具体的controller中的方法中,具体的执行流程就在上面,对一些重要的步骤我已经标注了注释,这里抛出掉多部分请求处理和异常捕获的部分,整个执行流程可以分为以下步骤:

  1. 获取请求对应的执行链
  2. 根据执行链的处理器来寻找能够适配该处理器的适配器
  3. 如果是Get请求,检查资源最后修改的时间,如果资源的最后修改时间早于请求发出的时间,则直接返回,让浏览器使用缓存
  4. 执行前置拦截方法
  5. 触发具体逻辑(我们写在controller中的方法)
  6. 执行后置处理方法
  7. 对最后的结果进行处理(使用返回的Model来渲染视图)

这就是整个springmvc的核心流程,我们这里只关注第一个部分:获取请求对应的执行链,也就是getHandler()方法

DispatcherServlet -- getHandler()

这个方法虽然简单,但是因为这是整个方法的核心部分,我认为还是有必要拿出来说的,我们来看这个方法的源码:

@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}
复制代码

整个流程很容易懂,就是单纯的遍历handlerMappings这个集合,然后根据请求找到对应的处理器,听起来很简单,但是这里面是有门道的,我们先来看handlerMappings这个属性,其属性定义如下:

private List<HandlerMapping> handlerMappings;
复制代码

原来是一个HandlerMapping的集合,就是下面这样:

从源码入手分析SpringMVC的原理
这些HandlerMapping都是用于定义 请求到处理器的映射

,这一点很重要,因为我们的每一个请求最终都是要对应到具体的处理器方法上的,而我们真正要看的就是RequestMappingHandlerMapping这个类,这里面封装了我们自己编写的controller中的方法,在这个类中,有下面这样一个属性:

private Map<String, Predicate<Class<?>>> pathPrefixes = new LinkedHashMap<>();
复制代码

这里面保存的就是我们的请求到具体方法的映射,整个RequestMappingHandlerMapping类就是通过@RequestMapping注解来创建我们的RequestMappingInfo实例,而RequestMappingInfo这个实例就封装了我们具体需要执行的方法,在调用时就可以通过反射来创建具体的controller实例,然后调用对应的方法即可

这些映射关系都是在一开始随着ApplicationContext进行加载,当加载完成后,我们的请求映射关系也随之确定了下来,接着就可以根据请求来判断具体要调用的是哪些方法了

最后再回来看getHandler()方法,这个方法最终会返回一个HandlerExecutionChain,其中封装了处理器和拦截器,如下:

private final Object handler;

	@Nullable
	private HandlerInterceptor[] interceptors;
复制代码

接下来的调用就完全依赖这两个属性,相信后面的步骤大家也应该都了解了

DispatcherServlet -- processDispatchResult()

最后再来看一个收尾的方法,这个方法就是用来对最终结果做处理,主要是负责将模型数据渲染到视图中,在如今前后端分离的项目中,这个方法的价值也越来越小了,不过还是有必要了解一下的,整个方法的源码我这里就不放了,其步骤主要就是两步:

  1. 如果抛出异常,就选择对应的异常处理器来处理
  2. 如果模型数据或视图不为空,就选择视图解析器来解析并渲染视图

没什么太难理解的地方,解析的含义就是根据视图名来找到对应的视图对象,渲染就是把模型数据填充到视图中,我这里就放一段解析视图名的resolveViewName()方法中的一段,其中会遍历查找以寻找合适的视图解析器:

if (this.viewResolvers != null) {
			for (ViewResolver viewResolver : this.viewResolvers) {
				View view = viewResolver.resolveViewName(viewName, locale);
				if (view != null) {
					return view;
				}
			}
		}
复制代码

总结

整个springmvc的核心流程已经讲完了,最后放一张具体的流程图来加深大家的理解

从源码入手分析SpringMVC的原理

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

编程珠玑

编程珠玑

Jon Bentley / 人民邮电出版社 / 2006-11 / 28.0

《编程珠玑》第一版是我早期职业生涯中阅读过的对我影响较大的书籍之一,在书中首次接触到的很多观点都让我长期受益。作者在这一版本中做了重要更新,新增加的很多例子让我耳目一新。——Steve McConnell,《代码大全》作者  如果让程序员列举出他们喜欢的书籍,Jon Bentley的《编程珠玑》一定可以归于经典之列。如同精美的珍珠出自饱受沙砾折磨的牡蛎,程序员们的精彩设计也来源泉于曾经折磨他们的实......一起来看看 《编程珠玑》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具