SpringBoot项目的启动

当我们在IDE中新建(或导入)了一个SpringBoot项目之后,我们如果想要启动这个SpringBoot项目,我们可以找到相应的带有@SpringBootApplication注解的启动类,该启动类是一个带有main方法的类,这个类就是SpringBoot项目的入口。所以想要运行的话,只需要在IDE对这个类点击Run As Java Application既可以启动。

而当我们需要将写好的代码发布到相应的linux服务器上时。我们一般都是需要将我们的SpringBoot代码打成Jar包(即使是使用ops等自动化运维工具,其本质也是从git拉取到代码,然后mvn clean install打包代码进行发布),然后使用shell脚本执行jar包。我们打开所有的运行SPringBoot项目jar包的shell脚本,你都会发现在start的时候会执行:

java -jar  ****/SpringBoot.jar

实际上这句话最后的效果跟在IDE里对启动类点击Run As Java Application是一样,都是为了执行启动类中的main方法,从而完成SpringBoot初始化过程。
那么直接点击启动的方式当然好理解,但是你有想过为什么执行了java -jar命令后就能够执行启动类的main方法吗?
接下来,我们就先来探究这个问题。

SpringBoot jar的启动原理分析

  • 基础知识准备

0.java -jar命令到底做了什么

在jar包中,我们会在目录META-INF里发现一个叫做MANIFEST.MF的文件,在该文件中,有一个叫Main-Class的特殊条目。
而java -jar命令就是用来执行这个Main-Class指定的类。

  • 解析SpringBoot jar的结构

我们在对SpringBoot项目进行打包的时候,我们会执行Maven的mvn clean install命令,打完包后,你会在target目录上发现如下几个文件:

xxxx.jar
xxx.jar.original
xxx-sources.jar

最后一个文件很好理解,就是这个项目的源码jar。而第一个文件是在spring boot插件的机制下,将一个普通的jar打成了一个可以执行的jar包,而xxx.jar.original则是maven打出的jar包,

jar包结构

我们接着来看下xxx.jar这个spring boot插件作用下打出来的jar包的结构,解压后,结构如下:

.
├── BOOT-INF
│   ├── classes
│   │   ├── application-dev.properties
│   │   ├── application-prod.properties
│   │   ├── application.properties
│   │   ├── com
│   │   │   └── weibangong
│   │   │       └── open
│   │   │           └── openapi
│   │   │               ├── SpringBootWebApplication.class
│   │   │               ├── config
│   │   │               │   ├── ProxyServletConfiguration.class
│   │   │               │   └── SwaggerConfig.class
│   │   │               ├── oauth2
│   │   │               │   ├── controller
│   │   │               │   │   ├── AccessTokenController.class
│   │   ├── logback-spring.xml
│   │   └── static
│   │       ├── css
│   │       │   └── guru.css
│   │       ├── images
│   │       │   ├── FBcover1200x628.png
│   │       │   └── NewBannerBOOTS_2.png
│   └── lib
│       ├── accessors-smart-1.1.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.weibangong.open
│           └── open-server-openapi
│               ├── pom.properties
│               └── pom.xml
└── org└── springframework└── boot└── loader├── ExecutableArchiveLauncher$1.class├── ExecutableArchiveLauncher.class├── JarLauncher.class├── LaunchedURLClassLoader$1.class├── LaunchedURLClassLoader.class├── Launcher.class├── archive│   ├── Archive$Entry.class│   ├── Archive$EntryFilter.class│   ├── Archive.class│   ├── ExplodedArchive$1.class│   ├── ExplodedArchive$FileEntry.class│   ├── ExplodedArchive$FileEntryIterator$EntryComparator.class├── ExplodedArchive$FileEntryIterator.class

这个jar除了我们写的应用程序打出的class以外还有一个单独的org包,应该是spring boot应用在打包的使用spring boot插件将这个package打进来,也就是增强了mvn生命周期中的package阶段,而正是这个包在启动过程中起到了关键的作用,另外把jar中将应用所需的各种依赖都打进来,并且打入了spring boot额外的package,这种可以all-in-one的jar也被称之为fat.jar,下文我们将一直以fat.jar来代替打出的jar的名字。

MANIFEST.MF文件
之前在提到java -jar命令的时候已经提到了这个文件了,那我们现在赶紧来看看Main-class是哪个,会是SpringBoot的启动类吗,如果直接就是SpringBoot启动类的话,那一切就解决了。

Manifest-Version: 1.0
Implementation-Title: iyourcar-service-game-carshow
Implementation-Version: 1.1.1
Archiver-Version: Plexus Archiver
Built-By: cc
Implementation-Vendor-Id: com.iyourcar
Spring-Boot-Version: 1.5.10.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.PropertiesLauncher
Start-Class: com.iyourcar.App
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_144
Implementation-URL: http://projects.spring.io/spring-boot/iyourcar-parent-app/iyourcar-service-game-carshow/

我们可以看到Main-Class不是SpringBoot启动类,但是Start-Class是,那么我们就可以猜想,在执行Main-Class: org.springframework.boot.loader.PropertiesLauncher的main方法的过程中,会去执行Start-Class:com.iyourcar.App。这样就可以进行SpringBoot项目启动过程。
到底是不是这样呢,我们分析源码

  • 启动原理分析
    从上面的分析中,我们已经得到了java -jar命令执行的类是org.springframework.boot.loader.PropertiesLauncher

我们来看看这个类的main方法。

public static void main(String[] args) throws Exception {PropertiesLauncher launcher = new PropertiesLauncher();args = launcher.getArgs(args);launcher.launch(args);
}

launcher方法
这个方法在父类Launcher中,找到父类方法launch(String[] args)方法

 protected void launch(String[] args)throws Exception{JarFile.registerUrlProtocolHandler();//找到自定义类加载器ClassLoader classLoader = createClassLoader(getClassPathArchives());//launch(args, getMainClass(), classLoader);}

launch(args, getMainClass(), classLoader);

protected void launch(String[] args, String mainClass, ClassLoader classLoader)throws Exception{//设置当前线程的ClassLoader为SpringBoot插件中指定的自定义ClassLoaderThread.currentThread().setContextClassLoader(classLoader);createMainMethodRunner(mainClass, args, classLoader).run();}

我们查看源码可以发现,这个mainClass就是MANIFEST.MF文件中Start-Class的值。

getMainClass()
该方法是Launcher类中的一个抽象方法,实际实现在PropertiesLauncher类中。

 protected String getMainClass()throws Exception{String mainClass = getProperty("loader.main", "Start-Class");if (mainClass == null) {throw new IllegalStateException("No 'loader.main' or 'Start-Class' specified");}return mainClass;}private String getProperty(String propertyKey, String manifestKey)throws Exception{return getProperty(propertyKey, manifestKey, null);}private String getProperty(String propertyKey, String manifestKey, String defaultValue)throws Exception{if (manifestKey == null){manifestKey = propertyKey.replace('.', '-');manifestKey = toCamelCase(manifestKey);}String property = SystemPropertyUtils.getProperty(propertyKey);if (property != null){String value = SystemPropertyUtils.resolvePlaceholders(this.properties, property);debug("Property '" + propertyKey + "' from environment: " + value);return value;}if (this.properties.containsKey(propertyKey)){String value = SystemPropertyUtils.resolvePlaceholders(this.properties, this.properties.getProperty(propertyKey));debug("Property '" + propertyKey + "' from properties: " + value);return value;}try{if (this.home != null){Manifest manifest = new ExplodedArchive(this.home, false).getManifest();if (manifest != null){String value = manifest.getMainAttributes().getValue(manifestKey);if (value != null){debug("Property '" + manifestKey + "' from home directory manifest: " + value);return SystemPropertyUtils.resolvePlaceholders(this.properties, value);}}}}catch (IllegalStateException localIllegalStateException) {}Manifest manifest = createArchive().getManifest();if (manifest != null){String value = manifest.getMainAttributes().getValue(manifestKey);if (value != null){debug("Property '" + manifestKey + "' from archive manifest: " + value);return SystemPropertyUtils.resolvePlaceholders(this.properties, value);}}return defaultValue == null ? defaultValue : SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue);}

我们接着回到launch(args, getMainClass(), classLoader);方法,它最终调用了createMainMethodRunner方法,后者实例化了MainMethodRunner对象并运行了run方法,我们转到MainMethodRunner源码中。

public class MainMethodRunner {private final String mainClassName;private final String[] args;public MainMethodRunner(String mainClass, String[] args) {this.mainClassName = mainClass;this.args = args == null?null:(String[])args.clone();}public void run() throws Exception {//这里将之前存入到当前线程的自定义ClassLoader取出来去加载com.iyourcar.App这个类Class mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);Method mainMethod = mainClass.getDeclaredMethod("main", new Class[]{String[].class});mainMethod.invoke((Object)null, new Object[]{this.args});}
}

我们查看run方法,很明显的看到执行了Start-Class:com.iyourcar.App的main方法,到这里,SpringBoot jar启动的过程其实就分析完了。

SrpingBoot jar启动过程中的自定义ClassLoader

在之前的分析中,我们已经知道了SpringBoot项目入口启动类是由SpringBoot插件中的自定义ClassLoader进行类加载的。

//找到自定义类加载器
ClassLoader classLoader = createClassLoader(getClassPathArchives());
这行代码作为入口分析

 protected ClassLoader createClassLoader(List<Archive> archives)throws Exception{//将archives的url都添加到自定义ClassLoader中,我们要知道archives中都包含哪些URLList<URL> urls = new ArrayList(archives.size());for (Archive archive : archives) {urls.add(archive.getUrl());}return createClassLoader((URL[])urls.toArray(new URL[urls.size()]));}protected ClassLoader createClassLoader(URL[] urls)throws Exception{return new LaunchedURLClassLoader(urls, getClass().getClassLoader());}

先找到parent Archive

  protected final Archive createArchive()throws Exception{//获取JAR包所在路径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);}
}

这段代码是通过得到JAR包(或者Java程序)所在路径,接着用路径来封装一个Archvie。最后,我们得到的是new ExplodedArchive(root);

 protected List<Archive> getClassPathArchives()throws Exception{//paths 当没有设置loader.path这个系统属性的时候,是一个空的ListList<Archive> lib = new ArrayList();//paths是一个空的listfor (String path : this.paths) {for (Archive archive : getClassPathArchives(path)) {if ((archive instanceof ExplodedArchive)){List<Archive> nested = new ArrayList(archive.getNestedArchives(new ArchiveEntryFilter(null)));nested.add(0, archive);lib.addAll(nested);}else{lib.add(archive);}}}//到这里都是一个空的lib,实际上就是addNestedEntries这个方法添加了URLaddNestedEntries(lib);return lib;}private void addNestedEntries(List<Archive> lib){try{lib.addAll(this.parent.getNestedArchives(new Archive.EntryFilter(){public boolean matches(Archive.Entry entry){if (entry.isDirectory()) {return entry.getName().equals("BOOT-INF/classes/");}return entry.getName().startsWith("BOOT-INF/lib/");}}));}catch (IOException localIOException) {}}

上述代码其实就是程序帮我们找到需要使用自定义ClassLoader加载的URL。
当我们没有设置loader.path这个系统属性的时候,我们拿到的paths其实是空的。
这种情况下,实际上注入URL只有addNestedEntries方法,结合ExplodedArchive源码,可以分析得到,添加的URL,包括BOOT-INF/classes下的所有文件(真正有效的是classes下的class文件),以及BOOT-INF/lib下的所有jar文件。

SpringBoot jar启动过程中的自定义ClassLoader

  protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{Handler.setUseFastConnectionExceptions(true);try{try{definePackageIfNecessary(name);}catch (IllegalArgumentException ex){if (getPackage(name) == null) {throw new AssertionError("Package " + name + " has already been defined but it could not be found");}}return super.loadClass(name, resolve);}finally{Handler.setUseFastConnectionExceptions(false);}}private void definePackageIfNecessary(String className){int lastDot = className.lastIndexOf('.');if (lastDot >= 0){String packageName = className.substring(0, lastDot);if (getPackage(packageName) == null) {try{definePackage(className, packageName);}catch (IllegalArgumentException ex){if (getPackage(packageName) == null) {throw new AssertionError("Package " + packageName + " has already been defined but it could not be found");}}}}}private void definePackage(final String className, final String packageName){try{AccessController.doPrivileged(new PrivilegedExceptionAction(){public Object run()throws ClassNotFoundException{String packageEntryName = packageName.replace('.', '/') + "/";String classEntryName = className.replace('.', '/') + ".class";for (URL url : LaunchedURLClassLoader.this.getURLs()) {try{URLConnection connection = url.openConnection();if ((connection instanceof JarURLConnection)){java.util.jar.JarFile jarFile = ((JarURLConnection)connection).getJarFile();if ((jarFile.getEntry(classEntryName) != null) && (jarFile.getEntry(packageEntryName) != null) && (jarFile.getManifest() != null)){LaunchedURLClassLoader.this.definePackage(packageName, jarFile.getManifest(), url);return null;}}}catch (IOException localIOException) {}}return null;}}, AccessController.getContext());}catch (PrivilegedActionException localPrivilegedActionException) {}}

这一部分其实就是springboot扩展URLClassLoader实现嵌套jar加载,这部分需要找个时间另写一篇文章来梳理,内容还是挺多的。

在看这部分的时候,我一直带着一个疑问,BOOT-INF/classes下面的文件都是class文件,应该是可以使用AppClassLoader(java 命令 加了-jar 参数以后,AppClassloader就只关注xxx.jar范围内的class了)这个类加载器直接加载的,那为什么需要使用自定义加载器来加载SpringBoot项目的启动入口类呢?
其实答案是很简单的,只是我当时钻牛角尖了。因为启动了这个入口类之后,我们后续是会这个入口类去加载Spring等BooT-INF/lib下面的jar包里的class的,而要加载这些类,我们是必须要用到这个自定义ClassLoader才可以对这种jar-in-jar格式里的class进行类加载的。所以也就导致了入口类是必须使用这个ClassLoader来进行类加载的,否则将无法加载程序中使用到的jar包里的类,肯定会导致报错。
如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B 。这些jar包是需要自定义加载器去处理的,就算没有使用jar-in-jar的结构,这些jar包也是需要使用自定义加载器去处理,因为这些jar包里的资源,AppClassLoader以及其祖先类加载器(ExtClassLoader ,BootStrapClassLoader )都是无法加载的(虽然说是祖先类加载器,但是并不是继承关系,只是通过parent属性注入关联了这个类加载树))。

另,在查看Jar包中的Manifest.MF文件的时候。我发现公司的两个项目的Mian-Class条目对应的值是不一样的。
project1

Main-Class: org.springframework.boot.loader.PropertiesLauncher

project2

Main-Class: org.springframework.boot.loader.JarLauncher

这就引起了我的疑问,为什么会不一样呢?
先给出导致这个结果的原因。

之所以我们打出的Jar包会和之前的Jar包的结构不一样,是因为打包的时候使用了springboot插件导致的。springboot插件增强了maven package(只对maven打包工具而言)。而这个springboot插件的设置其实就在pom文件中。
基本配置如下:

    <plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>

插件是可以在parent pom.xml中的<pluginManagement>节点进行统一配置的,我们公司也是如此。
而造成两个项目的Main-Class不一样的原因就是两个项目使用的parent pom文件的版本不一样。
project1中的parent pom.xml

 <plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><goals><goal>repackage</goal></goals></execution></executions><configuration><mainClass>${start-class}</mainClass><layout>ZIP</layout><addResources>true</addResources><executable>true</executable><fork>true</fork></configuration></plugin></plugins>

project2 的parent pom.xml

     <pluginManagement><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><executions><execution><goals><goal>repackage</goal></goals></execution></executions></plugin></plugins></pluginManagement>

你会发现,project1中配置了<layout>ZIP</layout>
正是因为这个配置,更改了Main-Class的值为org.springframework.boot.loader.PropertiesLauncher。

那到底PropertiesLauncher和JarLauncher有什么区别呢?为什么要有这两种不同的配置呢?
通过源码分析,我们会发现,PropertiesLauncher可以算是JarLauncher的一种扩展,是一种功能上的增强(这两个都用来启动springboot项目)。PropertiesLauncher允许通过配置loader.path使得jar包可以去加载外部的jar。而JarLauncher只能处理jar-in-jar这种模式。通常来说使用JarLauncher作为Main-Class被称为fat-jar,而PropertiesLauncher则可以将fat-jar变成thin-jar,之所以可以这样做,正是应为PropertiesLauncher使得jar包可以去加载外部的jar。

当Java虚拟机要加载第一个类的时候,加载过程:

(1). 首先当前线程的类加载器去加载线程中的第一个类(当前线程的类加载器:Thread类中有一个get/setContextClassLoader(ClassLoader cl);方法,可以获取/指定本线程中的类加载器)

(2). 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B

(3). 还可以直接调用ClassLoader.loadClass(String className)方法来指定某个类加载器去加载某个类
每个类加载器加载类时,又先委托给其上级类加载器当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则会抛出ClassNotFoundException,不是再去找发起者类加载器的儿子,因为没有getChild()方法。

SpringBoot Jar启动相关推荐

  1. SpringBoot - 探究Spring Boot应用是如何通过java -jar 启动的

    文章目录 Pre 引导 新建工程 打包 启动 java -jar 干啥的 打包插件 spring-boot-maven-plugin简介 包结构 META-INF内容 Archive的概念 JarFi ...

  2. linux停止jar程序,Linux 启动停止SpringBoot jar 程序部署Shell 脚本的方法

    废话不多说了,先给大家上代码,具体代码如下所示: #!/bin/bash cd `dirname $0` CUR_SHELL_DIR=`pwd` CUR_SHELL_NAME=`basename ${ ...

  3. java jar 启动项目,SpringBoot项目运行jar包启动的步骤流程解析

    SpringBoot项目在开发中,方便快捷,有一点原因就是SpringBoot项目可以打jar包运行:把jar包直接扔服务器上,然后运行jar包就能访问项目接口了.下面介绍SpringBoot项目打j ...

  4. springboot在启动jar由于配置hibernate的映射文件上classpath导致的!BOOT-INF/classes/!路径出现!号问题解决方法

    springboot在启动jar由于配置hibernate的映射文件上classpath导致的!BOOT-INF/classes/!路径出现!号问题解决方法 参考文章: (1)springboot在启 ...

  5. SpringBoot部署子工程java -jar启动时报错:xxxxxxx.jar中没有主清单属性

    1. 问题背景 项目结构:SpringBoot搭建的父子工程 本地开发环境:windows环境启动子工程正常 服务器部署环境:linux环境采用java -jar的方式进行服务器部署时,启动报错:xx ...

  6. linux启动脚本springboot,Linux 启动停止SpringBoot jar 程序部署Shell 脚本的方法

    废话不多说了,先给大家上代码,具体代码如下所示: #!/bin/bash cd `dirname $0` cur_shell_dir=`pwd` cur_shell_name=`basename ${ ...

  7. linux启动jar后回到根目录,SpringBoot 打包 Jar 启动后,获得jar包所在目录,SpringBoot获取根目录...

    获取根目录五种方法 //第一种 File path = new File(ResourceUtils.getURL("classpath:").getPath()); if (!p ...

  8. 硬核艿艿,新鲜出炉,直接带你弄懂 Spring Boot Jar 启动原理!

    " 摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/jar/ 「芋道源码」欢迎转载,保留摘要,谢谢! 1. 概述 2. MANIFEST.MF 3. J ...

  9. java -jar 启动程序/设置classpath

    目录 前言 java 类加载器与路径 java 设置路径的方法 设置 bootclasspath 设置 Extensions JAR files 设置 classpath 测试程序 java -jar ...

最新文章

  1. 社会工程学到底有多可怕
  2. Spring中经典的9种设计模式,一定要记牢,Java基础教程pdf百度云
  3. python对话框机制_Chromium 新的弹窗机制以及 HTML 的 dialog 元素
  4. 【STM32】GPIO模拟I2C程序示例
  5. SimpleExecutor.doQuery()-执行的StatementHandler 的query()方法
  6. android custom toast,Android自定义Toast
  7. 产品战略规划十步法ppt_从管理咨询角度谈如何系统地做产品战略规划?
  8. ubuntu java环境变量_ubuntu配置java环境变量
  9. 计算机考研补录,考研补录是什么意思 需要考试吗
  10. 手机模拟器自带root_VMOS Pro Android 手机上的模拟器 (手机版虚拟机)
  11. 如何将Ant Design Icon本地化
  12. 第11章 UART串口通信 练习题
  13. 数据治理-数据质量管理
  14. 【调剂】中国舰船研究院本部(北京)2023年硕士研究生调剂招生简章
  15. vue中实现文字超过2行... 展开-收起(兼容ie)
  16. Java调用Zebra800条码打印机
  17. 伪元素在父元素中居中_为什么第1号元素是宇宙中最多的元素?
  18. VAPS XT开发入门教程02:安装配置
  19. MFC的Dlg和App什么区别?应用程序类与对话框类
  20. 《基于卷积神经网络的深度迁移学习,用于燃气轮机燃烧室的故障检测》论文阅读

热门文章

  1. 一文搞懂毕业论文格式规范【超详细!!!】
  2. 小型无线电力传输系统
  3. html网页获取点击按钮获取当前时间
  4. 2018第三届中国青年健康论坛——生理、心理与教育(CYHF 2018)
  5. oracle日期计算
  6. 关于侧扫声呐中地貌描述问题
  7. java编写铝材公式_铝材的重量和单价的计算公式
  8. 地图软件--导入kml
  9. Apache ShardingSphere 代码格式化实战 —— Spotless
  10. 推荐5款设计软件,总能找到你爱的