Spring-Boot啓動以前作了哪些事?

Spring Boot Jar文件探究

初始化一個Spring 應用,添加以下依賴java

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.fxipp.spring</groupId>
    <artifactId>first-app-by-gui</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>first-app-by-gui</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

</project>複製代碼

執行mvn package命令打包,查看jar包的目錄結構web

.
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── com
│   │       └── fxipp
│   │           └── spring
│   │               └── FirstAppByGuiApplication.class
│   └── lib
│       ├── classmate-1.4.0.jar
│       ├── hibernate-validator-6.0.17.Final.jar
│       ├── jackson-annotations-2.9.0.jar
│       ├── jackson-core-2.9.9.jar
│       ......
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.fxipp.spring
│           └── first-app-by-gui
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── LaunchedURLClassLoader.class
                ├── ......
                ├── archive
                │   ├── Archive$Entry.class
                │   ├── Archive$EntryFilter.class
                │   ├── Archive.class
                │   ├── ......
                ├── data
                │   ├── RandomAccessData.class
                │   ├── RandomAccessDataFile$1.class
                │   ├──......
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── Bytes.class
                │   ├── ......
                └── util
                    └── SystemPropertyUtils.class
18 directories, 91 files複製代碼

文件結構比較複雜,解釋一下spring

  • BOOT-INF/classes: 存放應用編譯後的class文件;
  • BOOT-INF/lib:class path目錄, 存放應用依賴的jar包;
  • META-INF: 存放應用的元信息,如MANIFEST.MF文件;
  • org:存放Spring Boot自身的class文件;

Jar文件的執行器: Spring Boot Loader

咱們先從MANIFEST.MF文件查看apache

Manifest-Version: 1.0
Implementation-Title: first-app-by-gui
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.fxipp.spring.FirstAppByGuiApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.6.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher複製代碼

裏面記錄了應用的元信息,Spring的版本,應用的版本,Maven的版本,Main-Class等信息。不難發現,MainClass指向的是org.springframework.boot.loader.JarLauncher(如下簡稱JarLauncher),而不是咱們本身編寫的com.fxipp.spring.FirstAppByGuiApplicationapp

JarLauncher從名字看出是一個jar的執行器,他的class文件位於org.springframework.boot.loader目錄下,可見它是Spring自身的class文件。dom

JarLauncher的GAV org.springframework.boot:spring-boot-loader:2.1.6.RELEASEmaven

一般狀況下,他會在spring-boot-starter-parent引入到應用中,既然main-class指向到是JarLauncher,那咱們也能夠直接執行java org.springframework.boot.loader.JarLauncher,也能夠啓動Spring項目的。ide

java org.springframework.boot.loader.JarLauncher

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.6.RELEASE)

2019-06-19 20:30:52.202  INFO 3094 --- [           main] c.fxipp.spring.FirstAppByGuiApplication  : Starting FirstAppByGuiApplication on fangxideMacBook-Pro.local with PID 3094 (/Users/fangxi/Java/workspace/default/spring-boot/first-app-by-gui/target/temp/BOOT-INF/classes started by fangxi in /Users/fangxi/Java/workspace/default/spring-boot/first-app-by-gui/target/temp)複製代碼

既然能夠執行,那就說明了,JarLauncher這個類纔是Spring項目真正的入口。若是咱們執行本身寫的com.fxipp.spring.FirstAppByGuiApplication會怎麼樣?spring-boot

➜  classes java com.fxipp.spring.FirstAppByGuiApplication
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
    at com.fxipp.spring.FirstAppByGuiApplication.main(FirstAppByGuiApplication.java:10)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more複製代碼

啓動報錯,緣由是找不到org.springframework.boot.SpringApplication這個類,說白了就是沒有指定Class Path,Spring Boot應用的Class Path目錄是BOOT-INF/libui

也就是說,JarLauncher能夠執行成功,是由於Spring Boot知道了Class Path的路徑,說明JarLauncher在啓動調用com.fxipp.spring.FirstAppByGuiApplication以前,指定了Class Path的位置。

JarLauncher的代碼以下

public class JarLauncher extends ExecutableArchiveLauncher {

   static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

   static final String BOOT_INF_LIB = "BOOT-INF/lib/";

   public JarLauncher() {
   }

   protected JarLauncher(Archive archive) {
      super(archive);
   }

   @Override
   protected boolean isNestedArchive(Archive.Entry entry) {
      if (entry.isDirectory()) {
         return entry.getName().equals(BOOT_INF_CLASSES);
      }
      return entry.getName().startsWith(BOOT_INF_LIB);
   }

   public static void main(String[] args) throws Exception {
      new JarLauncher().launch(args);
   }

}複製代碼

  • Archive.Entry:這個類對對象,代編jar包中的資源文件。

isNestedArchive方法判斷entry對象是否是位於jar包內,若是在jar內部,返回true。若是不在jar包裏面,也就是咱們解壓了jar包,返回false。

重點看launch(String[])方法

protected void launch(String[] args) throws Exception {
    // 1
        JarFile.registerUrlProtocolHandler();
    // 2
        ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // 3
        launch(args, getMainClass(), classLoader);
    }複製代碼

這個方法一共3步

  1. 擴展JAR協議
    1. JDK默認支持file、http、jar等協議,因此JDK內部有默認的實現,位於sun.net.www.protocol包下。
    2. JarFile.registerUrlProtocolHandler();這個方法將org.springframework.boot.loader包下對應的JAR協議實現,覆蓋原有的JAR實現。
    3. 由於原有的JAR實現,ClassPath是咱們本身配置環境變量的時候制定的,不是BOOT-INF/lib
  2. 建立一個classloader,用於加載JarLauncher類,由於jar包可能會被解壓,解壓前和解壓後的的ClassLoader是不一樣的。
  3. 調用launch方法,將參數傳遞。
    1. args是咱們本身指定的參數。
    2. getMainClass()是獲取MANIFEST.MF文件裏面Statr-Class屬性,也就是獲取咱們自定義主類的Class 文件地址。
    3. 傳遞推出的類加載器

launch方法

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
   Thread.currentThread().setContextClassLoader(classLoader);
   createMainMethodRunner(mainClass, args, classLoader).run();
}

protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
   return new MainMethodRunner(mainClass, args);
}


複製代碼

public class MainMethodRunner {

   private final String mainClassName;

   private final String[] args;

   public MainMethodRunner(String mainClass, String[] args) {
      this.mainClassName = mainClass;
      this.args = (args != null) ? args.clone() : null;
   }

   public void run() throws Exception {
      Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
      Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
      mainMethod.invoke(null, new Object[] { this.args });
   }

}複製代碼

launch方法分析:

  1. 將ClassLoader放入當前線程裏面的ClassLoader裏面
  2. 建立MainMethodRunner對象,調用裏面的run()方法。
    1. run()方法先獲取到以前設定的ClassLoader。
    2. 利用ClassLoader加載Start-Class之類的類,也就是咱們本身的主類。
    3. 獲取主類裏面的main方法,經過反射執行。

總結

經過分析,咱們能夠看出,Spring Boot Loader在調用咱們本身的主類以前,主要作了三件事

  1. 擴展JDK默認的支持JAR對應的協議,由於Spring Boot啓動不單單須要JDK半身的JAR文件,還須要BOOT-INF/lib這個目錄下的文件。默認實現沒法將BOOT-INF/lib這個目錄看成ClassPath,故須要替換實現。
  2. 判斷當前的介質,是java -jar啓動,仍是java org.springframework.boot.loader.JarLauncher啓動。以便獲取對應的ClassLoader。
  3. 獲取MANIFEST.MF文件中的Start-Class屬性,也就是咱們自定義的主類。經過第二步獲取的ClassLoader加載獲取到Class文件,經過反射調用main方法,啓動應用。
相關文章
相關標籤/搜索