<pre id="p15p1"><del id="p15p1"><mark id="p15p1"></mark></del></pre>

<output id="p15p1"><dfn id="p15p1"><th id="p15p1"></th></dfn></output>

    <pre id="p15p1"></pre>
    <pre id="p15p1"></pre>

      <ruby id="p15p1"></ruby>

              行業動態
              上海興巖信息科技有限公司
              行業動態
              徹底搞懂 SpringBoot jar 可執行原理
              發布時間:2022-03-10 14:30:27
                |  
              閱讀量:242
              字號:
              A+ A- A
              涉及的知識點主要包括Maven的生命周期以及自定義插件,JDK提供關于jar包的工具類以及Springboot如何擴展,最后是自定義類加載器。

              spring-boot-maven-plugin

              SpringBoot 的可執行jar包又稱fat jar ,是包含所有第三方依賴的 jar 包,jar 包中嵌入了除 java 虛擬機以外的所有依賴,是一個 all-in-one jar 包。
              普通插件maven-jar-plugin生成的包和spring-boot-maven-plugin生成的包之間的直接區別,是fat jar中主要增加了兩部分,第一部分是lib目錄,存放的是Maven依賴的jar包文件,第二部分是spring boot loader相關的類。

              fat jar //目錄結構  
              ├─BOOT-INF  
              │  ├─classes  
              │  └─lib  
              ├─META-INF  
              │  ├─maven  
              │  ├─app.properties  
              │  ├─MANIFEST.MF        
              └─org  
                  └─springframework  
                      └─boot  
                          └─loader  
                              ├─archive  
                              ├─data  
                              ├─jar  
                              └─util  

              也就是說想要知道fat jar是如何生成的,就必須知道spring-boot-maven-plugin工作機制,而spring-boot-maven-plugin屬于自定義插件,因此我們又必須知道,Maven的自定義插件是如何工作的

              Maven的自定義插件

              Maven 擁有三套相互獨立的生命周期: clean、default 和 site, 而每個生命周期包含一些phase階段, 階段是有順序的, 并且后面的階段依賴于前面的階段。生命周期的階段phase與插件的目標goal相互綁定,用以完成實際的構建任務。

              <plugin>  
                  <groupId>org.springframework.boot</groupId>  
                  <artifactId>spring-boot-maven-plugin</artifactId>  
                  <executions>  
                      <execution>  
                          <goals>  
                              <goal>repackage</goal>  
                          </goals>  
                      </execution>  
                  </executions>  
              </plugin>  

              repackage目標對應的將執行到org.springframework.boot.maven.RepackageMojo#execute,該方法的主要邏輯是調用了org.springframework.boot.maven.RepackageMojo#repackage

              private void repackage() throws MojoExecutionException {  
                   //獲取使用maven-jar-plugin生成的jar,最終的命名將加上.orignal后綴  
                 Artifact source = getSourceArtifact();  
                  //最終文件,即Fat jar  
                 File target = getTargetFile();  
                  //獲取重新打包器,將重新打包成可執行jar文件  
                 Repackager repackager = getRepackager(source.getFile());  
                  //查找并過濾項目運行時依賴的jar  
                 Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),  
                       getFilters(getAdditionalFilters()));  
                  //將artifacts轉換成libraries  
                 Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,  
                       getLog());  
                 try {  
                     //提供Spring Boot啟動腳本  
                    LaunchScript launchScript = getLaunchScript();  
                     //執行重新打包邏輯,生成最后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());  
              }  

              我們關心一下org.springframework.boot.maven.RepackageMojo#getRepackager這個方法,知道Repackager是如何生成的,也就大致能夠推測出內在的打包邏輯。

              private Repackager getRepackager(File source) {  
                 Repackager repackager = new Repackager(source, this.layoutFactory);  
                 repackager.addMainClassTimeoutWarningListener(  
                       new LoggingMainClassTimeoutWarningListener());  
                  //設置main class的名稱,如果不指定的話則會查找第一個包含main方法的類,repacke最后將會設置org.springframework.boot.loader.JarLauncher  
                 repackager.setMainClass(this.mainClass);  
                 if (this.layout != null) {  
                    getLog().info("Layout: " + this.layout);  
                     //重點關心下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我們可以將之翻譯為文件布局,或者目錄布局,代碼一看清晰明了,同時我們需要關注,也是下一個重點關注對象org.springframework.boot.loader.JarLauncher,從名字推斷,這很可能是返回可執行jar文件的啟動類。

              MANIFEST.MF文件內容

              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文件為以上信息,可以看到兩個關鍵信息Main-ClassStart-Class。我們可以進一步,程序的啟動入口并不是我們SpringBoot中定義的main,而是JarLauncher#main,而再在其中利用反射調用定義好的Start-Class的main方法

              JarLauncher

              重點類介紹

              • 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抽象出來的統一訪問資源的層

                • JarFileArchivejar包文件的抽象
                • ExplodedArchive文件目錄

              這里重點描述一下JarFile的作用,每個JarFileArchive都會對應一個JarFile。在構造的時候會解析內部結構,去獲取jar包里的各個文件或文件夾類。我們可以看一下該類的注釋。

              /* 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只支持一個’!/‘,而Spring boot擴展了這個協議,讓它支持多個’!/‘,就可以表示jar in jar、jar in directory、fat jar的資源了。

              自定義類加載機制

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

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

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

              首先需要關注雙親委派機制很重要的一點是,如果一個類可以被委派最基礎的ClassLoader加載,就不能讓高層的ClassLoader加載,這樣是為了范圍錯誤的引入了非JDK下但是類名一樣的類。
              其二,如果在這個機制下,由于fat jar中依賴的各個第三方jar文件,并不在程序自己classpath下,也就是說,如果我們采用雙親委派機制的話,根本獲取不到我們所依賴的jar包,因此我們需要修改雙親委派機制的查找class的方法,自定義類加載機制。
              先簡單的介紹Springboot2中LaunchedURLClassLoader,該類繼承了java.net.URLClassLoader,重寫了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我們再探討他是如何修改雙親委派機制。
              在上面我們講到Spring boot支持多個’!/‘以表示多個jar,而我們的問題在于,如何解決查找到這多個jar包。我們看一下LaunchedURLClassLoader的構造方法。

              public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {  
                 super(urls, parent);  
              }  

              urls注釋解釋道the URLs from which to load classes and resources,即fat jar包依賴的所有類和資源,將該urls參數傳遞給父類java.net.URLClassLoader,由父類的java.net.URLClassLoader#findClass執行查找類方法,該類的查找來源即構造方法傳遞進來的urls參數。

              //LaunchedURLClassLoader的實現  
              protected Class<?> loadClass(String name, boolean resolve)  
                    throws ClassNotFoundException {  
                 Handler.setUseFastConnectionExceptions(true);  
                 try {  
                    try {  
                        //嘗試根據類名去定義類所在的包,即java.lang.Package,確保jar in 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.  
                
                          //這里異常表明,definePackageIfNecessary方法的作用實際上是預先過濾掉查找不到的包  
                          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)實際上會回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循雙親委派機制進行查找類,而Bootstrap ClassLoader和Extension ClassLoader將會查找不到fat jar依賴的類,最終會來到Application ClassLoader,調用java.net.URLClassLoader#findClass

              如何真正的啟動

              Springboot2和Springboot1的最大區別在于,Springboo1會新起一個線程,來執行相應的反射調用邏輯,而SpringBoot2則去掉了構建新的線程這一步。
              方法是org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)反射調用邏輯比較簡單,這里就不再分析,比較關鍵的一點是,在調用main方法之前,將當前線程的上下文類加載器設置成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();  
              // 構造LaunchedURLClassLoader類加載器,這里使用了2個URL,分別對應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個類都會在第二步本地查找中被找出(URLClassLoader的findClass方法)  
                      classLoader.loadClass("org.springframework.boot.loader.JarLauncher");  
                      classLoader.loadClass("org.springframework.boot.SpringApplication");  
              // 在第三步使用默認的加載順序在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>  

              總結

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

              我從Maven的自定義插件開始進行追蹤,鞏固了對Maven的知識點,在這個過程中甚至了解到JDK對jar的讀取是有提供對應的工具類。最后最重要的知識點則是自定義類加載器。整個代碼下來并不是說代碼究竟有多優秀,而是要學習他因何而優秀。


              亚洲国产精品激情在线观看,欧美日韩天堂在线视频,国产一区二区三区不卡在线看,91日本在线精品高清观看

              <pre id="p15p1"><del id="p15p1"><mark id="p15p1"></mark></del></pre>

              <output id="p15p1"><dfn id="p15p1"><th id="p15p1"></th></dfn></output>

                <pre id="p15p1"></pre>
                <pre id="p15p1"></pre>

                  <ruby id="p15p1"></ruby>