<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          徹底搞懂 SpringBoot jar 可執(zhí)行原理

          共 17444字,需瀏覽 35分鐘

           ·

          2022-07-04 22:40

          大家好,我是寶哥!


          文章篇幅較長(zhǎng),但是包含了SpringBoot 可執(zhí)行jar包從頭到尾的原理,請(qǐng)讀者耐心觀看。

          涉及的知識(shí)點(diǎn)主要包括Maven的生命周期以及自定義插件,JDK提供關(guān)于jar包的工具類以及Springboot如何擴(kuò)展,最后是自定義類加載器。

          spring-boot-maven-plugin

          SpringBoot 的可執(zhí)行jar包又稱fat jar ,是包含所有第三方依賴的 jar 包,jar 包中嵌入了除 java 虛擬機(jī)以外的所有依賴,是一個(gè) all-in-one jar 包。
          普通插件maven-jar-plugin生成的包和spring-boot-maven-plugin生成的包之間的直接區(qū)別,是fat jar中主要增加了兩部分,第一部分是lib目錄,存放的是Maven依賴的jar包文件,第二部分是spring boot loader相關(guān)的類。
          fat jar //目錄結(jié)構(gòu)  
          ├─BOOT-INF  
          │  ├─classes  
          │  └─lib  
          ├─META-INF  
          │  ├─maven  
          │  ├─app.properties  
          │  ├─MANIFEST.MF        
          └─org  
              └─springframework  
                  └─boot  
                      └─loader  
                          ├─archive  
                          ├─data  
                          ├─jar  
                          └─util  
          也就是說(shuō)想要知道fat jar是如何生成的,就必須知道spring-boot-maven-plugin工作機(jī)制,而spring-boot-maven-plugin屬于自定義插件,因此我們又必須知道,Maven的自定義插件是如何工作的

          Maven的自定義插件

          Maven 擁有三套相互獨(dú)立的生命周期: clean、default 和 site, 而每個(gè)生命周期包含一些phase階段, 階段是有順序的, 并且后面的階段依賴于前面的階段。生命周期的階段phase與插件的目標(biāo)goal相互綁定,用以完成實(shí)際的構(gòu)建任務(wù)。
          <plugin>  
              <groupId>org.springframework.boot</groupId>  
              <artifactId>spring-boot-maven-plugin</artifactId>  
              <executions>  
                  <execution>  
                      <goals>  
                          <goal>repackage</goal>  
                      </goals>  
                  </execution>  
              </executions>  
          </plugin>  
          repackage目標(biāo)對(duì)應(yīng)的將執(zhí)行到org.springframework.boot.maven.RepackageMojo#execute,該方法的主要邏輯是調(diào)用了org.springframework.boot.maven.RepackageMojo#repackage
          private void repackage() throws MojoExecutionException {  
               //獲取使用maven-jar-plugin生成的jar,最終的命名將加上.orignal后綴  
             Artifact source = getSourceArtifact();  
              //最終文件,即Fat jar  
             File target = getTargetFile();  
              //獲取重新打包器,將重新打包成可執(zhí)行jar文件  
             Repackager repackager = getRepackager(source.getFile());  
              //查找并過(guò)濾項(xiàng)目運(yùn)行時(shí)依賴的jar  
             Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),  
                   getFilters(getAdditionalFilters()));  
              //將artifacts轉(zhuǎn)換成libraries  
             Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,  
                   getLog());  
             try {  
                 //提供Spring Boot啟動(dòng)腳本  
                LaunchScript launchScript = getLaunchScript();  
                 //執(zhí)行重新打包邏輯,生成最后fat jar  
                repackager.repackage(target, libraries, launchScript);  
             }  
             catch (IOException ex) {  
                throw new MojoExecutionException(ex.getMessage(), ex);  
             }  
              //將source更新成 xxx.jar.orignal文件  
             updateArtifact(source, target, repackager.getBackupFile());  
          }  
          我們關(guān)心一下org.springframework.boot.maven.RepackageMojo#getRepackager這個(gè)方法,知道Repackager是如何生成的,也就大致能夠推測(cè)出內(nèi)在的打包邏輯。
          private Repackager getRepackager(File source) {  
             Repackager repackager = new Repackager(source, this.layoutFactory);  
             repackager.addMainClassTimeoutWarningListener(  
                   new LoggingMainClassTimeoutWarningListener());  
              //設(shè)置main class的名稱,如果不指定的話則會(huì)查找第一個(gè)包含main方法的類,repacke最后將會(huì)設(shè)置org.springframework.boot.loader.JarLauncher  
             repackager.setMainClass(this.mainClass);  
             if (this.layout != null) {  
                getLog().info("Layout: " + this.layout);  
                 //重點(diǎn)關(guān)心下layout 最終返回了 org.springframework.boot.loader.tools.Layouts.Jar  
                repackager.setLayout(this.layout.layout());  
             }  
             return repackager;  
          }  
          /**  
           * Executable JAR layout.  
           */
            
          public static class Jar implements RepackagingLayout {  
             @Override  
             public String getLauncherClassName() {  
                return "org.springframework.boot.loader.JarLauncher";  
             }  
             @Override  
             public String getLibraryDestination(String libraryName, LibraryScope scope) {  
                return "BOOT-INF/lib/";  
             }  
             @Override  
             public String getClassesLocation() {  
                return "";  
             }  
             @Override  
             public String getRepackagedClassesLocation() {  
                return "BOOT-INF/classes/";  
             }  
             @Override  
             public boolean isExecutable() {  
                return true;  
             }  
          }  
          layout我們可以將之翻譯為文件布局,或者目錄布局,代碼一看清晰明了,同時(shí)我們需要關(guān)注,也是下一個(gè)重點(diǎn)關(guān)注對(duì)象org.springframework.boot.loader.JarLauncher,從名字推斷,這很可能是返回可執(zhí)行jar文件的啟動(dòng)類。

          MANIFEST.MF文件內(nèi)容

          Manifest-Version: 1.0  
          Implementation-Title: oneday-auth-server  
          Implementation-Version: 1.0.0-SNAPSHOT  
          Archiver-Version: Plexus Archiver  
          Built-By: oneday  
          Implementation-Vendor-Id: com.oneday  
          Spring-Boot-Version: 2.1.3.RELEASE  
          Main-Class: org.springframework.boot.loader.JarLauncher  
          Start-Class: com.oneday.auth.Application  
          Spring-Boot-Classes: BOOT-INF/classes/  
          Spring-Boot-Lib: BOOT-INF/lib/  
          Created-By: Apache Maven 3.3.9  
          Build-Jdk: 1.8.0_171
          repackager生成的MANIFEST.MF文件為以上信息,可以看到兩個(gè)關(guān)鍵信息Main-ClassStart-Class。我們可以進(jìn)一步,程序的啟動(dòng)入口并不是我們SpringBoot中定義的main,而是JarLauncher#main,而再在其中利用反射調(diào)用定義好的Start-Class的main方法

          JarLauncher

          重點(diǎn)類介紹
          • java.util.jar.JarFile JDK工具類提供的讀取jar文件

          • org.springframework.boot.loader.jar.JarFileSpringboot-loader 繼承JDK提供JarFile類

          • java.util.jar.JarEntryDK工具類提供的jar文件條目

          • org.springframework.boot.loader.jar.JarEntry Springboot-loader 繼承JDK提供JarEntry類

          • org.springframework.boot.loader.archive.Archive Springboot抽象出來(lái)的統(tǒng)一訪問(wèn)資源的層

            • JarFileArchivejar包文件的抽象
            • ExplodedArchive文件目錄
          這里重點(diǎn)描述一下JarFile的作用,每個(gè)JarFileArchive都會(huì)對(duì)應(yīng)一個(gè)JarFile。在構(gòu)造的時(shí)候會(huì)解析內(nèi)部結(jié)構(gòu),去獲取jar包里的各個(gè)文件或文件夾類。我們可以看一下該類的注釋。
          /* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but  
          * offers the following additional functionality.  
          * <ul>  
          * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based  
          * on any directory entry.</li>  
          * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for  
          * embedded JAR files (as long as their entry is not compressed).</li>  
          </ul>  
          **/
            
          jar里的資源分隔符是!/,在JDK提供的JarFile URL只支持一個(gè)’!/‘,而Spring boot擴(kuò)展了這個(gè)協(xié)議,讓它支持多個(gè)’!/‘,就可以表示jar in jar、jar in directory、fat jar的資源了。

          自定義類加載機(jī)制

          • 最基礎(chǔ):Bootstrap ClassLoader(加載JDK的/lib目錄下的類)

          • 次基礎(chǔ):Extension ClassLoader(加載JDK的/lib/ext目錄下的類)

          • 普通:Application ClassLoader(程序自己classpath下的類)

          首先需要關(guān)注雙親委派機(jī)制很重要的一點(diǎn)是,如果一個(gè)類可以被委派最基礎(chǔ)的ClassLoader加載,就不能讓高層的ClassLoader加載,這樣是為了范圍錯(cuò)誤的引入了非JDK下但是類名一樣的類。
          其二,如果在這個(gè)機(jī)制下,由于fat jar中依賴的各個(gè)第三方j(luò)ar文件,并不在程序自己classpath下,也就是說(shuō),如果我們采用雙親委派機(jī)制的話,根本獲取不到我們所依賴的jar包,因此我們需要修改雙親委派機(jī)制的查找class的方法,自定義類加載機(jī)制。
          先簡(jiǎn)單的介紹Springboot2中LaunchedURLClassLoader,該類繼承了java.net.URLClassLoader,重寫了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我們?cè)偬接懰侨绾涡薷碾p親委派機(jī)制。
          在上面我們講到Spring boot支持多個(gè)’!/‘以表示多個(gè)jar,而我們的問(wèn)題在于,如何解決查找到這多個(gè)jar包。我們看一下LaunchedURLClassLoader的構(gòu)造方法。
          public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {  
             super(urls, parent);  
          }  
          urls注釋解釋道the URLs from which to load classes and resources,即fat jar包依賴的所有類和資源,將該urls參數(shù)傳遞給父類java.net.URLClassLoader,由父類的java.net.URLClassLoader#findClass執(zhí)行查找類方法,該類的查找來(lái)源即構(gòu)造方法傳遞進(jìn)來(lái)的urls參數(shù)。
          //LaunchedURLClassLoader的實(shí)現(xiàn)  
          protected Class<?> loadClass(String name, boolean resolve)  
                throws ClassNotFoundException {  
             Handler.setUseFastConnectionExceptions(true);  
             try {  
                try {  
                    //嘗試根據(jù)類名去定義類所在的包,即java.lang.Package,確保jar in jar里匹配的manifest能夠和關(guān)聯(lián)               //的package關(guān)聯(lián)起來(lái)  
                   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.  
            
                      //這里異常表明,definePackageIfNecessary方法的作用實(shí)際上是預(yù)先過(guò)濾掉查找不到的包  
                      throw new AssertionError("Package " + name + " has already been "  
                            + "defined but it could not be found");  
                   }  
                }  
                return super.loadClass(name, resolve);  
             }  
             finally {  
                Handler.setUseFastConnectionExceptions(false);  
             }  
          }  
          方法super.loadClass(name, resolve)實(shí)際上會(huì)回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循雙親委派機(jī)制進(jìn)行查找類,而Bootstrap ClassLoader和Extension ClassLoader將會(huì)查找不到fat jar依賴的類,最終會(huì)來(lái)到Application ClassLoader,調(diào)用java.net.URLClassLoader#findClass

          如何真正的啟動(dòng)

          Springboot2和Springboot1的最大區(qū)別在于,Springboo1會(huì)新起一個(gè)線程,來(lái)執(zhí)行相應(yīng)的反射調(diào)用邏輯,而SpringBoot2則去掉了構(gòu)建新的線程這一步。
          方法是org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)反射調(diào)用邏輯比較簡(jiǎn)單,這里就不再分析,比較關(guān)鍵的一點(diǎn)是,在調(diào)用main方法之前,將當(dāng)前線程的上下文類加載器設(shè)置成LaunchedURLClassLoader
          protected void launch(String[] args, String mainClass, ClassLoader classLoader)  
                throws Exception 
          {  
             Thread.currentThread().setContextClassLoader(classLoader);  
             createMainMethodRunner(mainClass, args, classLoader).run();  
          }  

          Demo

          public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {  
                  JarFile.registerUrlProtocolHandler();  
          // 構(gòu)造LaunchedURLClassLoader類加載器,這里使用了2個(gè)URL,分別對(duì)應(yīng)jar包中依賴包spring-boot-loader和spring-boot,使用 "!/" 分開,需要org.springframework.boot.loader.jar.Handler處理器處理  
                  LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(  
                          new URL[] {  
                                  new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.2.3.RELEASE.jar!/")  
                                  , new URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.1.3.RELEASE.jar!/")  
                          },  
                          Application.class.getClassLoader());  
          // 加載類  
          // 這2個(gè)類都會(huì)在第二步本地查找中被找出(URLClassLoader的findClass方法)  
                  classLoader.loadClass("org.springframework.boot.loader.JarLauncher");  
                  classLoader.loadClass("org.springframework.boot.SpringApplication");  
          // 在第三步使用默認(rèn)的加載順序在ApplicationClassLoader中被找出  
             classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");  
            
          //        SpringApplication.run(Application.class, args);  
              }  
            
          <dependency>  
              <groupId>org.springframework.boot</groupId>  
              <artifactId>spring-boot-loader</artifactId>  
              <version>2.1.3.RELEASE</version>  
          </dependency>  
          <dependency>  
              <groupId>org.springframework.boot</groupId>  
              <artifactId>spring-boot-maven-plugin</artifactId>  
              <version>2.1.3.RELEASE</version>  

          </dependency>  

          總結(jié)

          對(duì)于源碼分析,這次的較大收獲則是不能一下子去追求弄懂源碼中的每一步代碼的邏輯,即便我知道該方法的作用。我們需要搞懂的是關(guān)鍵代碼,以及涉及到的知識(shí)點(diǎn)。

          我從Maven的自定義插件開始進(jìn)行追蹤,鞏固了對(duì)Maven的知識(shí)點(diǎn),在這個(gè)過(guò)程中甚至了解到JDK對(duì)jar的讀取是有提供對(duì)應(yīng)的工具類。最后最重要的知識(shí)點(diǎn)則是自定義類加載器。整個(gè)代碼下來(lái)并不是說(shuō)代碼究竟有多優(yōu)秀,而是要學(xué)習(xí)他因何而優(yōu)秀。

          來(lái)源:juejin.im/post/5d2d6812e51d45777b1a3e5a

          精彩推薦:

          保存好這個(gè)腳本,一鍵自動(dòng)部署 Redis 任意版本

          從零開始搭建公司大型SaaS 平臺(tái)架構(gòu)技術(shù)棧,這套架構(gòu)絕了!

          看看人家SpringBoot的全局異常處理多么優(yōu)雅...

          IDEA 插件版的 Postman,接口調(diào)試簡(jiǎn)直太方便了!粉了。。。

          token 過(guò)期后,如何自動(dòng)續(xù)期?

          SpringBoot+Nacos+Kafka簡(jiǎn)單實(shí)現(xiàn)微服務(wù)流編排

          瀏覽 42
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美日韩午夜激情 | 日日嗨AV一区二区三区 | 激情网站五月天 | 国产视频久久久 | 操逼小动漫 |