Springboot 应用启动分析
https://blog.csdn.net/hengyunabc/article/details/50120001#comments
一,spring boot quick start
在spring boot里,很吸引人的一个特性是可以直接把应用打包成为一个jar/war,然后这个jar/war是可以直接启动的,不需要另外配置一个Web Server。
如果之前没有使用过spring boot可以通过下面的demo来感受下。
下面以这个工程为例,演示如何启动Spring boot项目:
git clone git@github.com:hengyunabc/spring-boot-demo.git mvn spring-boot-demo java -jar target/demo-0.0.1-SNAPSHOT.jar
如果使用的IDE是spring sts或者idea,可以通过向导来创建spring boot项目。
二,对spring boot的两个疑问
刚开始接触spring boot时,通常会有这些疑问
- spring boot如何启动的?
- spring boot embed tomcat是如何工作的? 静态文件,jsp,网页模板这些是如何加载到的?
下面来分析spring boot是如何做到的。
打包为单个jar时,spring boot的启动方式
maven打包之后,会生成两个jar文件:
demo-0.0.1-SNAPSHOT.jar demo-0.0.1-SNAPSHOT.jar.original
其中demo-0.0.1-SNAPSHOT.jar.original是默认的maven-jar-plugin生成的包。
demo-0.0.1-SNAPSHOT.jar是spring boot maven插件生成的jar包,里面包含了应用的依赖,以及spring boot相关的类。下面称之为fat jar。
2.1 先来查看spring boot打好的包的目录结构(不重要的省略掉):
1 ├── META-INF 2 │ ├── MANIFEST.MF 3 ├── application.properties 4 ├── com 5 │ └── example 6 │ └── SpringBootDemoApplication.class 7 ├── lib 8 │ ├── aopalliance-1.0.jar 9 │ ├── spring-beans-4.2.3.RELEASE.jar 10 │ ├── ... 11 └── org 12 └── springframework 13 └── boot 14 └── loader 15 ├── ExecutableArchiveLauncher.class 16 ├── JarLauncher.class 17 ├── JavaAgentDetector.class 18 ├── LaunchedURLClassLoader.class 19 ├── Launcher.class 20 ├── MainMethodRunner.class 21 ├── ...
2.1.1 MANIFEST.MF
1 Manifest-Version: 1.0 2 Start-Class: com.example.SpringBootDemoApplication 3 Implementation-Vendor-Id: com.example 4 Spring-Boot-Version: 1.3.0.RELEASE 5 Created-By: Apache Maven 3.3.3 6 Build-Jdk: 1.8.0_60 7 Implementation-Vendor: Pivotal Software, Inc. 8 Main-Class: org.springframework.boot.loader.JarLauncher
可以看到有Main-Class是org.springframework.boot.loader.JarLauncher ,这个是jar启动的Main函数。
还有一个Start-Class是com.example.SpringBootDemoApplication,这个是我们应用自己的Main函数。
@SpringBootApplication
public class SpringBootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootDemoApplication.class, args);
}
}
2.1.2 com/example 目录
这下面放的是应用的.class文件。
2.1.3 lib目录
这里存放的是应用的Maven依赖的jar包文件。
比如spring-beans,spring-mvc等jar。
2.1.4 org/springframework/boot/loader 目录
这下面存放的是Spring boot loader的.class文件。
2.2 Archive的概念
- 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。
public abstract class Archive { public abstract URL getUrl(); public String getMainClass(); public abstract Collection<Entry> getEntries(); public abstract List<Archive> getNestedArchives(EntryFilter filter);
可以看到Archive有一个自己的URL,比如:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/
还有一个getNestedArchives函数,这个实际返回的是demo-0.0.1-SNAPSHOT.jar/lib下面的jar的Archive列表。它们的URL是:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/aopalliance-1.0.jar jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar
2.3 JarLauncher
从MANIFEST.MF可以看到Main函数是JarLauncher,下面来分析它的工作流程。
JarLauncher类的继承结构是:
class JarLauncher extends ExecutableArchiveLauncher
class ExecutableArchiveLauncher extends Launcher
2.3.1 以demo-0.0.1-SNAPSHOT.jar创建一个Archive:
JarLauncher先找到自己所在的jar,即demo-0.0.1-SNAPSHOT.jar的路径,然后创建了一个Archive。
下面的代码展示了如何从一个类找到它的加载的位置的技巧:
protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource == null ? null : codeSource.getLocation().toURI()); String path = (location == null ? null : location.getSchemeSpecificPart()); if (path == null) { throw new IllegalStateException("Unable to determine code source archive"); } File root = new File(path); if (!root.exists()) { throw new IllegalStateException( "Unable to determine code source archive from " + root); } return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); }
2.3.2 获取lib/下面的jar,并创建一个LaunchedURLClassLoader
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函数。
1 /** 2 * Launch the application given the archive file and a fully configured classloader. 3 */ 4 protected void launch(String[] args, String mainClass, ClassLoader classLoader) 5 throws Exception { 6 Runnable runner = createMainMethodRunner(mainClass, args, classLoader); 7 Thread runnerThread = new Thread(runner); 8 runnerThread.setContextClassLoader(classLoader); 9 runnerThread.setName(Thread.currentThread().getName()); 10 runnerThread.start(); 11 } 12 13 /** 14 * Create the {@code MainMethodRunner} used to launch the application. 15 */ 16 protected Runnable createMainMethodRunner(String mainClass, String[] args, 17 ClassLoader classLoader) throws Exception { 18 Class<?> runnerClass = classLoader.loadClass(RUNNER_CLASS); 19 Constructor<?> constructor = runnerClass.getConstructor(String.class, 20 String[].class); 21 return (Runnable) constructor.newInstance(mainClass, args); 22 }
2.3.3 LaunchedURLClassLoader
LaunchedURLClassLoader和普通的URLClassLoader的不同之处是,它提供了从Archive里加载.class的能力。
结合Archive提供的getEntries函数,就可以获取到Archive里的Resource。当然里面的细节还是很多的,下面再描述。
2.4 spring boot应用启动流程总结
看到这里,可以总结下Spring Boot应用的启动流程:
- spring boot应用打包之后,生成一个fat jar,里面包含了应用依赖的jar包,还有Spring boot loader相关的类
- Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader来加载/lib下面的jar,并以一个新线程启动应用的Main函数。
2.5 JarFile URL的扩展
Spring boot能做到以一个fat jar来启动,最重要的一点是它实现了jar in jar的加载方式。
JDK原始的JarFile URL的定义可以参考这里:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/
jar包里的资源的URL:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/com/example/SpringBootDemoApplication.class
可以看到对于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:
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar!/META-INF/MANIFEST.MF
2.6 自定义URLStreamHandler,扩展JarFile和JarURLConnection
在构造一个URL时,可以传递一个Handler,而JDK自带有默认的Handler类,应用可以自己注册Handler来处理自定义的URL。
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。
2.7 ClassLoader如何读取到Resource
对于一个ClassLoader,它需要哪些能力?
- 查找资源
- 读取资源
对应的API是:
public URL findResource(String name)
public InputStream getResourceAsStream(String name)
上面提到,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
在IDE/开放目录启动Spring boot应用
在上面只提到在一个fat jar里启动Spring boot应用的过程,下面分析IDE里Spring boot是如何启动的。
在IDE里,直接运行的Main函数是应用自己的Main函数:
@SpringBootApplication public class SpringBootDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringBootDemoApplication.class, args); } }
其实在IDE里启动Spring boot应用是最简单的一种情况,因为依赖的Jar都让IDE放到classpath里了,所以Spring boot直接启动就完事了。
还有一种情况是在一个开放目录下启动Spring boot启动。所谓的开放目录就是把fat jar解压,然后直接启动应用。
java org.springframework.boot.loader.JarLauncher
这时,Spring boot会判断当前是否在一个目录里,如果是的,则构造一个ExplodedArchive(前面在jar里时是JarFileArchive),后面的启动流程类似fat jar的。
spirng boot的一些缺点:
当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等都是深爱开发者喜爱的项目、特性。几乎可以肯定设计者是有丰富的一线开发经验,深知开发人员的痛点。
转载;https://blog.csdn.net/hengyunabc/article/details/50120001#comments