天天看点

spring boot应用启动原理分析

在spring boot里,很吸引人的一个特性是可以直接把应用打包成为一个jar/war,然后这个jar/war是可以直接启动的,不需要另外配置一个web server。

如果之前没有使用过spring boot可以通过下面的demo来感受下。

下面以这个工程为例,演示如何启动spring boot项目:

如果使用的ide是spring sts或者idea,可以通过向导来创建spring boot项目。

也可以参考官方教程:

http://docs.spring.io/spring-boot/docs/current-snapshot/reference/htmlsingle/#getting-started-first-application

刚开始接触spring boot时,通常会有这些疑问

spring boot如何启动的?

spring boot embed tomcat是如何工作的? 静态文件,jsp,网页模板这些是如何加载到的?

下面来分析spring boot是如何做到的。

maven打包之后,会生成两个jar文件:

其中demo-0.0.1-snapshot.jar.original是默认的maven-jar-plugin生成的包。

demo-0.0.1-snapshot.jar是spring boot maven插件生成的jar包,里面包含了应用的依赖,以及spring boot相关的类。下面称之为fat jar。

先来查看spring boot打好的包的目录结构(不重要的省略掉):

依次来看下这些内容。

可以看到有main-class是org.springframework.boot.loader.jarlauncher ,这个是jar启动的main函数。

还有一个start-class是com.example.springbootdemoapplication,这个是我们应用自己的main函数。

这下面放的是应用的.class文件。

这里存放的是应用的maven依赖的jar包文件。

比如spring-beans,spring-mvc等jar。

这下面存放的是spring boot loader的.class文件。

archive即归档文件,这个概念在linux下比较常见

通常就是一个tar/zip格式的压缩包

jar是zip格式

在spring boot里,抽象出了archive的概念。

一个archive可以是一个jar(jarfilearchive),也可以是一个文件目录(explodedarchive)。可以理解为spring boot抽象出来的统一访问资源的层。

上面的demo-0.0.1-snapshot.jar 是一个archive,然后demo-0.0.1-snapshot.jar里的/lib目录下面的每一个jar包,也是一个archive。

可以看到archive有一个自己的url,比如:

还有一个getnestedarchives函数,这个实际返回的是demo-0.0.1-snapshot.jar/lib下面的jar的archive列表。它们的url是:

从manifest.mf可以看到main函数是jarlauncher,下面来分析它的工作流程。

jarlauncher类的继承结构是:

jarlauncher先找到自己所在的jar,即demo-0.0.1-snapshot.jar的路径,然后创建了一个archive。

下面的代码展示了如何从一个类找到它的加载的位置的技巧:

jarlauncher创建好archive之后,通过getnestedarchives函数来获取到demo-0.0.1-snapshot.jar/lib下面的所有jar文件,并创建为list。

注意上面提到,archive都是有自己的url的。

获取到这些archive的url之后,也就获得了一个url[]数组,用这个来构造一个自定义的classloader:launchedurlclassloader。

创建好classloader之后,再从manifest.mf里读取到start-class,即com.example.springbootdemoapplication,然后创建一个新的线程来启动应用的main函数。

launchedurlclassloader和普通的urlclassloader的不同之处是,它提供了从archive里加载.class的能力。

结合archive提供的getentries函数,就可以获取到archive里的resource。当然里面的细节还是很多的,下面再描述。

看到这里,可以总结下spring boot应用的启动流程:

spring boot应用打包之后,生成一个fat jar,里面包含了应用依赖的jar包,还有spring boot loader相关的类

fat jar的启动main函数是jarlauncher,它负责创建一个launchedurlclassloader来加载/lib下面的jar,并以一个新线程启动应用的main函数。

代码地址:https://github.com/spring-projects/spring-boot/tree/master/spring-boot-tools/spring-boot-loader

spring boot能做到以一个fat jar来启动,最重要的一点是它实现了jar in jar的加载方式。

jdk原始的jarfile url的定义可以参考这里:

http://docs.oracle.com/javase/7/docs/api/java/net/jarurlconnection.html

原始的jarfile url是这样子的:

jar包里的资源的url:

可以看到对于jar里的资源,定义以’!/’来分隔。原始的jarfile url只支持一个’!/’。

spring boot扩展了这个协议,让它支持多个’!/’,就可以表示jar in jar,jar in directory的资源了。

比如下面的url表示demo-0.0.1-snapshot.jar这个jar里lib目录下面的spring-beans-4.2.3.release.jar里面的manifest.mf:

在构造一个url时,可以传递一个handler,而jdk自带有默认的handler类,应用可以自己注册handler来处理自定义的url。

参考:

https://docs.oracle.com/javase/8/docs/api/java/net/url.html#url-java.lang.string-java.lang.string-int-java.lang.string-

spring boot通过注册了一个自定义的handler类来处理多重jar in jar的逻辑。

这个handler内部会用softreference来缓存所有打开过的jarfile。

在处理像下面这样的url时,会循环处理’!/’分隔符,从最上层出发,先构造出demo-0.0.1-snapshot.jar这个jarfile,再构造出spring-beans-4.2.3.release.jar这个jarfile,然后再构造出指向manifest.mf的jarurlconnection。

对于一个classloader,它需要哪些能力?

查找资源

读取资源

对应的api是:

上面提到,spring boot构造launchedurlclassloader时,传递了一个url[]数组。数组里是lib目录下面的jar的url。

对于一个url,jdk或者classloader如何知道怎么读取到里面的内容的?

实际上流程是这样子的:

launchedurlclassloader.loadclass

url.getcontent()

url.openconnection()

handler.openconnection(url)

最终调用的是jarurlconnection的getinputstream()函数。

从一个url,到最终读取到url里的内容,整个过程是比较复杂的,总结下:

spring boot注册了一个handler来处理”jar:”这种协议的url

spring boot扩展了jarfile和jarurlconnection,内部处理jar in jar的情况

在处理多重jar in jar的url时,spring boot会循环处理,并缓存已经加载到的jarfile

对于多重jar in jar,实际上是解压到了临时目录来处理,可以参考jarfilearchive里的代码

在获取url的inputstream时,最终获取到的是jarfile里的jarentrydata

这里面的细节很多,只列出比较重要的一些点。

然后,urlclassloader是如何getresource的呢?

urlclassloader在构造时,有url[]数组参数,它内部会用这个数组来构造一个urlclasspath:

在 urlclasspath 内部会为这些urls 都构造一个loader,然后在getresource时,会从这些loader里一个个去尝试获取。

如果获取成功的话,就像下面那样包装为一个resource。

从代码里可以看到,实际上是调用了url.openconnection()。这样完整的链条就可以连接起来了。

注意,urlclasspath这个类的代码在jdk里没有自带,在这里看到 http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/7u40-b43/sun/misc/urlclasspath.java#506

在上面只提到在一个fat jar里启动spring boot应用的过程,下面分析ide里spring boot是如何启动的。

在ide里,直接运行的main函数是应用自己的main函数:

其实在ide里启动spring boot应用是最简单的一种情况,因为依赖的jar都让ide放到classpath里了,所以spring boot直接启动就完事了。

还有一种情况是在一个开放目录下启动spring boot启动。所谓的开放目录就是把fat jar解压,然后直接启动应用。

这时,spring boot会判断当前是否在一个目录里,如果是的,则构造一个explodedarchive(前面在jar里时是jarfilearchive),后面的启动流程类似fat jar的。

spring boot在启动时,先通过一个简单的查找servlet类的方式来判断是不是在web环境:

如果是的话,则会创建annotationconfigembeddedwebapplicationcontext,否则spring context就是annotationconfigapplicationcontext:

spring boot通过获取embeddedservletcontainerfactory来启动对应的web服务器。

常用的两个实现类是tomcatembeddedservletcontainerfactory和jettyembeddedservletcontainerfactory。

启动tomcat的代码:

会为tomcat创建一个临时文件目录,如:

/tmp/tomcat.2233614112516545210.8080,做为tomcat的basedir。里面会放tomcat的临时文件,比如work目录。

还会初始化tomcat的一些servlet,比如比较重要的default/jsp servlet:

当spring boot应用被打包为一个fat jar时,是如何访问到web resource的?

实际上是通过archive提供的url,然后通过classloader提供的访问classpath resource的能力来实现的。

比如需要配置一个index.html,这个可以直接放在代码里的src/main/resources/static目录下。

对于index.html欢迎页,spring boot在初始化时,就会创建一个viewcontroller来处理:

像页面模板文件可以放在src/main/resources/template目录下。但这个实际上是模板的实现类自己处理的。比如thymeleafproperties类里的:

jsp页面和template类似。实际上是通过spring mvc内置的jstlview来处理的。

可以通过配置spring.view.prefix来设定jsp页面的目录:

对于错误页面,spring boot也是通过创建一个basicerrorcontroller来统一处理的。

对应的view是一个简单的html提醒:

spring boot的这个做法很好,避免了传统的web应用来出错时,默认抛出异常,容易泄密。

先通过maven-shade-plugin生成一个包含依赖的jar,再通过spring-boot-maven-plugin插件把spring boot loader相关的类,还有manifest.mf打包到jar里。

当在shell里启动spring boot应用时,会发现它的logger输出是有颜色的,这个特性很有意思。

可以通过这个设置来关闭:

原理是通过ansioutputapplicationlistener ,这个来获取这个配置,然后设置logback在输出时,加了一个 colorconverter,通过org.springframework.boot.ansi.ansioutput ,对一些字段进行了渲染。

可以参考launchedurlclassloader里的lockprovider

inputargumentsjavaagentdetector,原理是检测jar的url是否有”-javaagent:”的前缀。

applicationpid,可以获取pid。

spring boot里自己包装了一套logger,支持java, log4j, log4j2, logback,以后有需要自己包装logger时,可以参考这个。

在org.springframework.boot.logging包下面。

通过堆栈里获取的方式,判断main函数,找到原始启动的main函数。

当spring boot应用以一个fat jar方式运行时,会遇到一些问题。以下是个人看法:

日志不知道放哪,默认是输出到stdout的

数据目录不知道放哪, jenkinns的做法是放到 ${user.home}/.jenkins 下面

相对目录api不能使用,servletcontext.getrealpath(“/”) 返回的是null

spring boot应用喜欢把配置都写到代码里,有时会带来混乱。一些简单可以用xml来表达的配置可能会变得难读,而且凌乱。

spring boot通过扩展了jar协议,抽象出archive概念,和配套的jarfile,jarurlconnection,launchedurlclassloader,从而实现了上层应用无感知的all in one的开发体验。尽管executable war并不是spring提出的概念,但spring boot让它发扬光大。

spring boot是一个惊人的项目,可以说是spring的第二春,spring-cloud-config, spring-session, metrics, remote shell等都是深爱开发者喜爱的项目、特性。几乎可以肯定设计者是有丰富的一线开发经验,深知开发人员的痛点。