<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í)行原理

          共 9668字,需瀏覽 20分鐘

           ·

          2022-05-26 23:01


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

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

          spring-boot-maven-plugin

          SpringBoot 的可執(zhí)行jar包又稱fat jar ,是包含所有第三方依賴的 jar 包,jar 包中嵌入了除 java 虛擬機(jī)以外的所有依賴,是一個?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??
          也就是說想要知道fat jar是如何生成的,就必須知道spring-boot-maven-plugin工作機(jī)制,而spring-boot-maven-plugin屬于自定義插件,因此我們又必須知道,Maven的自定義插件是如何工作的

          Maven的自定義插件

          Maven 擁有三套相互獨(dú)立的生命周期: clean、default 和 site, 而每個生命周期包含一些phase階段, 階段是有順序的, 并且后面的階段依賴于前面的階段。生命周期的階段phase與插件的目標(biāo)goal相互綁定,用以完成實(shí)際的構(gòu)建任務(wù)。
          <plugin>??
          ????<groupId>org.springframework.bootgroupId>??
          ????<artifactId>spring-boot-maven-pluginartifactId>??
          ????<executions>??
          ????????<execution>??
          ????????????<goals>??
          ????????????????<goal>repackagegoal>??
          ????????????goals>??
          ????????execution>??
          ????executions>??
          plugin>??
          repackage目標(biāo)對應(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());??
          ????//查找并過濾項目運(yùn)行時依賴的jar??
          ???Set?artifacts?=?filterDependencies(this.project.getArtifacts(),??
          ?????????getFilters(getAdditionalFilters()));??
          ????//將artifacts轉(zhuǎn)換成libraries??
          ???Libraries?libraries?=?new?ArtifactsLibraries(artifacts,?this.requiresUnpack,??
          ?????????getLog());??
          ???try?{??
          ???????//提供Spring?Boot啟動腳本??
          ??????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這個方法,知道Repackager是如何生成的,也就大致能夠推測出內(nèi)在的打包邏輯。
          private?Repackager?getRepackager(File?source)?{??
          ???Repackager?repackager?=?new?Repackager(source,?this.layoutFactory);??
          ???repackager.addMainClassTimeoutWarningListener(??
          ?????????new?LoggingMainClassTimeoutWarningListener());??
          ????//設(shè)置main?class的名稱,如果不指定的話則會查找第一個包含main方法的類,repacke最后將會設(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我們可以將之翻譯為文件布局,或者目錄布局,代碼一看清晰明了,同時我們需要關(guān)注,也是下一個重點(diǎn)關(guān)注對象org.springframework.boot.loader.JarLauncher,從名字推斷,這很可能是返回可執(zhí)行jar文件的啟動類。

          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文件為以上信息,可以看到兩個關(guān)鍵信息Main-ClassStart-Class。我們可以進(jìn)一步,程序的啟動入口并不是我們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抽象出來的統(tǒng)一訪問資源的層

            • JarFileArchivejar包文件的抽象
            • ExplodedArchive文件目錄
          這里重點(diǎn)描述一下JarFile的作用,每個JarFileArchive都會對應(yīng)一個JarFile。在構(gòu)造的時候會解析內(nèi)部結(jié)構(gòu),去獲取jar包里的各個文件或文件夾類。我們可以看一下該類的注釋。
          /*?Extended?variant?of?{@link?java.util.jar.JarFile}?that?behaves?in?the?same?way?but??
          *?offers?the?following?additional?functionality.??
          *?
            ??
            *?
          • A?nested?{@link?JarFile}?can?be?{@link?#getNestedJarFile(ZipEntry)?obtained}?based??
            *?on?any?directory?entry.
          • ??
            *?
          • A?nested?{@link?JarFile}?can?be?{@link?#getNestedJarFile(ZipEntry)?obtained}?for??
            *?embedded?JAR?files?(as?long?as?their?entry?is?not?compressed).
          • ??
          ??
          **/
          ??
          jar里的資源分隔符是!/,在JDK提供的JarFile URL只支持一個’!/‘,而Spring boot擴(kuò)展了這個協(xié)議,讓它支持多個’!/‘,就可以表示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)是,如果一個類可以被委派最基礎(chǔ)的ClassLoader加載,就不能讓高層的ClassLoader加載,這樣是為了范圍錯誤的引入了非JDK下但是類名一樣的類。
          其二,如果在這個機(jī)制下,由于fat jar中依賴的各個第三方j(luò)ar文件,并不在程序自己classpath下,也就是說,如果我們采用雙親委派機(jī)制的話,根本獲取不到我們所依賴的jar包,因此我們需要修改雙親委派機(jī)制的查找class的方法,自定義類加載機(jī)制。
          先簡單的介紹Springboot2中LaunchedURLClassLoader,該類繼承了java.net.URLClassLoader,重寫了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我們再探討他是如何修改雙親委派機(jī)制。
          在上面我們講到Spring boot支持多個’!/‘以表示多個jar,而我們的問題在于,如何解決查找到這多個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í)行查找類方法,該類的查找來源即構(gòu)造方法傳遞進(jìn)來的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)起來??
          ?????????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ù)先過濾掉查找不到的包??
          ????????????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í)際上會回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循雙親委派機(jī)制進(jìn)行查找類,而Bootstrap ClassLoader和Extension ClassLoader將會查找不到fat jar依賴的類,最終會來到Application ClassLoader,調(diào)用java.net.URLClassLoader#findClass

          如何真正的啟動

          Springboot2和Springboot1的最大區(qū)別在于,Springboo1會新起一個線程,來執(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)用邏輯比較簡單,這里就不再分析,比較關(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個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:/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");??
          //?在第三步使用默認(rèn)的加載順序在ApplicationClassLoader中被找出??
          ???classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");??
          ??
          //????????SpringApplication.run(Application.class,?args);??
          ????}??
          ??
          <dependency>??
          ????<groupId>org.springframework.bootgroupId>??
          ????<artifactId>spring-boot-loaderartifactId>??
          ????<version>2.1.3.RELEASEversion>??
          dependency>??
          <dependency>??
          ????<groupId>org.springframework.bootgroupId>??
          ????<artifactId>spring-boot-maven-pluginartifactId>??
          ????<version>2.1.3.RELEASEversion>??

          dependency>??

          總結(jié)

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

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

          來源:juejin.im/post/5d2d6812e51d45777b1a3e5a


          瀏覽 39
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  黄色电影网站在线观看 | 99爱视频在线观看这里只有精品 | 免费成人性爱视频 | 孕妇孕交性一级A片 | 欧美午夜精品久久久久久蜜 |