<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 可以直接運行?

          共 15645字,需瀏覽 32分鐘

           ·

          2021-03-24 11:13

          不點藍(lán)字,我們哪來故事?

          每天 11 點更新文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領(lǐng)!》

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



          SpringBoot提供了一個插件spring-boot-maven-plugin用于把程序打包成一個可執(zhí)行的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內(nèi)部的結(jié)構(gòu)如下:

          ├── 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

          然后可以直接執(zhí)行jar包就能啟動程序了:

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

          打包出來fat jar內(nèi)部有4種文件類型:

          • META-INF文件夾:程序入口,其中MANIFEST.MF用于描述jar包的信息
          • lib目錄:放置第三方依賴的jar包,比如springboot的一些jar包
          • spring boot loader相關(guān)的代碼
          • 模塊自身的代碼

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

          Manifest-Version: 1.0
          Implementation-Title: executable-jar
          Implementation-Version: 1.0-SNAPSHOT
          Archiver-Version: Plexus Archiver
          Built-By: Format
          Start-Class: spring.study.executablejar.ExecutableJarApplication
          Implementation-Vendor-Id: spring.study
          Spring-Boot-Version: 1.3.5.RELEASE
          Created-By: Apache Maven 3.2.3
          Build-Jdk: 1.8.0_20
          Implementation-Vendor: Pivotal Software, Inc.
          Main-Class: org.springframework.boot.loader.JarLauncher

          我們看到,它的Main-Class是org.springframework.boot.loader.JarLauncher,當(dāng)我們使用java -jar執(zhí)行jar包的時候會調(diào)用JarLauncher的main方法,而不是我們編寫的SpringApplication。

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

          它是SpringBoot內(nèi)部提供的工具Spring Boot Loader提供的一個用于執(zhí)行Application類的工具類(fat jar內(nèi)部有spring loader相關(guān)的代碼就是因為這里用到了)。相當(dāng)于Spring Boot Loader提供了一套標(biāo)準(zhǔn)用于執(zhí)行SpringBoot打包出來的jar

          Spring Boot Loader抽象的一些類

          抽象類Launcher:各種Launcher的基礎(chǔ)抽象類,用于啟動應(yīng)用程序;跟Archive配合使用;目前有3種實現(xiàn),分別是JarLauncher、WarLauncher以及PropertiesLauncher

          Archive:歸檔文件的基礎(chǔ)抽象類。JarFileArchive就是jar包文件的抽象。它提供了一些方法比如getUrl會返回這個Archive對應(yīng)的URL;getManifest方法會獲得Manifest數(shù)據(jù)等。ExplodedArchive是文件目錄的抽象

          JarFile:對jar包的封裝,每個JarFileArchive都會對應(yīng)一個JarFile。JarFile被構(gòu)造的時候會解析內(nèi)部結(jié)構(gòu),去獲取jar包里的各個文件或文件夾,這些文件或文件夾會被封裝到Entry中,也存儲在JarFileArchive中。如果Entry是個jar,會解析成JarFileArchive。

          比如一個JarFileArchive對應(yīng)的URL為:

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

          它對應(yīng)的JarFile為:

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

          這個JarFile有很多Entry,比如:

          META-INF/
          META-INF/MANIFEST.MF
          spring/
          spring/study/
          ....
          spring/study/executablejar/ExecutableJarApplication.class
          lib/spring-boot-starter-1.3.5.RELEASE.jar
          lib/spring-boot-1.3.5.RELEASE.jar
          ...

          JarFileArchive內(nèi)部的一些依賴jar對應(yīng)的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

          我們看到如果有jar包中包含jar,或者jar包中包含jar包里面的class文件,那么會使 用 !/ 分隔開,這種方式只有org.springframework.boot.loader.jar.Handler能處 理,它是SpringBoot內(nèi)部擴展出來的一種URL協(xié)議。

          JarLauncher的執(zhí)行過程

          JarLauncher的main方法:

          public static void main(String[] args) {
              // 構(gòu)造JarLauncher,然后調(diào)用它的launch方法。參數(shù)是控制臺傳遞的
              new JarLauncher().launch(args);
          }  

          JarLauncher被構(gòu)造的時候會調(diào)用父類ExecutableArchiveLauncher的構(gòu)造方法。

          ExecutableArchiveLauncher的構(gòu)造方法內(nèi)部會去構(gòu)造Archive,這里構(gòu)造了JarFileArchive。構(gòu)造JarFileArchive的過程中還會構(gòu)造很多東西,比如JarFile,Entry …

          JarLauncher的launch方法:
          protected void launch(String[] args) {
            try {
              // 在系統(tǒng)屬性中設(shè)置注冊了自定義的URL處理器:org.springframework.boot.loader.jar.Handler。如果URL中沒有指定處理器,會去系統(tǒng)屬性中查詢
              JarFile.registerUrlProtocolHandler();
              // getClassPathArchives方法在會去找lib目錄下對應(yīng)的第三方依賴JarFileArchive,同時也會項目自身的JarFileArchive
              // 根據(jù)getClassPathArchives得到的JarFileArchive集合去創(chuàng)建類加載器ClassLoader。這里會構(gòu)造一個LaunchedURLClassLoader類加載器,這個類加載器繼承URLClassLoader,并使用這些JarFileArchive集合的URL構(gòu)造成URLClassPath
              // LaunchedURLClassLoader類加載器的父類加載器是當(dāng)前執(zhí)行類JarLauncher的類加載器
              ClassLoader classLoader = createClassLoader(getClassPathArchives());
              // getMainClass方法會去項目自身的Archive中的Manifest中找出key為Start-Class的類
              // 調(diào)用重載方法launch
              launch(args, getMainClass(), classLoader);
            }
            catch (Exception ex) {
              ex.printStackTrace();
              System.exit(1);
            }
          }

          // Archive的getMainClass方法
          // 這里會找出spring.study.executablejar.ExecutableJarApplication這個類
          public String getMainClass() throws Exception {
            Manifest manifest = getManifest();
            String mainClass = null;
            if (manifest != null) {
              mainClass = manifest.getMainAttributes().getValue("Start-Class");
            }
            if (mainClass == null) {
              throw new IllegalStateException(
                  "No 'Start-Class' manifest entry specified in " + this);
            }
            return mainClass;
          }

          // launch重載方法
          protected void launch(String[] args, String mainClass, ClassLoader classLoader)
              throws Exception 
          {
                // 創(chuàng)建一個MainMethodRunner,并把args和Start-Class傳遞給它
            Runnable runner = createMainMethodRunner(mainClass, args, classLoader);
                // 構(gòu)造新線程
            Thread runnerThread = new Thread(runner);
                // 線程設(shè)置類加載器以及名字,然后啟動
            runnerThread.setContextClassLoader(classLoader);
            runnerThread.setName(Thread.currentThread().getName());
            runnerThread.start();
          }

          MainMethodRunner的run方法:

          @Override
          public void run() {
            try {
              // 根據(jù)Start-Class進行實例化
              Class<?> mainClass = Thread.currentThread().getContextClassLoader()
                  .loadClass(this.mainClassName);
              // 找出main方法
              Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
              // 如果main方法不存在,拋出異常
              if (mainMethod == null) {
                throw new IllegalStateException(
                    this.mainClassName + " does not have a main method");
              }
              // 調(diào)用
              mainMethod.invoke(nullnew Object[] { this.args });
            }
            catch (Exception ex) {
              UncaughtExceptionHandler handler = Thread.currentThread()
                  .getUncaughtExceptionHandler();
              if (handler != null) {
                handler.uncaughtException(Thread.currentThread(), ex);
              }
              throw new RuntimeException(ex);
            }
          }

          Start-Class的main方法調(diào)用之后,內(nèi)部會構(gòu)造Spring容器,啟動內(nèi)置Servlet容器等過程。這些過程我們都已經(jīng)分析過了。

          關(guān)于自定義的類加載器LaunchedURLClassLoader

          LaunchedURLClassLoader重寫了loadClass方法,也就是說它修改了默認(rèn)的類加載方式(先看該類是否已加載這部分不變,后面真正去加載類的規(guī)則改變了,不再是直接從父類加載器中去加載)。LaunchedURLClassLoader定義了自己的類加載規(guī)則:

          private Class<?> doLoadClass(String name) throws ClassNotFoundException {

            // 1) Try the root class loader
            try {
              if (this.rootClassLoader != null) {
                return this.rootClassLoader.loadClass(name);
              }
            }
            catch (Exception ex) {
              // Ignore and continue
            }

            // 2) Try to find locally
            try {
              findPackage(name);
              Class<?> cls = findClass(name);
              return cls;
            }
            catch (Exception ex) {
              // Ignore and continue
            }

            // 3) Use standard loading
            return super.loadClass(name, false);
          }

          加載規(guī)則:

          • 如果根類加載器存在,調(diào)用它的加載方法。這里是根類加載是ExtClassLoader
          • 調(diào)用LaunchedURLClassLoader自身的findClass方法,也就是URLClassLoader的findClass方法
          • 調(diào)用父類的loadClass方法,也就是執(zhí)行默認(rèn)的類加載順序(從BootstrapClassLoader開始從下往下尋找)

          LaunchedURLClassLoader自身的findClass方法:

          protected Class<?> findClass(final String name)
               throws ClassNotFoundException
          {
              try {
                  return AccessController.doPrivileged(
                      new PrivilegedExceptionAction<Class<?>>() {
                          public Class<?> run() throws ClassNotFoundException {
                              // 把類名解析成路徑并加上.class后綴
                              String path = name.replace('.''/').concat(".class");
                              // 基于之前得到的第三方j(luò)ar包依賴以及自己的jar包得到URL數(shù)組,進行遍歷找出對應(yīng)類名的資源
                              // 比如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!/中被找出
                              // 那么找出的資源對應(yīng)的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 defineClass(name, res);
                                  } catch (IOException e) {
                                      throw new ClassNotFoundException(name, e);
                                  }
                              } else { // 找不到資源的話直接拋出ClassNotFoundException異常
                                  throw new ClassNotFoundException(name);
                              }
                          }
                      }, acc);
              } catch (java.security.PrivilegedActionException pae) {
                  throw (ClassNotFoundException) pae.getException();
              }
          }

          下面是LaunchedURLClassLoader的一個測試:

          // 注冊org.springframework.boot.loader.jar.Handler URL協(xié)議處理器
          JarFile.registerUrlProtocolHandler();
          // 構(gòu)造LaunchedURLClassLoader類加載器,這里使用了2個URL,分別對應(yīng)jar包中依賴包spring-boot-loader和spring-boot,使用 "!/" 分開,需要org.springframework.boot.loader.jar.Handler處理器處理
          LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
                  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!/")
                  },
                  LaunchedURLClassLoaderTest.class.getClassLoader());

          // 加載類
          // 這2個類都會在第二步本地查找中被找出(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");

          Spring Boot Loader的作用

          SpringBoot在可執(zhí)行jar包中定義了自己的一套規(guī)則,比如第三方依賴jar包在/lib目錄下,jar包的URL路徑使用自定義的規(guī)則并且這個規(guī)則需要使用org.springframework.boot.loader.jar.Handler處理器處理。它的Main-Class使用JarLauncher,如果是war包,使用WarLauncher執(zhí)行。這些Launcher內(nèi)部都會另起一個線程啟動自定義的SpringApplication類。

          這些特性通過spring-boot-maven-plugin插件打包完成。

          PS:如果覺得我的分享不錯,歡迎大家隨手點贊、在看。

          END

          往期推薦

          永遠(yuǎn)不要在代碼中使用「User」這個單詞!

          使用雪花id或uuid作為Mysql主鍵,被老板懟了一頓!

          紅包免費送!

          分庫分表?如何做到永不遷移數(shù)據(jù)和避免熱點?

          下方二維碼關(guān)注我

          技術(shù)草根,堅持分享 編程,算法,架構(gòu)

          看完文章,餓了點外賣,點擊 ??《無門檻外賣優(yōu)惠券,每天免費領(lǐng)!》

          朋友,助攻一把!點個在看!
          瀏覽 40
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  亚洲大乱婬交换 | 青青草2017在线视频 | 免费网站看sm调教视频 | 欧美性爱一级视频 | 天天操成人电影 |