SpringBoot 的jar包爲什麼可以直接啓動?

  首先,先準備一個jar包,我這裏準備了一個demo-0.0.1-SNAPSHOT.jar;先來看看jar包裏面的目錄結構:

├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── com
│   │       └── sf
│   │           └── demo
│   │               └── DemoApplication.class
│   └── lib
│       ├── spring-boot-2.1.3.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.1.3.RELEASE.jar
│       ├── spring-boot-starter-2.1.3.RELEASE.jar
│       ├── 這裏省略掉很多jar包
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.sf
│           └── demo
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── LaunchedURLClassLoader.class
                ├── Launcher.class
                ├── 省略class
                ├── archive
                │   ├── Archive$Entry.class
                │   ├── 省略class
                ├── data
                │   ├── RandomAccessData.class
                │   ├── 省略class
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── 省略class
                └── util
                    └── SystemPropertyUtils.class

這個文件目錄分爲BOOT-INF/classesBOOT-INF/libMETA-INForg

  • BOOT-INF/classes:主要存放應用編譯後的class文件

  • BOOT-INF/lib:主要存放應用依賴的jar包文件

  • META-INF:主要存放mavenMANIFEST.MF文件

  • org:主要存放springboot相關的class文件

  當你使用命令java -jar demo-0.0.1-SNAPSHOT.jar時,它會找到META-INF下的MANIFEST.MF文件,可以從文件中發現,其內容中的Main-Class屬性值爲org.springframework.boot.loader.JarLauncher,並且項目的引導類定義在Start-Class屬性中,值爲com.sf.demo.DemoApplication,該屬性是由springboot引導程序啓動需要的,JarLauncher就是對應的jar文件的啓動器.

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.sf.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.0.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher

  啓動類org.springframework.boot.loader.JarLauncher並非是項目中引入的類,而是spring-boot-maven-plugin插件repackage追加進去的.

探索JarLauncher的實現原理

  當執行java -jar命令或執行解壓後的org.springframework.boot.loader.JarLauncher類時,JarLauncher會將BOOT-INF/classes下的類文件和BOOT-INF/lib下依賴的jar加入到classpath下,最後調用META-INF下的MANIFEST.MF文件的Start-Class屬性來完成應用程序的啓動,也就是說它是springboot loader提供了一套標準用於執行springboot打包出來的JAR包.

JarLauncher重點類的介紹:
  • java.util.jar.JarFileJDK工具類,用於讀取JAR文件的內容

  • org.springframework.boot.loader.jar.JarFile:繼承於JDK工具類JarFile類並擴展了一些嵌套功能

  • java.util.jar.JarEntryJDK工具類,此類用於表示JAR文件條目

  • org.springframework.boot.loader.jar.JarEntry:也是繼承於JDK工具類JarEntry

  • org.springframework.boot.loader.archive.Archivespring boot loader抽象出來的統一訪問資源的接口

  • org.springframework.boot.loader.archive.JarFileArchiveJAR文件的實現

  • org.springframework.boot.loader.archive.ExplodedArchive:文件目錄的實現

在項目裏面添加一個依賴配置,就可以看JarLauncher的源碼:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-loader</artifactId>
  <scope>provided</scope>
</dependency>
org.springframework.boot.loader.ExecutableArchiveLauncher
public class JarLauncher extends ExecutableArchiveLauncher {

   private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";

   static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
      if (entry.isDirectory()) {
         return entry.getName().equals("BOOT-INF/classes/");
      }
      return entry.getName().startsWith("BOOT-INF/lib/");
   };

   public JarLauncher() {
   }

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

   @Override
   protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
      // Only needed for exploded archives, regular ones already have a defined order
      if (archive instanceof ExplodedArchive) {
         String location = getClassPathIndexFileLocation(archive);
         return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
      }
      return super.getClassPathIndex(archive);
   }

   private String getClassPathIndexFileLocation(Archive archive) throws IOException {
      Manifest manifest = archive.getManifest();
      Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
      String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
      return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
   }

   @Override
   protected boolean isPostProcessingClassPathArchives() {
      return false;
   }

   @Override
   protected boolean isSearchCandidate(Archive.Entry entry) {
      return entry.getName().startsWith("BOOT-INF/");
   }

   @Override
   protected boolean isNestedArchive(Archive.Entry entry) {
      return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
   }

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

}
org.springframework.boot.loader.Launcher
/**
 * 
 * 啓動程序的基類,該啓動程序可以使用一個或多個支持的完全配置的類路徑來啓動應用程序
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @since 1.0.0
 */
public abstract class Launcher {

   private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";

   /**
    * 啓動應用程序,此方法是子類方法{@code public static void main(String[] args)}調用的初始入口點
    * @param args the incoming arguments
    * @throws Exception if the application fails to launch
    */
   protected void launch(String[] args) throws Exception {
      if (!isExploded()) {
         //①註冊一個自定義URL的jar協議
         JarFile.registerUrlProtocolHandler();
      }
      //②創建指定archive的類加載器
      ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
      String jarMode = System.getProperty("jarmode");
      //③獲取Start-Class屬性對應的com.sf.demo.DemoApplication
      String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
      //④利用反射調用Start-Class,執行main方法
      launch(args, launchClass, classLoader);
   }
}

①註冊一個自定義URLJAR協議

org.springframework.boot.loader.jar.JarFile#registerUrlProtocolHandler

spring boot loader擴展了URL協議,將包名org.springframework.boot.loader追加到java系統屬性java.protocol.handler.pkgs中,該包下存在協議對應的Handler類,即org.springframework.boot.loader.jar.Handler其實現協議爲JAR.

/**
 * 註冊一個'java.protocol.handler.pkgs'屬性,讓URLStreamHandler處理jar的URL
 */
public static void registerUrlProtocolHandler() {
   String handlers = System.getProperty(PROTOCOL_HANDLER, "");
   System.setProperty(PROTOCOL_HANDLER,
         ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
   resetCachedUrlHandlers();
}
org.springframework.boot.loader.jar.JarFile#resetCachedUrlHandlers
/**
 * 防止已經使用了jar協議,需要重置URLStreamHandlerFactory緩存的處理程序。
 */
private static void resetCachedUrlHandlers() {
   try {
      URL.setURLStreamHandlerFactory(null);
   }
   catch (Error ex) {
      // Ignore
   }
}

②創建指定archive的類加載器

org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchivesIterator
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
   Archive.EntryFilter searchFilter = this::isSearchCandidate;
   Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
         (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
   if (isPostProcessingClassPathArchives()) {
      archives = applyClassPathArchivePostProcessing(archives);
   }
   return archives;
}
org.springframework.boot.loader.Launcher#createClassLoader(java.util.Iterator<org.springframework.boot.loader.archive.Archive>)
/**
 * 創建一個指定的archives的類加載器
 * @param archives the archives
 * @return the classloader
 * @throws Exception if the classloader cannot be created
 * @since 2.3.0
 */
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
   List<URL> urls = new ArrayList<>(50);
   while (archives.hasNext()) {
      Archive archive = archives.next();
      urls.add(archive.getUrl());
      archive.close();
   }
   return createClassLoader(urls.toArray(new URL[0]));
}
org.springframework.boot.loader.Launcher#createClassLoader(java.util.Iterator<org.springframework.boot.loader.archive.Archive>)
/**
	 * 創建一個指定的自定義URL的類加載器
	 * @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(isExploded(), urls, getClass().getClassLoader());
	}

③獲取Start-Class屬性對應的com.sf.demo.DemoApplication

org.springframework.boot.loader.ExecutableArchiveLauncher#getMainClass

@Override
protected String getMainClass() throws Exception {
   Manifest manifest = this.archive.getManifest();
   String mainClass = null;
   if (manifest != null) {
      //從配置文件獲取Start-Class對應的com.sf.demo.DemoApplication
      mainClass = manifest.getMainAttributes().getValue("Start-Class");
   }
   if (mainClass == null) {
      throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
   }
   return mainClass;
}

④利用反射調用Start-Class,執行main方法

/**
	 * 啓動應用程序
	 * @param args the incoming arguments
	 * @param launchClass the launch class to run
	 * @param classLoader the classloader
	 * @throws Exception if the launch fails
	 */
	protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
    //將當前線程的上下文類加載器設置成LaunchedURLClassLoader
		Thread.currentThread().setContextClassLoader(classLoader);
    //啓動應用程序
		createMainMethodRunner(launchClass, args, classLoader).run();
	}
  /**
	 * 構造一個MainMethodRunner類,來啓動應用程序
	 * @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);
	}
org.springframework.boot.loader.MainMethodRunner
/**
 * 用來調用main方法的工具類
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @since 1.0.0
 */
public class MainMethodRunner {

   private final String mainClassName;

   private final String[] args;

   /**
    * Create a new {@link MainMethodRunner} instance.
    * @param mainClass the main class
    * @param args incoming arguments
    */
   public MainMethodRunner(String mainClass, String[] args) {
      this.mainClassName = mainClass;
      this.args = (args != null) ? args.clone() : null;
   }
	//利用反射啓動應用程序
   public void run() throws Exception {
      Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
      Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
      mainMethod.setAccessible(true);
      mainMethod.invoke(null, new Object[] { this.args });
   }

}

我們先了解一下類加載機制:

在這裏插入圖片描述

  我們知道雙親委派模型的原則,當一個類加載器收到類加載任務,會先交給其父類加載器去完成,因此最終加載任務都會傳遞到頂層的啓動類加載器,只有當父類加載器無法完成加載任務時,纔會嘗試加載任務。

  由於demo-0.0.1-SNAPSHOT.jar中依賴的各個JDK包,並不在程序自己的classpath下,它是存放在JDK包裏的BOOT-INF/lib目錄下,如果我們採用雙親委派機制的話,根本獲取不到我們JAR包的依賴,因此我們需要破壞雙親委派模型,使用自定義類加載機制。

  在springboot2中,LaunchedURLClassLoader自定義類加載器繼承URLClassLoader,重寫了loadClass方法;在JDK裏面,JAR的資源分隔符是!/,但是JDK中只支持一個!/,這無法滿足spring boot loader的需求,so,springboot擴展了JarFile,從這裏可以看到org.springframework.boot.loader.jar.JarFile#createJarFileFromEntry,它支持了多個!/,表示jar文件嵌套JAR文件、JAR文件嵌套Directory.

org.springframework.boot.loader.LaunchedURLClassLoader
public class LaunchedURLClassLoader extends URLClassLoader {
  @Override
  protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
     if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
        ........省略代碼
     try {
        try {
           //嘗試根據類名去定義類所在的包,即java.lang.Package,確保jar文件嵌套jar包裏匹配的manifest能夠和package關聯起來
           definePackageIfNecessary(name);
        }
        catch (IllegalArgumentException ex) {
           // Tolerate race condition due to being parallel capable
           if (getPackage(name) == null) {
              // This should never happen as the IllegalArgumentException indicates
              // that the package has already been defined and, therefore,
              // getPackage(name) should not return 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);
     }
  }
}
org.springframework.boot.loader.LaunchedURLClassLoader#definePackageIfNecessary
/**
 * 在進行調用findClass方法之前定義一個包,確保嵌套jar與包關聯
 * @param className the class name being found
 */
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) {
            // Tolerate race condition due to being parallel capable
            if (getPackage(packageName) == null) {
               // This should never happen as the IllegalArgumentException
               // indicates that the package has already been defined and,
               // therefore, getPackage(name) should not have returned null.
               throw new AssertionError(
                     "Package " + packageName + " has already been defined but it could not be found");
            }
         }
      }
   }
}

總結:

1、springboot 擴展了JDKURL協議;

2、springboot 自定義了類加載器LaunchedURLClassLoader

3、Launcher利用反射調用StartClass#main方法(org.springframework.boot.loader.MainMethodRunner#run);

4、springboot1springboot2主要區別是在啓動應用程序時,springboot1會啓動一個線程去反射調用,springboot2直接調用;

參考資料:

https://www.yht7.com/news/18153

https://segmentfault.com/a/1190000016192449

https://cloud.tencent.com/developer/article/1469863

https://www.cnblogs.com/xxzhuang/p/11194559.html

http://www.10qianwan.com/articledetail/577937.html

https://blog.csdn.net/shenchaohao12321/article/details/103543446

https://fangjian0423.github.io/2017/05/31/springboot-executable-jar/

如果文章存在問題、不妥的地方或者有疑惑麻煩給我留言,大家一起學習,感謝您~
歡迎關注我的微信公衆號,裏面有很多幹貨,各種面試題
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章