测试环境搭建: 本次搭建是基于springboot来实现的,代码在码云的链接:https://gitee.com/yangxioahui/thymeleaf.git
项目结构代码如下:
一: controller的三种实现方式:
1. 第一种是大家非常熟悉的,使用@Controller和@RequestMapping
浏览器测试第一个方法结果:
2.第二种: 实现Controller 接口,并在beanName里写上url;
浏览器测试结果:
3. 第三种:实现HttpRequestHandler接口,并在beanName里写上url;
浏览器测试结果:
总结: 第一种的请求参数是不确定的,随便我们自己定义 第二和第三种,请求参数是固定的,并且他们的url都是定义在请求路径上的,只是返回值不一样
二: 思考
要执行一个方法,第一步:先找到该方法所在的类的对象,第二步: 调用该对象的指定方法:
如何找到一个类的实例对象:
我们在浏览器输入一个url,对应的controller就会被执行,那么肯定会在某个地方存在一个map集合,map集合的key就是url,value值又是什么呢? 对于第一种实现controller的方式(@RequestMapping 方式),value值至少要包含:
@RequestMapping注解标注的方法对象 和 该方法所属的controller对象,一个controller可以有多个带有@RequestMapping 注解的方法;对于第二和第三种实现controller的方式,由于他们的对象只会有一个方法处理请求,url是beanName
那么他们的map的value值就是HttpRequestHandler或Controller 接口的实现类的对象即可;因此 三种实现controller的方式,最终会有2种map对象进行存储,这2种对象存在不同的映射器中,我们暂且都叫它们做处理器映射器,也叫HandlerMapping;
Handler 就是我们所说的controller;我们分别给这两种映射器起一个名字: @RequestMapping 方式的就叫做RequestMappingHandlerMapping,而实现HttpRequestHandler或Controller 接口方式的控制器,因为url就是他们的beanName,所以起个名称:
BeanNameUrlHandlerMapping;既然2种方式都是通过url查找一个controller,那么我们将他们抽象出来,提供一个父接口,HandlerMapping,要提供一个查找Handler的方法吧:参数有HttpServletRequest request即可,那么返回值呢?
两种方式的返回值都不一样,用谁都不好,用Object 可以吧,当然可以,但如果我要在我的方法调用前后加上拦截器,这样Object就不合理了,那么就起个公共的名字吧,叫做处理器执行链条,这样可以加入很多的拦截器了: HandlerExecutionChain,于是接口的方法如下:
HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception,那么问题来了? 一个请求过来,我怎么知道要用那种HandlerMapping呢? 到底是RequestMappingHandlerMapping还是BeanNameUrlHandlerMapping,没法确定,既然这样,那就用个集合存起2种HandlerMapiing:
List<HandlerMapping> handlerMappings; 一个请求过来,先问第一个HandlerMapping,看看它的map集合中有没有对应的Handler,没有继续找第二个,都没有叫404 响应
过程如图:
源码分析BeanNameUrlHandlerMapping 的map集合创建过程:
先看看其继承体系:
我们在其父类中看到了对应的map集合:
现在,要关注的是,第二和第三种实现controller的方式,是什么时候将他们的url和对应的controller对象存到HandlerMap中的,我们在继承体系中发现,BeanNameUrlHandlerMapping实现了ApplicationContextAware接口,
spring在创建BeanNameUrlHandlerMapping会调用对应的接口实现方法setApplicationContext(@Nullable ApplicationContext context),debug 断点调试打在该方法上:然后启动代码:
小结: 上面的方法是从spring容器中取出所有的beanName,然后看看是不是以url开头,是的话就创建对应的bean ,并且存到handlerMap中,我们看看判断beanName是不是url开头的方法:
String[] urls = determineUrlsForHandler(beanName),为何会是数组,因为一个bean可以有多个别名:
接着我们看看是如何将bean 跟其url存到map中的:
registerHandler(urls, beanName);
源码分析RequestMappingHandlerMapping中的map集合是如何将RequestMapping注解对应的方法存进去的,先看继承体系:
父类AbstractHandlerMethodMapping有个属性:
从继承体系中,可以看到,RequestMappingHandlerMapping实现了InitializingBean接口,该接口是一个生命周期接口,spring在创建bean时,会调用该接口的afterPropertiesSet() 方法,那么我们debug调试看看:
我们先看是如何获取到对应的beanName的:getCandidateBeanNames()
接着看看如何处理beanName:
processCandidateBean(beanName);
现在已经找到带有@controller或者@RequestMapping注解的类了,从上面条件可以看到,类上只要有其中一个注解都可以了,所以下面搭配也是可以的:
找到对应的类,接下来要处理器里面的方法了吧:detectHandlerMethods(beanName);
跟进去看看是如何找到对应的方法的:
最后我们看看是如何将找到的方法存到map中的:registerHandlerMethod(handler, invocableMethod, mapping);
通过上图分析,一个url 过来,先从urlLookup这个集合中找到@requestMapping信息封装的对象,然后以该对象作为key,从mappingLookup中获取对应的controller和方法
小结:我们已经解决了第一个问题:如何通过url找到一个Handler,那么第二个问题来了,找到Handler后,如何执行方法?
三种实现controller的方式,第一种执行是最复杂的,因为参数不固定:
另外2种方式,比较固定:
后2种方式的返回值不一样,前面我们说过HandlerMapping有个获取handler的方法:HandlerExecutionChain getHandler(HttpServletRequest request),所以三种方式通过url获取到的handler最终被封装成了HandlerExecutionChain
现在我们要执行里面的方法时,对于第一种controller实现方法,HandlerExecutionChain的handler属性对象其实是HandlerMethod对象,而对于第二种方式,就是Controller接口的实现类对象,第三种方式:HttpRequestHandler接口实现类对象;
这样的话,我们需要判断handler对象类型,来找到对应的处理方式,如果是实现Controller接口就用xx 方式,如果是HttpRequestHandler接口就有aa方式,对于@Controller注解的又是另外一种方式,这种找处理方式的过程就是适配过程,也可以叫做
查找适配器过程,给适配器起名叫做HandlerAdapter,至少存在3种HandlerAdappter实现类,该接口要提供一个方法,判断支不支持当前处理器,支持,就可以调用该类的另一个执行处理器的方法:
源码分析:
我们先看实现Controller接口方式的controller对应的适配器:SimpleControllerHandlerAdapter
接着我们来看看实现HttpRequestHandler接口的方式:HttpRequestHandlerAdapter
最复杂的是实现RequestMapping注解的方法,因为参数是多样化的:RequestMappingHandlerAdapter
综上所述,我们已经解决了如何通过url找到一个handler,并且如何通过hanlder找到执行器,大概流程是 : url->分别判断BeanNameUrlHandlerMapping和RequestMappingHandlerMapping找到对应hanlder
->分别判断HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter,RequestMappingHandlerAdapter 找到对应的适配器进行方法调用;
我们知道,tomcat接收到请求后,会根据路径找到对应的servlet,而在springMVC中,只提供了一个servlet,那就是DispatcherSetvlet,他拦截的路径是“/”代表所有请求,下面我们来分析其核心过程:
先看继承体系:
可见,其继承的是HttpServlet:
那么,我们上面分析的HandlerMapping和HandlerAdapter是什么时候添加到DispathcerServlet的呢?
在下面的配置类中,有个内部类:
这个内部类的继承关系:
WebMvcConfigurationSupport这个类里面有很多方法:
可以看到,通过该配置类将我们要的HandlerMapping和HandlerAdapter注入到spring容器中了,那么我们回到DispatcherServlet中:
//在非springboot项目,使用的是默认策略,这怎么理解呢?我们在DispatcherServlet所在包下看到一个文件:
看看里面的内容:
回到DisapcherServlet中:
我们回到之前获取默认策略方法那里:
从上面的分析,我们已经知道:
List<HandlerMapping> handlerMappings 和 List<HandlerAdapter> handlerAdapters 是如何添加到DispacherServlet中去的了,下面我们进行调用链路调试:
//该方法是get请求,所以我们debug在DispatcherServlet父类的doGet中:
有上面的分析可以知道,我们在service层要获取HttpServletRequest
,可以通过:HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
继续跟进:
该方法是核心代码:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;//我们之前分析的HanderMapping的getHandler方法,返回值就是HandlerExecutionChain 了
boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try {
ModelAndView mv = null;
Exception dispatchException = null; try {
processedRequest = checkMultipart(request); //校验请求是不是文件上传类型,主要的逻辑:StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
multipartRequestParsed = (processedRequest != request); // Determine handler for the current request.
mappedHandler = getHandler(processedRequest);//通过请求对象的url,遍历各个HandlerMapping,找到对应的Hanler
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);//找不到就是404
return;
} // Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); //通过找到的Handler,遍历HandlerAdapter,看看哪个可以执行该Handler的方法 // Process last-modified header, if supported by the handler.
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) { //如果是get或者head请求,判断上次请求和这次请求的最后修改时间,如果没变化,前端就用它们之前缓存的内容即可
return;
}
} if (!mappedHandler.applyPreHandle(processedRequest, response)) { //执行拦截器链条的所有前置拦截处理
return;
} // Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); //执行目标方法,得到ModleAndView对象 if (asyncManager.isConcurrentHandlingStarted()) {
return;
} applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);//执行拦截器的后置处理方法
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
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()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
下面具体跟进去看看:
因为本次调试用的是RequestMapping方式的controller,因此会用到RequestMappingHandlerMapping:
跟进去:
在这个Map里有我们要找的Handler
至此,一个请求过来,通过请求url,找到了对应的Handler:我们回到主流程:
前面分析过RequestMappingHandlerMapping的map集合的Handler对象是HandlerMethod,因此RequestMappingHandlerAdapter 只要判断hanler是不是HandlerMethod
至此,HandlerAdapter已经找到了,接着就是处理handler的目标方法了:
执行方法前会调用拦截器链路:
下面是真正执行目标方法了:
@RequestMapping注解的方式,执行方法逻辑会非常复杂:
我们这里会用到下面的一个参数解析器,因为我们的路径是:product/info/{id}
继续跟进:
最后的结果返回封装成ModleAndView对象
HandlerAdapter执行后,获得了ModleAndView
我本次使用的是Thymeleaf 模板技术,其有个ThymeleafProperties类,指定了视图的前缀和后缀
所以,我们得到的ModelAndView 中的view 会被拼接上前缀和后缀,这样视图的路径就变成: classpath:/templates/product.html ,而我在该路径下已经有了该文件
继续前面调试:
中间省略n个步骤,最终到达这里:
所以我们跟进下面方法:
进入里面,发现它是先从缓存取模板,没有取到再重新去解析:
显然,我第一次调用,缓存是没有的:
继续跟进,省略多个步骤后:
继续跟进到达:
跟进里面可以看到
数据填充过程就不分析了,有兴趣自己调试
问题: 三种controller实现方式,它们的 HandlerAdapter 返回的对象是ModelAndView,但有些时候是不用进行试图解析的,那么是怎么判断需不需要进行试图解析的呢
我们调试第三种实现方式:
我们看看wasCleared()这个方法:
最后看看MV的结构
最后总结:
问题?我们如何自定义HandlerMapping和HandlerAdapter还有参数解析器,响应值解析器,下编博客将会讲解
springmvc 源码分析(二)-- DiapartcherServlet核心调用流程分析的更多相关文章
-
springMVC源码分析--HandlerInterceptor拦截器调用过程(二)
在上一篇博客springMVC源码分析--HandlerInterceptor拦截器(一)中我们介绍了HandlerInterceptor拦截器相关的内容,了解到了HandlerInterceptor ...
-
7、SpringMVC源码分析(2):分析HandlerAdapter.handle方法,了解handler方法的调用细节以及@ModelAttribute注解
从上一篇 SpringMVC源码分析(1) 中我们了解到在DispatcherServlet.doDispatch方法中会通过 mv = ha.handle(processedRequest, res ...
-
springMVC源码分析--容器初始化(二)DispatcherServlet
在上一篇博客springMVC源码分析--容器初始化(一)中我们介绍了spring web初始化IOC容器的过程,springMVC作为spring项目中的子项目,其可以和spring web容器很好 ...
-
框架-springmvc源码分析(二)
框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...
-
springMVC源码分析--SimpleServletHandlerAdapter(二)
上一篇博客springMVC源码分析--HandlerAdapter(一)中我们主要介绍了一下HandlerAdapter接口相关的内容,实现类及其在DispatcherServlet中执行的顺序,接 ...
-
springMVC源码分析--AbstractHandlerMapping(二)
上一篇博客springMVC源码分析--HandlerMapping(一)中我们简单的介绍了HandlerMapping,接下来我们介绍一下它的抽象实现类AbstractHandlerMapping
-
springMVC源码分析--国际化实现Session和Cookie(二)
上一篇博客springMVC源码分析--国际化LocaleResolver(一)中我们介绍了springMVC提供的国际化的解决方案,接下来我们根据springMVC提供的解决方案来简单的实现一个多语 ...
-
springMVC源码分析--视图AbstractView和InternalResourceView(二)
上一篇博客springMVC源码分析--视图View(一)中我们介绍了简单介绍了View的结构实现及运行流程,接下来我们介绍一下View的实现类做的处理操作. AbstractView实现了rende ...
-
springMVC源码分析--HandlerMethodReturnValueHandlerComposite返回值解析器集合(二)
在上一篇博客springMVC源码分析--HandlerMethodReturnValueHandler返回值解析器(一)我们介绍了返回值解析器HandlerMethodReturnValueHand ...
随机推荐
-
【Hello CC.NET】CC.NET 实现自动化集成
一.背景 公司的某一金融项目包含 12 个子系统,新需求一般按分支来开发,测完后合并到主干发布.开发团队需要同时维护开发环境.测试环境.模拟环境(主干).目前面临最大的两个问题: 1.子系统太多,每次 ...
-
安装完CentOS 7 后必做的七件事
CentOS是最多人用来运行服务器的 Linux 版本,最新版本是 CentOS 7.当你兴趣勃勃地在一台主机或 VPS 上安装 CentOS 7 后,首要的工作肯定是加强它的安全性,以下列出的七件事 ...
-
Python引用模块和查找模块路径
模块间相互独立相互引用是任何一种编程语言的基础能力.对于"模块"这个词在各种编程语言中或许是不同的,但我们可以简单认为一个程序文件是一个模块,文件里包含了类或者方法的定义.对于编译 ...
-
Foundation--NSDictionary+NSMutableDictionary
键值对 key(一般为字符串对象)---vaule(必须是对象) Person *p1 =[[Person alloc ]init]; Person *p2 =[[Person alloc ]init ...
-
js字符串 数组处理
/*字符串处理*/ var a="Hello world!" console.log(a.indexOf(& ...
-
HiHocoder1415 : 后缀数组三&#183;重复旋律3 &; Poj2774:Long Long Message
题面 HiHocoder1415 Poj2774 Sol 都是求最长公共子串,\(hihocoder\)上讲的很清楚 把两个串拼在一起,中间用一个特殊字符隔开 那么答案就是排序后相邻两个不同串的后缀的 ...
-
nodeJS之crypto模块公钥加密及解密
nodeJS之crypto模块公钥加密及解密 NodeJS有以下4个与公钥加密相关的类.1. Cipher: 用于加密数据:2. Decipher: 用于解密数据:3. Sign: 用于生成签名:4. ...
-
ATS metric query
ATS metric query 参考:ATS metric query proxy.node.cache_hit_mem_ratio proxy.node.cache_hit_mem_ratio_a ...
-
luogu4389 付公主的背包
题目链接:洛谷 题目大意:现在有$n$个物品,每种物品体积为$v_i$,对任意$s\in [1,m]$,求背包恰好装$s$体积的方案数(完全背包问题). 数据范围:$n,m\leq 10^5$ 这道题 ...
-
这篇说的是Unity Input 输入控制器
关于Unity3D是什么.我就不多做解释了.由于工作原因,该系列原创教程不定期更新.每月必然有更新.谢谢各位 Unity Input---输入控制管理器: Edit->Project Setti ...