今天小编尝试从源码层面上对spring mvc的初始化过程进行分析,一起揭开spring mvc的真实面纱,也许我们都已经学会使用spring mvc,或者说对spring mvc的原理在理论上已经能倒背如流。在开始之前,这可能需要你掌握java ee的一些基本知识,比如说我们要先学会java ee 的servlet技术规范,因为spring mvc框架实现,底层是遵循servlet规范的。
在开始源码分析之前,我们可能需要一个简单的案例工程,不慌,小编已经安排好了:
样例工程下载地址 : https://github.com/smallercoder/spring-mvc-test
那下面就让我们开始吧!
一、前置知识
大家都知道,我们在使用spring mvc时通常会在 web.xml
文件中做如下配置:
web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
<?xml version= "1.0" encoding= "utf-8" ?> <web-app version= "3.0" xmlns= "http://java.sun.com/xml/ns/javaee" xmlns:xsi= "http://www.w3.org/2001/xmlschema-instance" xsi:schemalocation= "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" > <!-- 上下文参数,在监听器中被使用 --> <context-param> <param-name>contextconfiglocation</param-name> <param-value> classpath:applicationcontext.xml </param-value> </context-param> <!-- 监听器配置 --> <listener> <listener- class >org.springframework.web.context.contextloaderlistener</listener- class > </listener> <!-- 前端控制器配置 --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet- class >org.springframework.web.servlet.dispatcherservlet</servlet- class > <init-param> <param-name>contextconfiglocation</param-name> <param-value>classpath:applicationcontext-mvc.xml</param-value> </init-param> <load-on-startup> 1 </load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app> |
上面的配置总结起来有几点内容,分别是:dispatcherservlet
当我们将spring mvc应用部署到tomcat时,当你不配置任何的 context-param
和 listener
参数,只配置一个 dispatcherservlet
时,那么tomcat在启动的时候是不会初始化spring web上下文的,换句话说,tomcat是不会初始化spring框架的,因为你并没有告诉它们spring的配置文件放在什么地方,以及怎么去加载。所以 listener
监听器帮了我们这个忙,那么为什么配置监听器之后就可以告诉tomcat怎么去加载呢?因为 listener
是实现了servlet技术规范的监听器组件,tomcat在启动时会先加载 web.xml
中是否有servlet监听器存在,有则启动它们。 contextloaderlistener
是spring框架对servlet监听器的一个封装,本质上还是一个servlet监听器,所以会被执行,但由于 contextloaderlistener
源码中是基于 contextconfiglocation
和 contextclass
两个配置参数去加载相应配置的,因此就有了我们配置的 context-param
参数了, servlet
标签里的初始化参数也是同样的道理,即告诉web服务器在启动的同时把spring web上下文( webapplicationcontext
)也给初始化了。
上面讲了下tomcat加载spring mvc应用的大致流程,接下来将从源码入手分析启动原理。
二、spring mvc web 上下文启动源码分析
假设现在我们把上面 web.xml
文件中的 <load-on-startup>1</load-on-startup>
给去掉,那么默认tomcat启动时只会初始化spring web上下文,也就是说只会加载到 applicationcontext.xml
这个文件,对于 applicationcontext-mvc.xml
这个配置文件是加载不到的, <load-on-startup>1</load-on-startup>
的意思就是让 dispatcherservlet
延迟到使用的时候( 也就是处理请求的时候
)再做初始化。
我们已经知道spring web是基于 servlet
标准去封装的,那么很明显,servlet怎么初始化, webapplicationcontext
web上下文就应该怎么初始化。我们先看看 contextloaderlistener
的源码是怎样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class contextloaderlistener extends contextloader implements servletcontextlistener { // 初始化方法 @override public void contextinitialized(servletcontextevent event) { initwebapplicationcontext(event.getservletcontext()); } // 销毁方法 @override public void contextdestroyed(servletcontextevent event) { closewebapplicationcontext(event.getservletcontext()); contextcleanuplistener.cleanupattributes(event.getservletcontext()); } } |
contextloaderlistener
类实现了 servletcontextlistener
,本质上是一个servlet监听器,tomcat将会优先加载servlet监听器组件,并调用 contextinitialized
方法,在 contextinitialized
方法中调用 initwebapplicationcontext
方法初始化spring web上下文,看到这焕然大悟,原来spring mvc的入口就在这里,哈哈~~~赶紧跟进去 initwebapplicationcontext
方法看看吧!
initwebapplicationcontext()
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 创建web上下文,默认是xmlwebapplicationcontext if ( this .context == null ) { this .context = createwebapplicationcontext(servletcontext); } if ( this .context instanceof configurablewebapplicationcontext) { configurablewebapplicationcontext cwac = (configurablewebapplicationcontext) this .context; // 如果该容器还没有刷新过 if (!cwac.isactive()) { if (cwac.getparent() == null ) { applicationcontext parent = loadparentcontext(servletcontext); cwac.setparent(parent); } // 配置并刷新容器 configureandrefreshwebapplicationcontext(cwac, servletcontext); } } |
上面的方法只做了两件事:
1、如果spring web容器还没有创建,那么就创建一个全新的spring web容器,并且该容器为root根容器,下面第三节讲到的servlet spring web容器是在此根容器上创建起来的
2、配置并刷新容器
上面代码注释说到默认创建的上下文容器是 xmlwebapplicationcontext
,为什么不是其他web上下文呢?为啥不是下面上下文的任何一种呢?
我们可以跟进去 createwebapplicationcontext
后就可以发现默认是从一个叫 contextloader.properties
文件加载配置的,该文件的内容为:
具体实现为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
protected class <?> determinecontextclass(servletcontext servletcontext) { // 自定义上下文,否则就默认创建xmlwebapplicationcontext string contextclassname = servletcontext.getinitparameter(context_class_param); if (contextclassname != null ) { try { return classutils.forname(contextclassname, classutils.getdefaultclassloader()); } catch (classnotfoundexception ex) { throw new applicationcontextexception( "failed to load custom context class [" + contextclassname + "]" , ex); } } else { // 从属性文件中加载类名,也就是org.springframework.web.context.support.xmlwebapplicationcontext contextclassname = defaultstrategies.getproperty(webapplicationcontext. class .getname()); try { return classutils.forname(contextclassname, contextloader. class .getclassloader()); } catch (classnotfoundexception ex) { throw new applicationcontextexception( "failed to load default context class [" + contextclassname + "]" , ex); } } } |
上面可以看出其实我们也可以自定义spring web的上下文的,那么怎么去指定我们自定义的上下文呢?答案是通过在 web.xml
中指定 contextclass
参数,因此第一小结结尾时说 contextclass
参数和 contextconfiglocation
很重要~~至于 contextconfiglocation
参数,我们跟进 configureandrefreshwebapplicationcontext
即可看到,如下图:
总结:
spring mvc启动流程大致就是从一个叫 contextloaderlistener
开始的,它是一个servlet监听器,能够被web容器发现并加载,初始化监听器 contextloaderlistener
之后,接着就是根据配置如 contextconfiglocation
和 contextclass
创建web容器了,如果你不指定 contextclass
参数值,则默认创建的spring web容器类型为 xmlwebapplicationcontext
,最后一步就是根据你配置的 contextconfiglocation
文件路径去配置并刷新容器了。
三、 dispatcherservlet 控制器的初始化
好了,上面我们简单地分析了spring mvc容器初始化的源码,我们永远不会忘记,我们默认创建的容器类型为 xmlwebapplicationcontext
,当然我们也不会忘记,在 web.xml
中,我们还有一个重要的配置,那就是 dispatcherservlet
。下面我们就来分析下 dispatcherservlet
的初始化过程。
dispatcherservlet
,就是一个servlet,一个用来处理request请求的servlet,它是spring mvc的核心,所有的请求都经过它,并由它指定后续操作该怎么执行,咋一看像一扇门,因此我管它叫“闸门”。在我们继续之前,我们应该共同遵守一个常识,那就是-------无论是监听器还是servlet,都是servlet规范组件,web服务器都可以发现并加载它们。
下面我们先看看 dispatcherservlet
的继承关系:
看到这我们是不是一目了然了, dispatcherservlet
继承了 httpservlet
这个类, httpservlet
是servlet技术规范中专门用于处理http请求的servlet,这就不难解释为什么spring mvc会将 dispatcherservlet
作为统一请求入口了。
因为一个servlet的生命周期是 init()
-> service()
-> destory()
,那么 dispatcherservlet
怎么初始化呢?看上面的继承图,我们进到 httpservletbean
去看看。
果不其然, httpservletbean
类中有一个 init()
方法, httpservletbean
是一个抽象类, init()
方法如下:
可以看出方法采用 final
修饰,因为 final
修饰的方法是不能被子类继承的,也就是子类没有同样的 init()
方法了,这个 init
方法就是 dispatcherservlet
的初始化入口了。
接着我们跟进 frameworkservlet
的 initservletbean()
方法:
在方法中将会初始化不同于第一小节的web容器,请记住,这个新的spring web 容器是专门为 dispactherservlet
服务的,而且这个新容器是在第一小节根root容器的基础上创建的,我们在 <servlet>
标签中配置的初始化参数被加入到新容器中去。
至此, dispatchersevlet
的初始化完成了,听着有点蒙蔽,但其实也是这样,上面的分析仅仅只围绕一个方法,它叫 init()
,所有的servlet初始化都将调用该方法。
总结:
dispactherservlet
的初始化做了两件事情,第一件事情就是根据根web容器,也就是我们第一小节创建的 xmlwebapplicationcontext
,然后创建一个专门为 dispactherservlet
服务的web容器,第二件事情就是将你在web.xml文件中对 dispactherservlet
进行的相关配置加载到新容器当中。
三、每个request调用请求经历了哪些过程
其实说到这才是 dispatcherservlet
控制器的核心所在,因为web框架无非就是接受请求,处理请求,然后响应请求。当然了,如果 dispactherservlet
只是单纯地接受处理然后响应请求,那未免太弱了,因此spring设计者加入了许许多多的新特性,比如说拦截器、消息转换器、请求处理映射器以及各种各样的 resolver
,因此spring mvc非常强大。
dispatcherservlet
类不做相关源码分析,因为它就是一个固定的执行步骤,什么意思呢?一个request进来,大致就经历这样的过程:
接受请求 -----> 是否有各种各样的处理器 handler
-------> 是否有消息转换器 handleradapter
--------> 响应请求
上面每一步如果存在相应的组件,当然前提是你在项目中有做相关的配置,则会执行你配置的组件,最后响应请求。因此明白大致的流程之后,如果你想调试一个request,那么你完全可以在 dispatcherservlet
类的 dodispatch
方法中打个断点,跟完代码之后你就会发现其实大致流程就差不多了。
四、后话
本文的工程是基于传统的web.xml加载web项目,当然在spring mvc中我们也可以完全基于注解的方式进行配置,我们可以通过实现 webapplicationinitializer
来创建自己的web启动器,也可以通过继承 abstractannotationconfigdispatcherservletinitializer
来创建相应的spring web容器(包括上面说到的根容器和servlet web容器),最后通过继承 webmvcconfigurationsupport
再一步进行自定义配置(相关拦截器,bean等)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://juejin.im/post/5b207dc86fb9a01e49294f42