SpringBoot應用啟動流程
阿新 • • 發佈:2019-01-22
> SpringBoot在微服務上應用是越來越多,同樣教程也比較多,但是我相信會有人跟我一樣的迷惑,它的載入過程是什麼樣的,要經過哪些類,然後又為什麼會能直接把應用打包成jar/war,然後就可以直接執行?本次使用的SpringBoot版本1.5.2。
# 從快速啟動Demo中看載入過程
有很多人都下載了Springboot 版的eclipse,構建專案也比較方便,其實我感覺也挺好的,但是我習慣了用IDEA,所以這次就從官網上初始化一個新的專案
## 準備快速啟動專案
### 在官網初始化一個新的專案
### 執行打包
為了給大家展示jar/war打包的內容,然後我打包了兩次,同時把相關的內容給大家貼出來。
war打完包的目錄結構
區別不大jar打包的時候是BOOT-INF放我們自己開發的程式碼,war打包的時候是WEB-INF。
#### 我們先看打包的META-INF裡的MANIFEST.MF檔案。
> jar MANIFEST.MF
```
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: angy
Implementation-Vendor-Id: com.hiext
Spring-Boot-Version: 1.5.2.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.hiext.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.0.5
Build-Jdk: 1.8.0_111
Implementation-URL: http://projects.spring.io/spring-boot/authorizeServer/
`` `
> war MANIFEST.MF
`` `
Manifest-Version: 1.0
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: angy
Implementation-Vendor-Id: com.hiext
Spring-Boot-Version: 1.5.2.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.hiext.DemoApplication
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Created-By: Apache Maven 3.0.5
Build-Jdk: 1.8.0_111
Implementation-URL: http://projects.spring.io/spring-boot/authorizeServer/
```
從檔案的比較上來看載入的不同的類,然後SpringBoot的程式碼也放在不同的位置。
#### 細說MANIFEST.MF檔案
> 這個 manifest 檔案定義了與擴充套件和包相關的資料,是java打包必不可少的一個檔案。這個檔案也描述了jar包的一些很多資訊,主要分為一下幾類
* 1. 一般屬性
1. Manifest-Version
3. Signature-Version
定義jar檔案的簽名版本
4. Class-Path
應用程式或者類裝載器使用該值來構建內部的類搜尋路徑 * 2. 應用程式相關屬性 * 3. 包擴充套件屬性 我們重點看應用程式相關的屬性,就是Main-class,這是jar的入口類,也就是要求這個類必須是可執行的類。 一旦定義了該屬性即可通過 java -jar x.jar來執行該jar檔案。 Start-Class的配置就是我們自己寫的main函數了。 #### 其他資料夾的介紹 BOOT-INF/WEB-INF 這裡放的是我們自己要寫開發的程式碼然後編譯後的class與相關的jar等等。 #####Archive的概念 * archive即歸檔檔案,這個概念在linux下比較常見 * 通常就是一個tar/zip格式的壓縮包 * jar是zip格式 在spring boot裡,抽象出了Archive的概念。 一個archive可以是一個jar(JarFileArchive),也可以是一個檔案目錄(ExplodedArchive)。可以理解為Spring boot抽象出來的統一訪問資源的層。 我們看下Archive這個介面的定義: ``` package org.springframework.boot.loader.archive; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import java.util.jar.Manifest; import org.springframework.boot.loader.Launcher; /** * An archive that can be launched by the {@link Launcher}. * * @author Phillip Webb * @see JarFileArchive */ public interface Archive extends Iterable<Archive.Entry> { /** * Returns a URL that can be used to load the archive. * @return the archive URL * @throws MalformedURLException if the URL is malformed */ URL getUrl() throws MalformedURLException; /** * Returns the manifest of the archive. * @return the manifest * @throws IOException if the manifest cannot be read */ Manifest getManifest() throws IOException; /** * Returns nested {@link Archive}s for entries that match the specified filter. * @param filter the filter used to limit entries * @return nested archives * @throws IOException if nested archives cannot be read */ List<Archive> getNestedArchives(EntryFilter filter) throws IOException; /** * Represents a single entry in the archive. */ interface Entry { /** * Returns {@code true} if the entry represents a directory. * @return if the entry is a directory */ boolean isDirectory(); /** * Returns the name of the entry. * @return the name of the entry */ String getName(); } /** * Strategy interface to filter {@link Entry Entries}. */ interface EntryFilter { /** * Apply the jar entry filter. * @param entry the entry to filter * @return {@code true} if the filter matches */ boolean matches(Entry entry); } } ``` > Archive的實現有兩個一個是ExplodedArchive 一個是JarFileArchive ,就是對應上面說的兩種訪問資源的形式 一種是資料夾 一種是直接訪問jar。從介面來看,每一個資源都有一個URL。 ```
jar:file:/C:/Users/angy/AppData/Local/Temp/junit7462285095317089505/archive.jar!/BOOT-INF/classes!/ ``` 有了上面資源的保障,下面來看看JarLauncher 在MANIFEST.MF裡可以看到Main函式載入的是JarLauncher,下面來分析它的工作流程 同樣JarLauncher的原始碼在上面的SpringBoot的tools裡,JarLauncher 繼承了ExecutableArchiveLauncher類,然後ExecutableArchiveLauncher又繼承了Launcher類。 從JarLauncher 入手,JarLauncher 在構造物件的時候是呼叫ExecutableArchiveLauncher的構造方法,需要一個Archive的實現。 ``` protected ExecutableArchiveLauncher(Archive archive) { this.archive = archive; } ``` Archive是在Launcher類中建立的 ``` 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)); //返回其他Jar } ``` JarLauncher先找到自己所在的jar,然後通過Launcher來呼叫抽象的方法getClassPathArchives()來找到其他的Jar包。下面是ExecutableArchiveLauncher類getClassPathArchives()的實現。 ``` protected List<Archive> getClassPathArchives() throws Exception { List<Archive> archives = new ArrayList<Archive>( this.archive.getNestedArchives(new EntryFilter() { @Override public boolean matches(Entry entry) { return isNestedArchive(entry); } })); postProcessClassPathArchives(archives); return archives; } ``` 從上面可以看到是通過getNestedArchives函式來獲取到jar包下lib下面的所有jar檔案,並建立為List。然後呼叫Launcher類裡面的createClassLoader方法。用於返回LaunchedURLClassLoader的啟動載入器。 ``` /** * Create a classloader for the specified archives. * @param archives the archives * @return the classloader * @throws Exception if the classloader cannot be created */ protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { List<URL> urls = new ArrayList<URL>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return createClassLoader(urls.toArray(new URL[urls.size()])); } /** * Create a classloader for the specified URLs. * @param urls the URLs * @return the classloader * @throws Exception if the classloader cannot be created */ protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); } ``` 建立好ClassLoader之後,再從MANIFEST.MF裡讀取到Start-Class,即com.hiext.DemoApplication,以下是讀取Start-Class的方法。 ``` @Override protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { mainClass = manifest.getMainAttributes().getValue("Start-Class"); } if (mainClass == null) { throw new IllegalStateException( "No 'Start-Class' manifest entry specified in " + this); } return mainClass; } ``` 獲得到jar的Start-class後通過launch方法來建立執行緒,並執行main函式。 ``` /** * Launch the application given the archive file and a fully configured classloader. * @param args the incoming arguments * @param mainClass the main class to run * @param classLoader the classloader * @throws Exception if the launch fails */ protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { Thread.currentThread().setContextClassLoader(classLoader); createMainMethodRunner(mainClass, args, classLoader).run(); } /** * Create the {@code MainMethodRunner} used to launch the application. * @param mainClass the main class * @param args the incoming arguments * @param classLoader the classloader * @return the main method runner */ protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner(mainClass, args); } ``` 以上是整個Jar的啟動流程,本人是在tools原始碼裡debug下一步一步執行後寫下。程式碼均來之官方的原始碼。 知識碎片,重在整理,路很長,一步一個腳印,就好。
用來定義manifest檔案的版本,例如:Manifest-Version: 1.0
2. Created-By
宣告該檔案的生成者,一般該屬性是由jar命令列工具生成的,例如:Created-By: Apache Ant 1.5.1
3. Signature-Version
定義jar檔案的簽名版本
4. Class-Path
應用程式或者類裝載器使用該值來構建內部的類搜尋路徑 * 2. 應用程式相關屬性 * 3. 包擴充套件屬性 我們重點看應用程式相關的屬性,就是Main-class,這是jar的入口類,也就是要求這個類必須是可執行的類。 一旦定義了該屬性即可通過 java -jar x.jar來執行該jar檔案。 Start-Class的配置就是我們自己寫的main函數了。 #### 其他資料夾的介紹 BOOT-INF/WEB-INF 這裡放的是我們自己要寫開發的程式碼然後編譯後的class與相關的jar等等。 #####Archive的概念 * archive即歸檔檔案,這個概念在linux下比較常見 * 通常就是一個tar/zip格式的壓縮包 * jar是zip格式 在spring boot裡,抽象出了Archive的概念。 一個archive可以是一個jar(JarFileArchive),也可以是一個檔案目錄(ExplodedArchive)。可以理解為Spring boot抽象出來的統一訪問資源的層。 我們看下Archive這個介面的定義: ``` package org.springframework.boot.loader.archive; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.List; import java.util.jar.Manifest; import org.springframework.boot.loader.Launcher; /** * An archive that can be launched by the {@link Launcher}. * * @author Phillip Webb * @see JarFileArchive */ public interface Archive extends Iterable<Archive.Entry> { /** * Returns a URL that can be used to load the archive. * @return the archive URL * @throws MalformedURLException if the URL is malformed */ URL getUrl() throws MalformedURLException; /** * Returns the manifest of the archive. * @return the manifest * @throws IOException if the manifest cannot be read */ Manifest getManifest() throws IOException; /** * Returns nested {@link Archive}s for entries that match the specified filter. * @param filter the filter used to limit entries * @return nested archives * @throws IOException if nested archives cannot be read */ List<Archive> getNestedArchives(EntryFilter filter) throws IOException; /** * Represents a single entry in the archive. */ interface Entry { /** * Returns {@code true} if the entry represents a directory. * @return if the entry is a directory */ boolean isDirectory(); /** * Returns the name of the entry. * @return the name of the entry */ String getName(); } /** * Strategy interface to filter {@link Entry Entries}. */ interface EntryFilter { /** * Apply the jar entry filter. * @param entry the entry to filter * @return {@code true} if the filter matches */ boolean matches(Entry entry); } } ``` > Archive的實現有兩個一個是ExplodedArchive 一個是JarFileArchive ,就是對應上面說的兩種訪問資源的形式 一種是資料夾 一種是直接訪問jar。從介面來看,每一個資源都有一個URL。 ```
jar:file:/C:/Users/angy/AppData/Local/Temp/junit7462285095317089505/archive.jar!/BOOT-INF/classes!/ ``` 有了上面資源的保障,下面來看看JarLauncher 在MANIFEST.MF裡可以看到Main函式載入的是JarLauncher,下面來分析它的工作流程 同樣JarLauncher的原始碼在上面的SpringBoot的tools裡,JarLauncher 繼承了ExecutableArchiveLauncher類,然後ExecutableArchiveLauncher又繼承了Launcher類。 從JarLauncher 入手,JarLauncher 在構造物件的時候是呼叫ExecutableArchiveLauncher的構造方法,需要一個Archive的實現。 ``` protected ExecutableArchiveLauncher(Archive archive) { this.archive = archive; } ``` Archive是在Launcher類中建立的 ``` 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)); //返回其他Jar } ``` JarLauncher先找到自己所在的jar,然後通過Launcher來呼叫抽象的方法getClassPathArchives()來找到其他的Jar包。下面是ExecutableArchiveLauncher類getClassPathArchives()的實現。 ``` protected List<Archive> getClassPathArchives() throws Exception { List<Archive> archives = new ArrayList<Archive>( this.archive.getNestedArchives(new EntryFilter() { @Override public boolean matches(Entry entry) { return isNestedArchive(entry); } })); postProcessClassPathArchives(archives); return archives; } ``` 從上面可以看到是通過getNestedArchives函式來獲取到jar包下lib下面的所有jar檔案,並建立為List。然後呼叫Launcher類裡面的createClassLoader方法。用於返回LaunchedURLClassLoader的啟動載入器。 ``` /** * Create a classloader for the specified archives. * @param archives the archives * @return the classloader * @throws Exception if the classloader cannot be created */ protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { List<URL> urls = new ArrayList<URL>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return createClassLoader(urls.toArray(new URL[urls.size()])); } /** * Create a classloader for the specified URLs. * @param urls the URLs * @return the classloader * @throws Exception if the classloader cannot be created */ protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); } ``` 建立好ClassLoader之後,再從MANIFEST.MF裡讀取到Start-Class,即com.hiext.DemoApplication,以下是讀取Start-Class的方法。 ``` @Override protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { mainClass = manifest.getMainAttributes().getValue("Start-Class"); } if (mainClass == null) { throw new IllegalStateException( "No 'Start-Class' manifest entry specified in " + this); } return mainClass; } ``` 獲得到jar的Start-class後通過launch方法來建立執行緒,並執行main函式。 ``` /** * Launch the application given the archive file and a fully configured classloader. * @param args the incoming arguments * @param mainClass the main class to run * @param classLoader the classloader * @throws Exception if the launch fails */ protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { Thread.currentThread().setContextClassLoader(classLoader); createMainMethodRunner(mainClass, args, classLoader).run(); } /** * Create the {@code MainMethodRunner} used to launch the application. * @param mainClass the main class * @param args the incoming arguments * @param classLoader the classloader * @return the main method runner */ protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner(mainClass, args); } ``` 以上是整個Jar的啟動流程,本人是在tools原始碼裡debug下一步一步執行後寫下。程式碼均來之官方的原始碼。 知識碎片,重在整理,路很長,一步一個腳印,就好。