首頁 > 軟體

SpringBoot的jar包如何啟動的實現

2022-03-20 13:01:24

一、簡介

​ 使用過SprongBoot打過jar包的都應該知道,目標檔案一般都會生成兩個檔案,一個是以.jar的包,一個是.jar.original檔案。那麼使用SpringBoot會打出兩個包,而.jar.original的作用是什麼呢?還有就是java -jar是如何將一個SpringBoot專案啟動,之間都進行了那些的操作?

​ 其實.jar.originalmavenSpringBoot重新打包之前的原始jar包,內部只包含了專案的使用者類,不包含其他的依賴jar包,生成之後,SpringBoot重新打包之後,最後生成.jar包,內部包含了原始jar包以及其他的參照依賴。以下提及的jar包都是SpringBoot二次加工打的包。

二、jar包的內部結構

SpringBoot打出的jar包,可以直接通過解壓的方式檢視內部的構造。一般情況下有三個目錄。

  • BOOT-INF:這個資料夾下有兩個資料夾classes用來存放使用者類,也就是原始jar.original裡的類;還有一個是lib,就是這個原始jar.original參照的依賴。
  • META-INF:這裡是通過java -jar啟動的入口資訊,記錄了入口類的位置等資訊。
  • org:Springboot loader的程式碼,通過它來啟動。

這裡主要介紹一下/BOOT-INF/MANIFEST.MF檔案

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: cn.com.springboot.center.AuthEenterBootstrap

Main-Class:記錄了java -jar的啟動入口,當使用該命令啟動時就會呼叫這個入口類的main方法,顯然可以看出,Springboot轉移了啟動的入口,不是使用者編寫的xxx.xxx.BootStrap的那個入口類。

Start-Class:記錄了使用者編寫的xxx.xxx.BootStrap的那個入口類,當內嵌的jar包載入完成之後,會使用LaunchedURLClassLoader執行緒載入類來載入這個使用者編寫的入口類。

三、載入過程

1.使用到的一些類

3.1.1 Archive

​ 歸檔檔案介面,實現迭代器介面,它有兩個子類,一個是JarFileArchivejar包檔案使用,提供了返回這個jar檔案對應的url、或者這個jar檔案的MANIFEST檔案資料資訊等操作。是ExplodedArchive是檔案目錄的使用也有獲取這個目錄url的方法,以及獲取這個目錄下的所有Archive檔案方法。

3.1.2 Launcher

​ 啟動程式的基礎類別,這邊最後是通過JarLauncher#main()來啟動。ExecutableArchiveLauncher是抽象類,提供了獲取Start-Class類路徑的方法,以及是否還有內嵌對應檔案的判斷方法和獲取到內嵌對應檔案集合的後置處理方法的抽象,由子類JarLauncherWarLauncher自行實現。

3.1.3 Spring.loader下的JarFile和JarEntry

jarFile繼承於jar.util.jar.JarFileJarEntry繼承於java.util.jar.JarEntry,對原始的一些方法進行重寫覆蓋。每一個JarFileArchive都擁有一個JarFile方法,用於儲存這個jar包對應的檔案,而每一個JarFile都有一個JarFileEntries,JarFileEntries是一個迭代器。總的來說,在解析jar包時,會將jar包內的檔案封裝成JarEntry物件後由JarFile物件儲存檔案列表的迭代器。所以JarFileArchiveJarFileEntries之間是通過JarFile連線,二者都可以獲取到JarFile物件。

2.過程分析

MANIFEST.MF檔案中的Main-class指向入口開始。

建立JarLauncher並且通過它的launch()方法開始載入jar包內部資訊。

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

JarLauncher的空構造方法時一個空實現,剛開始看的時候還懵了一下,以為是在後續的操作中去載入的檔案,其實不然,在建立時由父類別ExecutableArchiveLauncher的構造方法去載入的檔案。

載入為歸檔檔案物件:

this.archive = createArchive();

具體的載入方法:判斷路徑是否是一個資料夾,是則返回ExplodedArchive物件,否則返回JarFileArchive 進入JarFileArchive類:通過這個new方法建立JarFile物件

public class JarFileArchive implements Archive {

  public JarFileArchive(File file, URL url) throws IOException {
    this(new JarFile(file));
    this.url = url;
  }
}

進入到JarFile方法:通過RandomAccessDataFile讀取檔案的內容,並傳遞給本類中的方法進行具體的解析。

public class JarFile extends java.util.jar.JarFile {

  public JarFile(File file) throws IOException {
    this(new RandomAccessDataFile(file));
  }
}

進入jarLauncherlaunch方法:註冊URL協定的處理器,沒有指定時,預設指向org.springframework.boot.loader包路徑,獲取類路徑下的歸檔檔案Archive並通過這些歸檔檔案的URL,建立執行緒上下文類載入器,使用類載入器和使用者編寫的啟動入口類,通過反射呼叫它的main方法。

protected void launch(String[] args) throws Exception {
  JarFile.registerUrlProtocolHandler();
  ClassLoader classLoader = createClassLoader(getClassPathArchives());
  launch(args, getMainClass(), classLoader);
}

JarLaunchergetClassPathArchives是在ExecutableArchiveLauncher中實現:獲取歸檔檔案中滿足EntryFilterg過濾器的項,isNestedArchive方法由具體的之類實現。獲取到當前歸檔檔案下的所有子歸檔檔案之後的後置操作,是一個擴充套件點。在JarLauncher中是一個空實現。

JarLauncher的具體實現,這裡通過判斷是否在BOOT-INF/lib/包下返回true 也就是說只會把jar包下的BOOT-INF/lib/下的檔案載入為Archive物件

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

JarFileArchivegetNestedArchives方法:若匹配器匹配到則獲取內嵌歸檔檔案。

具體的獲取內嵌歸檔檔案邏輯:根據具體的Entry物件,建立JarFile物件並封裝成歸檔檔案物件後返回。

protected Archive getNestedArchive(Entry entry) throws IOException {
    try {
        JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
        return new JarFileArchive(jarFile);
    }
}

獲取到引數entry對應的RandomAccessData物件,這裡根據springboot擴充套件的url協定,在父路徑的基礎上新增!/來標記子包。

private JarFile createJarFileFromFileEntry(JarEntry entry) throws IOException {
    RandomAccessData entryData = this.entries.getEntryData(entry.getName());
    return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(),
                       entryData, JarFileType.NESTED_JAR);
}

到這基本上讀取jar內部資訊,載入為對應歸檔檔案物件的大概過程已經講完了,接下來分析一下在獲取到了整個jar的歸檔檔案物件後的處理。

通過歸檔檔案物件列表,獲取對應的url資訊,並通過url資訊建立LaunchedURLClassLoader

protected ClassLoader createClassLoader(List<Archive> archives) {
    List<URL> urls = new ArrayList<URL>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    return createClassLoader(urls.toArray(new URL[urls.size()]));
}

獲取到對應的LaunchedUrlClassLoader類載入器之後,設定執行緒的上下文類載入器為該載入器。根據MANIFI.MF檔案中的start-classs資訊建立專案啟動入口主類物件,並通過返回物件的run方法啟動

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

進入MainMethodRunnerrun方法:先通過當前執行緒獲取到main入口類,然後通過反射呼叫啟動專案啟動類的main方法

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 });
}

最後來說一下這個LaunchedURLClassLoader,它繼承於URLClassLoader,並重寫了loadClass方法

LaunchedClassLoaderloadClass方法:呼叫父類別loadClass方法,走正常委派流程,最終會被LaunchURLClassLoader載入。

@Override
protected Class<?> loadClass(String name, boolean resolve){
   try {
      try {
         definePackageIfNecessary(name);
      }
      return super.loadClass(name, resolve);
   }
}

進入URLClassLoader中根據springboot解析進行解析。根據名稱將路徑轉化為以.class結尾的/分隔的格式。通過UrlClassPath物件根據路徑獲取資源類檔案

new PrivilegedExceptionAction<Class<?>>() {
    public Class<?> run() throws ClassNotFoundException {
      String path = name.replace('.', '/').concat(".class");
      Resource res = ucp.getResource(path, false);
      if (res != null) {
        try {
          return defineClass(name, res);
        }
      }
    }
  }

四、總結

Springboot主要實現了對URL載入方式進行了擴充套件,並且對一些物件ArchiveJarFileEntry等進行了抽象和擴充套件,最後使用LaunchedUrlClassLoader來進行處理。

到此這篇關於SpringBoot的jar包如何啟動的實現的文章就介紹到這了,更多相關SpringBoot jar包啟動內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com