一、
在传统spring项目中,聚合springmvc的时候,不使用web.xml,可以使用ServletContainerInitializer这个servlet3.0提供的接口,达到不需要web.xml的效果。但是在springboot构建的项目中,我们并没有看到这个接口的使用。在默认情况下,使用的是内嵌tomcat,那springmvc或者说dispatcherServlet又是怎么注册到servlet容器的呢,按照惯例,如果是springboot项目,我们可以从自动配置中入手,逐步分析。
首先看一下dispatcherServlet是如何实例化的。
在spring-boot-autoconfigure-2.3.12.RELEASE.jar中META-INF/spring.factories中可以找到 DispatcherServletAutoConfiguration
这个类的配置是在ServletWebServerFactoryAutoConfiguration的后面,从名字可以猜出这个是生成容器的工厂自动配置类
然后内部还有一个内部类,实例化了一个dispatcherServlet,还注意到,它实例化的条件装配是两个,一个是DefaultDispatcherServletCondition 判断逻辑封装在别的地方的类,还有一个是类路径中是否有ServletRegistration
先看第一个DefaultDispatcherServletCondition,它也是内部类,整个逻辑所表达的意思是,spring容器中只能有一个DispatcherServlet
然后整个自动配置类中,不仅配置了DispatcherServlet, 还有一个被实例化的类 DispatcherServletRegistrationBean
那么不会无缘无故放一个bean在这里的,先从内部逻辑开始看
1、它注入了DispatcherServlet
2、它将DispatcherServlet 包裹起来了,也就是DispatcherServletRegistrationBean 持有 DispatcherServlet
然后看它的注解
1、DispatcherServletRegistrationCondition 这个注解的作用是检查容器中,是否有且只有一个 DispatcherServlet
2、import了我们上文的DispatcherServlet配置类 ,这个决定了他们之间的配置的顺序,先是 DispatcherServlet配置,然后是 DispatcherServletRegistrationBean包装一番
那么这个 DispatcherServletRegistrationBean 到底是什么东西,最后看下它的继承树大概也能猜到,它是ServletContextInitializer的接口实现
从名字先猜测这个作用是在servlet容器启动时做初始化的,结合上下文,是用来初始化 DispatcherServlet
二、
在了解了 DispatcherServlet的自动配置之后,只发现了一个 DispatcherServletRegistrationBean是可以用来初始化 DispatcherServlet的,那么这个初始化器的执行时机在哪里呢?
其实这个问题约等于tomcat的启动过程中,DispatcherServlet何时被注册,所以接下来看看内嵌tomcat的启动过程
那么按照惯例,这个内嵌tomcat的启动必然是伴随着spring容器的启动而启动的,所以首先是来到 AbstractApplicationContext --> refresh()中
onRefresh在抽象类中是个空方法,专门留给子类实现的一个扩展点,我们创建的容器是AnnotationConfigServletWebServerApplicationContext ,而它的父类是ServletWebServerApplicationContext
onRefresh方法的实现就是在 ServletWebServerApplicationContext中,直接明了,内嵌tomcat的启动入口
进入后可以看到,先是拿到工厂,然后通过工厂拿到webServer
工厂是在上文中一闪而过的DispatcherServlet自动配置类中所顺序依赖的另一个ServletWebServerFactoryAutoConfiguration自动配置类所添加
先看getWebServer方法的参数,这下就和上文衔接起来了,这个方法是会返回一个ServletContextInitializer,而这个接口是个函数式接口
然后通过方法引用与 selfInitialize方法的方法签名是一致的,所以这个selfInitialize是会在待会儿的getWebServer方法的内部或者其他的方法中被调用
顺便一提,ServletContextInitializer的接口方法签名是 void onStartup(ServletContext servletContext) throws ServletException;
从这个方法里面可以猜测出,在待会儿创建webServer的时候,会调用这个初始化一次,但是这个方法内部又循环调用了 ServletContextInitializer, 有点像委派
知道我们的初始化器已经获得了之后,稍微跟踪一下路线,首先回到getWebServer方法
首先可以看到实例化了一个tomcat,然后给它设置了一些组件,例如connector、service、host、engine
然后可以看到我们的初始化器进入了prepareContext方法
可以看到,这个方法先是实例化了一个context, 然后这个context是放到了host里面了,而我们的初始化器是放入configureContext方法中了
跟进方法可以看到,我们的初始化器是放到了starter里面了,而starter是放到了context里面,而且是作为Servlet容器的初始化器
这里大概就可以知道,我们的dispatcherServlet的初始化器 --> TomcatStarter --> TomcatEmbeddedContext --> Host
这里插入一个上面遗漏的,可以方便后文的理解,在Tomcat实例化之后,给它设置各种组件的时候,在获取host的时候,会顺便将host设置到engine中
所以包裹的次序是 engine > host > context > starter > dispatcherServlet的初始化器
那么在知道了初始化器的去向之后,接下来退出prepareContext方法,来到最后一个方法 getTomcatWebServer(tomcat)
首先在这里需要注意一下,因为接下来会出现套娃现象,需要先解释一下这个server是 StandardServer类,它是继承了LifecycleBase
关键是不止它一个继承了,接下来很多的类都继承了,所以会看到,只要调用了start(),都会跳转回到LifecycleBase 的startInternal()
然后这个startInternal() 是由StandardServer实现的,然后会调用service的start
那么调用StandardService的start之后,就会跳转到LifecycleBase, 和上面的刚刚方法一样,也会调用startInternal() ,这里就不重复放图了
这个是StandardService的startInternal()
这个是StandardEngine的startInternal()
这个是StandardEngine的父类 ContainerBase 的startInternal()
来到这里,可以停一停,这里使用了异步start它的child ,而container这个接口的实现,基本上面讲到的几个组件,是都有实现的
那么这里找到的child,我们当前是在 StandardEngine的实现中,那么它的child就是StandardHost,也就是在另外一条线程中,会调用 host的start方法
这个是 StandardHost的startInternal()
接下里是StandardHost的父类 ContainerBase 的startInternal()
那么就和StandardEngine一样的流程,紧接着就是调用StandardHost的child的start()方法,那么host 的child就是context
那么就会在新的一条线程中来到了context的startInternal(),这个方法就有点和之前的不一样了,特别长,这里我只查找我们关心的,就是初始化器的应用
很明显,key就是ServletContainerInitializer,然后会调用它的onStartup方法
而这个ServletContainerInitializer其实就是上文提及的TomcatStarter
那么在TomcatStarter的这个方法里面,又可以看到,循环调用了所有的 ServletContextInitializer
而这个 ServletContextInitializer 也就是我们上文中提及的,在一开始就已经放进来的 getSelfInitializer方法返回的,而这个返回是个方法引用
那是因为selfInitialize这个方法的方法签名和 ServletContextInitializer 这个函数式接口的的方法签名一致,也就是说接下来在调用 initializer.onStartup(servletContext);的时候
其实就是调用 selfInitialize
那么在这个方法里面,先是从容器中拿出来所有的 ServletContextInitializer接口的实现(猜测,spring的其中一种套路),然后循环调用它们的onStartup方法,真正的初始化我们的servlet组件
首先看看如何拿到我们自动配置的ServletContextInitializer
在这一步
1 、循环类型,也就是ServletContextInitializer.class
2、通过这个类型从容器拿到了所有这个接口的实现,key是beanName ,value是bean
仔细看第二步的方法,其实就是spring的常用套路之一,从容器里面拿到接口的所有实现的beanName ,然后逐个通过beanName拿到bean实例
然后在第三步,终于看到了自动配置中的 ServletRegistrationBean接口
它的实现就是 DispatcherServletRegistrationBean, 我们在自动配置注入的类,而这个类是持有我们的dispatcherServlet的
那么在找到了我们的 DispatcherServletRegistrationBean 之后,接着当然就是调用它的onStartup方法
this.servlet ,这个方法是在 DispatcherServletRegistrationBean,那么这个servlet当然就是我们自动配置的dispatcherServlet了,此时就被注册进ServletContext中了
三、
至此,内嵌的tomcat启动和最重要的dispatcherServlet是如何不通过serlvet3.0提供的接口注册到tomcat中的过程简单走过
其实,在上面的循环应用所有的ServletContextInitializer中,还有其他的初始化器,在javaWeb中,有三大支柱,servlet、listener和filter,都会在这里做初始化并注册到tomcat中。
如有错漏,欢迎指正。