深入分析SpringBoot源码如何内嵌Tomcat容器?
作者:陌北有棵树,Java人,架构师社区合伙人!
【一】总述
SpringBoot的诞生,极大的简化了Spring框架的使用过程,提升了开发效率,可以把它理解为一个整合包,使用了SpringBoot,就可以不用自己去进行繁琐的配置,通过几个简单的注解,就可以构建一个基于REST的服务。同时,SpringBoot的快速构建部署的特性,为当下大热的微服务落地提供了极大的便利,可以说是构建微服务的理想框架。
归纳来说SpringBoot的特性有如下几点:
自动配置
内置tomcat、jetty、undertow 三大web容器
将Web应用打成jar包启动
那么SpringBoot是怎样做到上述三个特性的呢?是我接下来的研究方向,本篇主要研究的是后两个特点,如何内嵌了Web容易,将应用打成jar包,怎么还能像Web程序一样运行。
本文是笔者患难与共的好兄弟Dewey Ding及笔者Debug了若干天的成果,谨以此篇留作纪念。
【二】问题引出和总体思路
按照常规的Web容器的启动方式,明显是无法和SpringBoot这种jar包的运行方式兼容的,那么他们之间是如何做到无缝衔接的合作呢?
最终运行的依然是SpringMVC框架,那么SpringBoot又是如何做到在内置了Tomcat的同时,又和SpringMVC无缝衔接的呢?
综上所述,整个系列需要研究的技术点如下(本文并未完全覆盖):
SpringBoot启动Tomcat
SpringMVC初始化DispatcherServlet
Tomcat的两种启动方式
Tomcat在SpringBoot中如何拿到配置
【三】SpringBoot启动过程具体分析
在SpringBoot中,一个Web应用从启动到接收请求,我粗略将它分为四步:
SpringBoot初始化
初始化SpringApplication:包括环境变量、资源、构造器、监听器
开始启动:启动监听(listeners)、加载配置(environment)、创建上下文(applicationContext)
自动化配置:这个部分等到后面单独研究
Tomcat初始化
Tomcat接收请求
SpringMVC初始化
这一部分的学习真可谓一波三折,每次Debug SpringApplication的run方法,都会迷失在茫茫的源码之中,看书和博客也都是云里雾里,所以这次决定换一种学法,先从宏观上了解都要做什么,至于具体细节,等到需要的时候再去分析。比如今天要了解的是和Tomcat启动相关的部分,那么就先只了解这个模块。
关于SpringBoot和Tomcat是如何合作的,在实际Debug之前,我们先抛出如下几个问题:
SpringBoot有main()方法,可以直接打成jar包运行;SpringMVC没有main方法,所以无法自己启动,要依赖Tomcat,Tomcat本质是个容器,所以Tomcat中一定有main方法或者线程的start()方法来启动Spring程序
/WEB-INF是Web应用需要的,SpringMVC配置了这个,是为了Tomcat读取,从而启动Web容器
项目要部署到webapp目录下,才能被Tomcat运行
那么问题来了,SpringBoot没有做这些配置,是怎么做到内置Tomcat容器,并让Tomcat启动的呢?
【SpringBoot和Tomcat的初始化】
我们先来看Tomcat的启动时在SpringBoot启动的哪一步?这里只列举比较关键的几步:
SpringApplication的run方法
org.springframework.boot.SpringApplication#run(java.lang.String...)
刷新IOC容器(Bean的实例化)
org.springframework.boot.SpringApplication#refreshContext()org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh()
创建WebServer
在org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh调用了createWebServer
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer()
private void createWebServer() { WebServer webServer = this.webServer; ServletContext servletContext = getServletContext(); if (webServer == null && servletContext == null) { // 重点:这时初始化了dispatcherServlet ServletWebServerFactory factory = getWebServerFactory(); // 创建 this.webServer = factory.getWebServer(getSelfInitializer()); } else if (servletContext != null) { try { // 启动 getSelfInitializer().onStartup(servletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } initPropertySources();}
这里就是Tomcat的创建和启动过程了,关于下面这两行代码中,蕴含着我们尚未去发现的秘密,后面会继续分析(蕴含着的秘密真的坑苦了我们,竟然与它擦肩而过,然后绕了一大圈才回来,泪奔中~~):
this.webServer = factory.getWebServer(getSelfInitializer());······getSelfInitializer().onStartup(servletContext);
在这里创建了Tomcat,Connector,Host,Engine并且设置一些属性,关于Tomcat的具体内容,由于内容比较多,就不在此篇详细展开,后续会专门研究。这里我们发现,在Tomcat启动的时候,servletClass还没有获取到dispatcherServlet,而在第一次收到请求时,servletClass就变成了dispatcherServlet,这里就对我们形成了一定误导,以为是第一次收到请求时Tomcat才加载了默认的wrapper,后来才发现出现了偏差,经历了无数次断点后,才回到正确的路上。
这里不得不提到,一个最开始被我们忽略,后来才发现是个重要的地方:getSelfInitializer() 。关于SpringBoot是如何把“/”和“dispatcherServlet”的关联给到Tomcat这件事,就蕴含在下面这段代码之中:
//注意 selfInitializeprivate org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return this::selfInitialize;}//注意 getServletContextInitializerBeans()private void selfInitialize(ServletContext servletContext) throws ServletException { prepareWebApplicationContext(servletContext); registerApplicationScope(servletContext); WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext); for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); }}//注意 ServletContextInitializerBeans()protected Collection<ServletContextInitializer> getServletContextInitializerBeans() { return new ServletContextInitializerBeans(getBeanFactory());}// 注意 this.initializers 和 addServletContextInitializerBeanspublic ServletContextInitializerBeans(ListableBeanFactory beanFactory, Class<? extends ServletContextInitializer>... initializerTypes) { this.initializers = new LinkedMultiValueMap<>(); this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes) : Collections.singletonList(ServletContextInitializer.class); addServletContextInitializerBeans(beanFactory); addAdaptableBeans(beanFactory); List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream() .flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE)) .collect(Collectors.toList()); this.sortedList = Collections.unmodifiableList(sortedInitializers); logMappings(this.initializers);}
让我们来看两张对比图:
通过上面两个Debug的断点图,我们可以看到,在执行了addServletContextInitializerBeans(beanFactory)和addAdaptableBeans(beanFactory)方法之后,this.initializers的赋值发生了变化,两个Servlet,四个Filter都被赋到里面,至于这两个方法中的处理逻辑,此时已经心累到不想去看,暂记jira。
接下来的onStartup()方法,调用了接口org.springframework.boot.web.servlet.ServletContextInitializeronStartup()方法。
这里有下面几点需要注意:
SpringBoot创建Tomcat时,会先创建一个根上下文,webapplicationcontext传给tomcat
启动web容器,要先getWebserver,会创建tomcat的Webserver - 这里会把根上下文作为参数给org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer,这里和tomcat的context进行merge
初始化servletcontext - 会把root上下文放进去,后面初始化dispatcherServlet时,会通过servletcontext拿到根上下文
启动tomcat:调用Tomcat中Host、Engine的启动方法
【DIspatcherServlet的初始化】
初始化DispatcherServlet,是在第一次发起Web请求的时候(AbstractAnnotationConfigDispatcherServletInitializer)
AbstractAnnotationConfigDispatcherServletInitializer
初始化ContextLoaderListener:创建ApplicationContext(根上下文)
初始化DispatcherServlet - 这时需要用ApplicationContext
ApplicationContext是ServletContext的父上下文(WebApplicationContext是Spring为web应用提供的接口)
可以创建多个DispatcherServlet,每个可以创建自己内部的子上下文(每个Servlet都会有一个上下文)
DispatcherServlet建立上下文是为了持有Spring MVC的Bean对象
具体调用过程如下:
javax.servlet.GenericServlet#init(javax.servlet.ServletConfig)
GenericServlet实现了Servlet接口
这个方法启动SpringBoot时会调用一次,初始化dispatcherServlet时还会调用一次,不过两次的config中的serviceClass不一样,第二次是dispatcherServlet
org.springframework.web.servlet.HttpServletBean#init
org.springframework.web.servlet.FrameworkServlet#initServletBean
protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { // A context instance was injected at construction time -> use it wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { // The context has not yet been refreshed -> provide services such as // setting the parent context, setting the application context id, etc if (cwac.getParent() == null) { // The context instance was injected without an explicit parent -> set // the root application context (if any; may be null) as the parent cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // No context instance was injected at construction time -> see if one // has been registered in the servlet context. If one exists, it is assumed // that the parent context (if any) has already been set and that the // user has performed any initialization such as setting the context id wac = findWebApplicationContext(); } if (wac == null) { // No context instance is defined for this servlet -> create a local one wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -> trigger initial onRefresh manually here. synchronized (this.onRefreshMonitor) { //重要的刷新上下文操作 onRefresh(wac); } } if (this.publishContext) { // Publish the context as a servlet context attribute. String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); } return wac; }
获取根上下文(这时根上下文已经在SpringBoot启动时初始化好了)
然后把根上下文给webApplicationContext
org.springframework.web.servlet.DispatcherServlet#onRefresh - 这时就要刷新子上下文了,刷新上下文要刷新HandlerMapping,HandlerAdapter这些
protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context);}
这里记录几个重要的时间点:
这时会打印“Initializing Servlet 'dispatcherServlet'”日志,
org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext
这部分比较重要,附上代码:
【四】SpringMVC和SpringBoot在启动过程中不同点归纳
关于SpringMVC、SpringBoot与Tomcat合作中,启动方式不同点的总结如下
SpringMVC
先启动tomcat,tomcat会去找web.xml,找到的是DispatcherServlet
初始化DispatcherServlet的上下文,要从ServletContext中找根上下文,这时是没有的
创建根上下文
Springboot
在启动过程中,SpringBoot会将DispatcherServlet给到Tomcat的Service的defaultWrapper中(如何给到的后面会说明)
Tomcat启动时,就会把“/”路径和DispatcherServlet匹配
当第一次发起Web请求时,初始化DispatcherServlet,包括HandlerMapping,HandlerAdapter
长按订阅更多精彩▼
如有收获,点个在看,诚挚感谢
深入分析SpringBoot源码如何内嵌Tomcat容器?相关推荐
- Springboot源码分析之内嵌tomcat源码分析
Springboot源码是内嵌tomcat的,这个和完整的tomcat还是不同. 内嵌tomcat的源码在tomcat-embed-core等3个jar包里 展开tomcat-embed-core的c ...
- 精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现
概述 我们知道 Spring Boot 能够创建独立的 Spring 应用,内部嵌入 Tomcat 容器(Jetty.Undertow),让我们的 jar 无需放入 Servlet 容器就能直接运行. ...
- SpringBoot源码分析之内置Servlet容器
原文链接:http://fangjian0423.github.io/2017/05/22/springboot-embedded-servlet-container/ SpringBoot内置了Se ...
- springboot源码分析
快速开发底层原理 SpringBoot核心理念 能够实现帮助开发者快速的整合第三方框架(Spring.Mybatis.hibernate) 原理:Maven依赖封装整合和自定义starter. 完全去 ...
- java tomcat源码_详解Tomcat系列(一)-从源码分析Tomcat的启动
在整个Tomcat系列文章讲解之前, 我想说的是虽然整个Tomcat体系比较复杂, 但是Tomcat中的代码并不难读, 只要认真花点功夫, 一定能啃下来. 由于篇幅的原因, 很难把Tomcat所有的知 ...
- SpringBoot源码笔记分析
SpringBoot源码笔记分析 1.基础 1.配置SpringBoot热部署 1.引入依赖 <dependency><groupId>org.springframework. ...
- 外观模式源码解析(springjdbc+myabtis+tomcat)
我们首先看一下外观模式在SpringJDBC中的一些应用,我们看一下JdbcUtils,为了更好理解,把外观模式重新讲了一下,用积分的场景,我们直接继续看源码,这个是spring.jdbc包下的,这个 ...
- 深入分析Ribbon源码分析
本文来分析下Ribbon源码 文章目录 Ribbon源码分析 负载均衡器 AbstractLoadBalancer BaseLoadBalancer DynamicServerListLoadBala ...
- 不会部署并调试SpringBoot源码?一看必会IDEA操作
专题系列分类:玩转SpringBoot2.x系列教程 前言 你是否遇在面试中遇到如下提问: 面试官:了解过SpringBoot 的源码吗 ? 我:- 没有. 面试官:下一位. 当你从面试公司出来后,你 ...
最新文章
- 模板 - Min_25 筛法求素数和
- python数据趋势算法_Python数据拟合与广义线性回归算法学习
- 用进化算法来优化SVM的参数C和Gamma——利用SCOOP库进行分布式加速计算
- torch的拼接函数_从零开始深度学习Pytorch笔记(13)—— torch.optim
- java安卓获取mac_android开发分享以编程方式获取Android设备的MAC
- Oracle学习(十三)优化专题 【持续更新】
- C++ Vector 使用心得
- 12v电流表的正确接法_滑动变阻器的分压与限流接法
- 环形buffer缓冲区
- python中条件、循环等
- Python基础语法案例(Fibonacci):选择结构、循环结构、异常处理结构、代码优化
- Eclipse添加JBOSS支持
- 菜鸟的草缸 篇五:五个月的草缸血泪史:除藻-鱼病-翻缸的经验(完结篇)
- 2009 中国协同软件机遇年?
- 【有奖征询】可查询商票及企业境外债软件有奖征询
- 正则表达式,和python re模块
- dell 730xd硬raid配置
- Salesforce的V2MOM工作法:明确企业在做什么
- 微信小程序标题栏加logo–基于IView-weapp
- HOJ 2786 Convert Kilometers to Miles
热门文章
- 小雨坐地铁--[最短路分层建图+虚点]
- 求一个数的因子个数/因子和/质因子 C/C++实现
- 树上动态插点 ---- F. Imbalance Value of a Tree(树上动态插点 + 并查集)
- 服务器群装系统,手把手教你安装及配置服务器集群系统
- 【搜索专题】BFS中的多源BFS-双端队列BFS
- angularjs2 html转义,详解Angular.js数据绑定时自动转义html标签及内容
- java字节流6_JavaIO流之字节流
- 树形dp——树的最远距离 hdu2196
- Vue中数组赋值问题
- 七层负载均衡 nginx