當前位置: 妍妍網 > 碼農

面試官:為什麽SpringBoot的 jar 可以直接執行?

2024-03-22碼農

來源:http://fangjian0423.github.io/

SpringBoot提供了一個外掛程式spring-boot-maven-plugin用於把程式打包成一個可執行的jar包。在pom檔裏加入這個外掛程式即可:

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

打包完生成的executable-jar-1.0-SNAPSHOT.jar內部的結構如下:

├── META-INF│ ├── MANIFEST.MF│ └── maven│ └── spring.study│ └── executable-jar│ ├── pom.properties│ └── pom.xml├── lib│ ├── aopalliance-1.0.jar│ ├── classmate-1.1.0.jar│ ├── spring-boot-1.3.5.RELEASE.jar│ ├── spring-boot-autoconfigure-1.3.5.RELEASE.jar│ ├── ...├── org│ └── springframework│ └── boot│ └── loader│ ├── ExecutableArchiveLauncher$1. class│ ├── ...└── spring └── study └── executablejar └── ExecutableJarApplication. class

然後可以直接執行jar包就能啟動程式了:

java -jar executable-jar-1.0-SNAPSHOT.jar

打包出來fat jar內部有4種檔型別:

  • META-INF資料夾:程式入口,其中MANIFEST.MF用於描述jar包的資訊

  • lib目錄:放置第三方依賴的jar包,比如springboot的一些jar包

  • spring boot loader相關的程式碼

  • 模組自身的程式碼

  • MANIFEST.MF檔的內容:

    Manifest-Version: 1.0Implementation-Title: executable-jarImplementation-Version: 1.0-SNAPSHOTArchiver-Version: PlexusArchiverBuilt-By: FormatStart- class: spring.study.executablejar.ExecutableJarApplicationImplementation-Vendor-Id: spring.studySpring-Boot-Version: 1.3.5.RELEASECreated-By: ApacheMaven 3.2.3Build-Jdk: 1.8.0_20Implementation-Vendor: PivotalSoftware, Inc.Main- class: org.springframework.boot.loader.JarLauncher

    我們看到,它的Main- class是org.springframework.boot.loader.JarLauncher,當我們使用java -jar執行jar包的時候會呼叫JarLauncher的main方法,而不是我們編寫的SpringApplication。

    那麽JarLauncher這個類是的作用是什麽的?

    它是SpringBoot內部提供的工具Spring Boot Loader提供的一個用於執行Application類的工具類(fat jar內部有spring loader相關的程式碼就是因為這裏用到了)。相當於Spring Boot Loader提供了一套標準用於執行SpringBoot打包出來的jar

    # Spring Boot Loader抽象的一些類

    抽象類Launcher:各種Launcher的基礎抽象類,用於啟動應用程式;跟Archive配合使用;目前有3種實作,分別是JarLauncher、WarLauncher以及PropertiesLauncher

    Archive:歸檔檔的基礎抽象類。JarFileArchive就是jar包檔的抽象。它提供了一些方法比如getUrl會返回這個Archive對應的URL;getManifest方法會獲得Manifest數據等。ExplodedArchive是檔目錄的抽象

    JarFile:對jar包的封裝,每個JarFileArchive都會對應一個JarFile。JarFile被構造的時候會解析內部結構,去獲取jar包裏的各個檔或資料夾,這些檔或資料夾會被封裝到Entry中,也儲存在JarFileArchive中。如果Entry是個jar,會解析成JarFileArchive。

    比如一個JarFileArchive對應的URL為:

    jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/

    它對應的JarFile為:

    /Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar

    這個JarFile有很多Entry,比如:

    META-INF/META-INF/MANIFEST.MFspring/spring/study/....spring/study/executablejar/ExecutableJarApplication. classlib/spring-boot-starter-1.3.5.RELEASE.jarlib/spring-boot-1.3.5.RELEASE.jar...

    JarFileArchive內部的一些依賴jar對應的URL(SpringBoot使用org.springframework.boot.loader.jar.Handler處理器來處理這些URL):

    jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-starter-web-1.3.5.RELEASE.jar!/jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher. class

    # JarLauncher的執行過程

    JarLauncher的main方法:

    publicstaticvoidmain(String[] args) { // 構造JarLauncher,然後呼叫它的launch方法。參數是控制台傳遞的new JarLauncher().launch(args);}

    JarLauncher被構造的時候會呼叫父類ExecutableArchiveLauncher的構造方法。

    ExecutableArchiveLauncher的構造方法內部會去構造Archive,這裏構造了JarFileArchive。構造JarFileArchive的過程中還會構造很多東西,比如JarFile,Entry …

    JarLauncher的launch方法:protectedvoid launch(String[] args) {try {// 在系統內容中設定註冊了自訂的URL處理器:org.springframework.boot.loader.jar.Handler。如果URL中沒有指定處理器,會去系統內容中查詢 JarFile.registerUrlProtocolHandler();// get classPathArchives方法在會去找lib目錄下對應的第三方依賴JarFileArchive,同時也會計畫自身的JarFileArchive// 根據get classPathArchives得到的JarFileArchive集合去建立類載入器 classLoader。這裏會構造一個LaunchedURL classLoader類載入器,這個類載入器繼承URL classLoader,並使用這些JarFileArchive集合的URL構造成URL classPath// LaunchedURL classLoader類載入器的父類載入器是當前執行類JarLauncher的類載入器 classLoader classLoader = create classLoader(get classPathArchives());// getMain class方法會去計畫自身的Archive中的Manifest中找出key為Start- class的類// 呼叫多載方法launch launch(args, getMain class(), classLoader); }catch (Exception ex) { ex.printStackTrace(); System.exit(1); }}// Archive的getMain class方法// 這裏會找出spring.study.executablejar.ExecutableJarApplication這個類publicString getMain class() throws Exception { Manifest manifest = getManifest();String main class = null;if (manifest != null) { main class = manifest.getMainAttributes().getValue("Start- class"); }if (main class == null) {thrownew IllegalStateException("No 'Start- class' manifest entry specified in " + this); }return main class;}// launch多載方法protectedvoid launch(String[] args, String main class, classLoader classLoader) throws Exception {// 建立一個MainMethodRunner,並把args和Start- class傳遞給它 Runnable runner = createMainMethodRunner(main class, args, classLoader);// 構造新執行緒 Thread runnerThread = new Thread(runner);// 執行緒設定類載入器以及名字,然後啟動 runnerThread.setContext classLoader( classLoader); runnerThread.setName(Thread.currentThread().getName()); runnerThread.start();}

    MainMethodRunner的run方法:

    @Overridepublicvoid run() {try {// 根據Start- class進行例項化 class<?> main class = Thread.currentThread().getContext classLoader() .load class(this.main className);// 找出main方法 Method mainMethod = main class.getDeclaredMethod("main", String[]. class);// 如果main方法不存在,丟擲異常if (mainMethod == null) {thrownew IllegalStateException(this.main className + " does not have a main method"); }// 呼叫 mainMethod.invoke(null, newObject[] { this.args }); }catch (Exception ex) { UncaughtExceptionHandler handler = Thread.currentThread() .getUncaughtExceptionHandler();if (handler != null) { handler.uncaughtException(Thread.currentThread(), ex); }thrownew RuntimeException(ex); }}

    # 關於自訂的類載入器LaunchedURL classLoader

    LaunchedURL classLoader重寫了load class方法,也就是說它修改了預設的類載入方式(先看該類是否已載入這部份不變,後面真正去載入類的規則改變了,不再是直接從父類載入器中去載入)。LaunchedURL classLoader定義了自己的類載入規則:

    private class<?> doLoad class(String name) throws classNotFoundException {// 1) Try the root class loadertry {if (this.root classLoader != null) {returnthis.root classLoader.load class(name); } }catch (Exception ex) {// Ignore and continue }// 2) Try to find locallytry { findPackage(name); class<?> cls = find class(name);return cls; }catch (Exception ex) {// Ignore and continue }// 3) Use standard loadingreturnsuper.load class(name, false);}

    載入規則:

  • 呼叫LaunchedURL classLoader自身的find class方法,也就是URL classLoader的find class方法

  • 呼叫父類的load class方法,也就是執行預設的類載入順序(從Bootstrap classLoader開始從下往下尋找)

  • LaunchedURL classLoader自身的find class方法:

    protected class<?> find class(final String name)throws classNotFoundException{try {return AccessController.doPrivileged(new PrivilegedExceptionAction< class<?>>() {public class<?> run() throws classNotFoundException {// 把類名解析成路徑並加上. class字尾 String path = name.replace('.', '/').concat(". class");// 基於之前得到的第三方jar包依賴以及自己的jar包得到URL陣列,進行遍歷找出對應類名的資源// 比如path是org/springframework/boot/loader/JarLauncher. class,它在jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/中被找出// 那麽找出的資源對應的URL為jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/org/springframework/boot/loader/JarLauncher. class Resource res = ucp.getResource(path, false);if (res != null) { // 找到了資源try {return define class(name, res); } catch (IOException e) {thrownew classNotFoundException(name, e); } } else { // 找不到資源的話直接丟擲 classNotFoundException異常thrownew classNotFoundException(name); } } }, acc); } catch (java.security.PrivilegedActionException pae) {throw ( classNotFoundException) pae.getException(); }}

    下面是LaunchedURL classLoader的一個測試:

    // 註冊org.springframework.boot.loader.jar.Handler URL協定處理器JarFile.registerUrlProtocolHandler();// 構造LaunchedURL classLoader類載入器,這裏使用了2個URL,分別對應jar包中依賴包spring-boot-loader和spring-boot,使用 "!/" 分開,需要org.springframework.boot.loader.jar.Handler處理器處理LaunchedURL classLoader classLoader = new LaunchedURL classLoader(new URL[] {new URL("jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-loader-1.3.5.RELEASE.jar!/") , new URL("jar:file:/Users/Format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/lib/spring-boot-1.3.5.RELEASE.jar!/") }, LaunchedURL classLoaderTest. class.get classLoader());// 載入類// 這2個類都會在第二步本地尋找中被找出(URL classLoader的find class方法) classLoader.load class("org.springframework.boot.loader.JarLauncher"); classLoader.load class("org.springframework.boot.SpringApplication");// 在第三步使用預設的載入順序在Application classLoader中被找出 classLoader.load class("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");

    # Spring Boot Loader的作用

    org.springframework.boot.loader.jar.Handler處理器處理。它的Main- class使用JarLauncher,如果是war包,使用WarLauncher執行。這些Launcher內部都會另起一個執行緒啟動自訂的SpringApplication類。

    這些特性透過spring-boot-maven-plugin外掛程式打包完成。

    熱門推薦