SpringBoot構建Jar包源碼分析

我們知道,SpringBoot僅憑一個Jar包就能將我們構建的整個工程跑起來,如果你也想知道這個能跑起來的jar內部結構是如何構建出來的,請耐心讀完本篇,本篇內容可能有點多,但包你有收穫。如果讀完沒有收穫,請拉到文章最後,我再告訴你一個絕招。

分析Springboot重構Jar包源碼前我們先按平常方式創建一個springboot項目,通過IDEA或springboot提供的網站(https://start.spring.io/)很容易就可以創建出一個springboot web工程。注意:創建時要把Web Starter選擇上,不然後面不會啟動Tomcat容器。創建好的項目目錄如下:

SpringBoot構建Jar包源碼分析

pom.xml文件如下:

<code>

<project>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelversion>4.0.0/<modelversion>

<parent>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-starter-parent/<artifactid>

<version>2.2.2.RELEASE/<version>

<relativepath>

/<parent>

<groupid>com.sourcecode.analysis/<groupid>

<artifactid>sourcecode-analysis-springboot/<artifactid>

<version>0.0.1-SNAPSHOT/<version>

<name>sourcecode-analysis-springboot/<name>

<description>Demo project for Spring Boot/<description>



<properties>

<java.version>1.8/<java.version>

/<properties>



<dependencies>

<dependency>

<groupid>org.springframework.boot/<groupid>


<artifactid>spring-boot-starter/<artifactid>

/<dependency>



<dependency>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-starter-web/<artifactid>

/<dependency>



<dependency>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-starter-test/<artifactid>

<scope>test/<scope>

<exclusions>

<exclusion>

<groupid>org.junit.vintage/<groupid>

<artifactid>junit-vintage-engine/<artifactid>

/<exclusion>

/<exclusions>

/<dependency>

/<dependencies>



<build>

<plugins>

<plugin>

<groupid>org.springframework.boot/<groupid>


<artifactid>spring-boot-maven-plugin/<artifactid>

/<plugin>

/<plugins>

/<build>



/<project>/<code>

SourcecodeAnalysisSpringbootApplication.java類如下:

<code>package com.sourcecode.analysis.springboot;



import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;



@SpringBootApplication

public class SourcecodeAnalysisSpringbootApplication {



public static void main(String[] args) {

SpringApplication.run(SourcecodeAnalysisSpringbootApplication.class, args);

}



}/<code>

application.properties文件默認是空的。

當springboot項目創建好之後,我們可以直接運行SourcecodeAnalysisSpringbootApplication.java類的main方法即可啟動web工程,啟動成功控制檯如下:(默認端口8080)

SpringBoot構建Jar包源碼分析

除此之外我們也可以通過執行maven package方式將項目打成jar包,通過java -jar 命令來運行,運行效果如下:

SpringBoot構建Jar包源碼分析

springboot項目創建過程就是這麼多,接下來就可以在這個腳手架基礎上去填我們的業務代碼了。在springboot之前要實現這個過程可是要配置一大堆xml的,但是現在這些事情springboot通過starter的方式幫我們做了。由於我們本章主要內容是講解springboot重構jar包的源碼,關於springboot starter如何幫我們簡化配置的等到後面有時間再寫一篇來介紹。

當通過java -jar命令啟動jar包時,首先會先從jar包中META-INF/MANIFEST.MF文件中找到Main-Class的值作為主類來運行jar包,這是java基礎知識。所以說要了解springboot是如何啟動的,我們首先需要將springboot打出來的jar包解壓出來,找到META-INF/MANIFEST.MF文件並打開,我們可以看到大概如下內容:

<code>Manifest-Version: 1.0

Implementation-Title: sourcecode-analysis-springboot

Implementation-Version: 0.0.1-SNAPSHOT

Start-Class: com.sourcecode.analysis.springboot.SourcecodeAnalysisSpri

ngbootApplication

Spring-Boot-Classes: BOOT-INF/classes/

Spring-Boot-Lib: BOOT-INF/lib/

Build-Jdk-Spec: 1.8

Spring-Boot-Version: 2.2.2.RELEASE

Created-By: Maven Archiver 3.4.0

Main-Class: org.springframework.boot.loader.JarLauncher/<code>

可以看到Main-Class的值為: org.springframework.boot.loader.JarLauncher。通過java -jar運行時執行的主方法便是 org.springframework.boot.loader.JarLauncher類的main方法。而我們通過IDEA手工運行的主類被配置為key為Start-Class的值中,這個類是怎麼寫到META-INF/MANIFEST.MF文件中的呢?因為我們前面是通過maven package命令打包出來的,所以要解開這個問題我們要回到maven打包階段去思考,想到這裡此時我們應該看下pom.xml的插件配置,可以看到如下核心配置:

<code><build>


<plugins>

<plugin>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-maven-plugin/<artifactid>

/<plugin>

/<plugins>

/<build>/<code>

從名字感官上可以看出這應該是springboot打包插件,我們可以大膽猜測springboot通過自己實現的maven package階段插件對maven-jar-plugin插件創建的jar動了手腳。為了找到具體線索,查看maven package命令日誌,可以看到打包階段執行完maven-jar-plugin插件後接下來果然是執行了spring-boot-maven-plugin插件中的repackage進行jar重構。

SpringBoot構建Jar包源碼分析

為了深入瞭解springboot是如何對maven-jar-plugin插件原始jar包動手腳的,我們把springboot源碼下載下來準備進行源碼分析,springboot github 源碼clone地址:https://github.com/spring-projects/spring-boot.git。克隆下來源碼目錄結構如下:

緊接著我們順藤摸瓜,通過全局搜索repackag關鍵字的方式從springboot源碼中找到一個註解@Mojo為repackage的RepackageMojo類文件:

這個類頂層繼承自maven插件抽象父類AbstractMojo,且被定義為打包階段執行,這個類正是spring-boot-maven-plugin插件中執行的repackage,我們找到插件默認執行的execute方法源碼如下:

<code>@Override

public void execute() throws MojoExecutionException, MojoFailureException {

if (this.project.getPackaging().equals("pom")) {

getLog().debug("repackage goal could not be applied to pom project.");

return;

}

if (this.skip) {

getLog().debug("skipping repackaging as per configuration.");

return;

}



//重新打包

repackage();

}/<code>

我們繼續跟入repackage()方法,源碼如下:

<code>private void repackage() throws MojoExecutionException {

// 獲取maven-jar-plugin插件構建的jar包 Artifact對象

Artifact source = getSourceArtifact();

// 獲取maven-jar-plugin插件構建的jar包 File對象

File target = getTargetFile();

// 實例化重新打包對象,整個打包工作基本由這個對象完成

//將maven-jar-plugin插件構建的jar包 File對象傳入給source屬性

Repackager repackager = getRepackager(source.getFile());

// 過濾掉spring-boot-devtools依賴以及系統本地的依賴

// this.project.getArtifacts()返回

//pom.xml當前階段範圍的所有依賴信息對象集合,包含傳遞過來的

Set<artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));

// 創建lib信息對象,這裡純粹傳參實例化沒有邏輯

Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());

try {

// 獲取sh應用管理腳本(start/stop/restart),可配置


// 這裡默認返回null,唯有配置插件時配置了executable

//或embeddedLaunchScript時才會實例化

LaunchScript launchScript = getLaunchScript();

// 進入核心重構jar方法

repackager.repackage(target, libraries, launchScript);

}

catch (IOException ex) {

throw new MojoExecutionException(ex.getMessage(), ex);

}

//將重構好的File對象set到Artifact對象中,更新jar包文件

updateArtifact(source, target, repackager.getBackupFile());

}/<artifact>/<code>

這個方法核心邏輯如下:

1、實例化maven-jar-plugin插件構建的原始jar包(後面統稱這個jar為原始jar)文件對象

2、實例化啟動腳本LaunchScript對象,由於沒有配置參數,所以這裡返回null

3、實例化Libraries依賴信息對象,這裡原理是讀取pom.xml依賴後轉為Set<artifact>集合​/<artifact>

4、實例化重構jar包核心工作對象Repackager

5、調用Repackager對象的repackager重構jar方法,將實例化好的參數帶入

6、最後重構完成後更新原來Artifact File對象

我們繼續跟入Repackager對象的repackage方法,源碼如下:

<code>public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException {







//參數校驗

if (destination == null || destination.isDirectory()) {

throw new IllegalArgumentException("Invalid destination");

}

if (libraries == null) {

throw new IllegalArgumentException("Libraries must not be null");

}





//實例化layout屬性



//layout作用是定義重構jar包內部目錄名稱



//以及定義是否需要寫入classloader使jar文件可執行,默認true

if (this.layout == null) {

//這裡默認根據文件後綴判斷實例化哪個佈局對象,



//分別有Jar War Expanded(zip)等,默認實例化JarLayout對象

this.layout = getLayoutFactory().getLayout(this.source);

}



// maven-jar-plugin構建的原始jar包File對象

destination = destination.getAbsoluteFile();

File workingSource = this.source;

if (alreadyRepackaged() && this.source.equals(destination)) {

return;

}

if (this.source.equals(destination)) {



workingSource = getBackupFile();

workingSource.delete();



// 將原始jar包備份(重命名)為原始jar名稱.original文件

renameFile(this.source, workingSource);

}



//刪除原始jar文件

destination.delete();



try {

//JarFile是jdk自帶的類,可從中獲取jar對象信息

try (JarFile jarFileSource = new JarFile(workingSource)) {

//將備份jar作為源,將刪除後的原始jar作新的目的地,



//傳入lib對象、啟動腳本等參數進入repackage對象的重載方法

repackage(jarFileSource, destination, libraries, launchScript);

}

}

finally {



if (!this.backupSource && !this.source.equals(workingSource)) {

//目前版本源碼中,這裡永遠不會執行 因為backupSource寫死為true

deleteFile(workingSource);

}

}

}/<code>

這個方法核心邏輯如下:

對傳入的參數做必要性判斷

根據jar包後綴判斷初始化layout對象,這裡初始化的是JarLayout,JarLayout主要佈局信息為:BOOT-INF/lib為lib目錄,BOOT-INF/classes為class類文件目錄,需要寫入springboot自定義的classloader使jar可正常執行

將maven-jar-plugin插件構建的原始jar包備份後綴為original文件中,並將原始jar包刪除,這麼做目的是準備好從備份original文件到原始jar包的重構環境

進入Repackager對象的repackage重載方法,將準備好的參數帶入

我們繼續跟入Repackager對象的repackage重載方法,源碼如下:

<code>private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript)

throws IOException {



// 遍歷前面傳入的artifacts



//並封裝到內部Map<string> libraryEntryNames中

// 過程中將BOOT-INF/lib/目錄拼接上去,



//例如put(“BOOT-INF/lib/aaa.jar”, Library)

// 同時會對判斷是否存在重複依賴,存在會報錯Duplicate library aaa.jar

WritableLibraries writeableLibraries = new WritableLibraries(libraries);



// JarWriter是springboot封裝了apache的JarArchiveOutputStream類

// 底層就是ziparchive版本的OutputStream輸出流,支持往jar寫東西的對象

// 這裡簡單理解成new FileOutputStream(new 前面被刪除的原始jar文件對象),

// 意思是準備往前面被刪除的原始jar文件寫入東西(刪除後文件是乾淨的)

try (JarWriter writer = new JarWriter(destination, launchScript)) {



// 寫入META-INF/MANIFEST.MF文件內容

// 我們看到的META-INF/MANIFEST.MF文件內容全部都是這個方法寫入的

writer.writeManifest(buildManifest(sourceJar));



// 根據layout配置寫入SpringBoot實現的ClassLoader

// 寫入classloader原因是重構後的jar包的lib目錄為BOOT-INF/lib,

// 外部的classloader並不知道重構後的lib應該到哪裡加載,



// 所以springboot插件需要實現一個classloader並設置到線程上下文



// 給後續加載類時使用,這裡寫入的classloader是springboot



// 項目的其中一個module,打成jar包後解壓寫入進去的

writeLoaderClasses(writer);





if (this.layout instanceof RepackagingLayout) {

//將備份的original中class文件和其他資源文件全部寫入到原始jar的



//BOOT-INF/classes/目錄下,寫入過程進行了SHA-1簽名

writer.writeEntries(sourceJar,

new RenamingEntryTransformer(((RepackagingLayout) this.layout).getRepackagedClassesLocation()),

writeableLibraries);

}

else {

writer.writeEntries(sourceJar, writeableLibraries);

}



// 最後遍歷前面WritableLibraries對象內部



// Map<string> libraryEntryNames

// 將所有lib寫入到原始jar中

writeableLibraries.write(writer);

}

}/<string>/<string>/<code>

這個方法從頂層直觀告訴我們springboot對原始jar包動手腳的整個先後順序。核心邏輯如下:

實例化lib信息預寫對象,主要是遍歷libratis對象的artifacts屬性,封裝到map中

將原始jar File對象包裝成JarWrite對象,準備往乾淨的(因為前面被刪過)原始jar文件寫入新的東西

往原始jar寫入META-INF/MANIFEST.MF文件內容

往原始jar寫入classloader

從備份的original文件複製class類文件以及其他資源文件寫入到原始jar BOOT-INF/classes目錄下

將從pom.xml解析到的artifacts lib依賴包文件流寫入到原始jar BOOT-INF/lib目錄下

為了更加深度解析,我們分別進入每個寫入方法大致過一下處理邏輯源碼,下面邏輯都寫在每一行代碼註釋中,請用心看註釋描述。另外配套前面頂層源碼的截圖信息。

實例化lib信息預寫對象源碼如下:

<code>// Repackager類的一個內部類

private final class WritableLibraries implements UnpackHandler {



private final Map<string> libraryEntryNames = new LinkedHashMap<>();



//外部new調用的參數構造器

private WritableLibraries(Libraries libraries) throws IOException {

// libraries實現類為ArtifactsLibraries,前面分析代碼過程new出來的

// doWithLibraries方法代碼下面有截圖,邏輯很簡單就是遍歷artifact,然後

// 判斷去重後調用這裡的箭頭函數,最後工作就是保存到map中

libraries.doWithLibraries((library) -> {

if (isZip(library.getFile())) {

// 這裡layout代碼如下,寫死ruturn BOOT-INF/lib/

// public String getLibraryDestination(String libraryName, LibraryScope scope) {

// return "BOOT-INF/lib/";

// }

String libraryDestination = Repackager.this.layout.getLibraryDestination(library.getName(),

library.getScope());

if (libraryDestination != null) {




// 放到map中,key=BOOT-INF/lib/ + jar名稱, value=library信息對象

// 這裡提一下,library裡面有個File file屬性,這個屬性才是jar文件對象

Library existing = this.libraryEntryNames.putIfAbsent(libraryDestination + library.getName(),

library);

if (existing != null) {

throw new IllegalStateException("Duplicate library " + library.getName());

}

}

}

});

}



// 判斷是否允許打開文件,主要後面sha1Hash簽名時判斷用

@Override

public boolean requiresUnpack(String name) {

Library library = this.libraryEntryNames.get(name);

return library != null && library.isUnpackRequired();

}



// 文件加密方法

@Override

public String sha1Hash(String name) throws IOException {

Library library = this.libraryEntryNames.get(name);

if (library == null) {

throw new IllegalArgumentException("No library found for entry name '" + name + "'");

}

return FileUtils.sha1Hash(library.getFile());

}



//可以看到寫入是通過外部傳入的JarWriter來執行的

//這個外部JarWriter實際就是原始jar的JarWriter對象

private void write(JarWriter writer) throws IOException {

for (Entry<string> entry : this.libraryEntryNames.entrySet()) {

writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),

entry.getValue());

}

}



}



ArtifactsLibraries對象的doWithLibraries方法源碼如下:

public void doWithLibraries(LibraryCallback callback) throws IOException {

Set<string> duplicates = getDuplicates(this.artifacts);

for (Artifact artifact : this.artifacts) {

LibraryScope scope = SCOPES.get(artifact.getScope());

if (scope != null && artifact.getFile() != null) {

String name = getFileName(artifact);

// 處理重複jar名稱

if (duplicates.contains(name)) {

this.log.debug("Duplicate found: " + name);

name = artifact.getGroupId() + "-" + name;

this.log.debug("Renamed to: " + name);

}

// 回調傳入的函數,artifact.getFile()是重點,

// 它是真正jar的File 對象,可以讀出來寫入到其它地方

callback.library(new Library(name, artifact.getFile(), scope, isUnpackRequired(artifact)));

}

}

}/<string>/<string>/<string>/<code>


2、將原始jar File對象包裝成JarWrite對象源碼如下:

寫入META-INF/MANIFE文件內容源碼如下:

<code>public JarWriter(File file, LaunchScript launchScript) throws FileNotFoundException, IOException {

FileOutputStream fileOutputStream = new FileOutputStream(file);

if (launchScript != null) {

// 如果存在腳本,直接寫入原始jar文件


fileOutputStream.write(launchScript.toByteArray());

// 修改下文件執行權限

setExecutableFilePermission(file);

}

this.jarOutput = new JarArchiveOutputStream(fileOutputStream);

this.jarOutput.setEncoding("UTF-8");

}/<code>

buildManifest源碼如下:

<code>private Manifest buildManifest(JarFile source) throws IOException {

// 備份original文件的Manifest文件

Manifest manifest = source.getManifest();

if (manifest == null) {

manifest = new Manifest();

manifest.getMainAttributes().putValue("Manifest-Version", "1.0");

}

// 重新實例化一個新的Manifest文件

manifest = new Manifest(manifest);

String startClass = this.mainClass;

if (startClass == null) {

startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);

}

if (startClass == null) {

//這裡根據規則去找Application啟動類,也就是Start-Class的值

//IDEA我們手工run的那個主類

//如果查找超過一定時間會發出警告,建議讓你手工配置,查找比較耗時

startClass = findMainMethodWithTimeoutWarning(source);

}

//layout。getLauncherClassName方法如下:

// public String getLauncherClassName() {

// return "org.springframework.boot.loader.JarLauncher";

// }

String launcherClassName = this.layout.getLauncherClassName();

if (launcherClassName != null) {

// 寫入 Main-Class

manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, launcherClassName);

if (startClass == null) {

throw new IllegalStateException("Unable to find main class");

}

// 寫入 Start-Class

manifest.getMainAttributes().putValue(START_CLASS_ATTRIBUTE, startClass);

}

else if (startClass != null) {

manifest.getMainAttributes().putValue(MAIN_CLASS_ATTRIBUTE, startClass);

}

String bootVersion = getClass().getPackage().getImplementationVersion();

manifest.getMainAttributes().putValue(BOOT_VERSION_ATTRIBUTE, bootVersion);

// 寫入Spring-Boot-Classes = BOOT-INF/classes/

manifest.getMainAttributes().putValue(BOOT_CLASSES_ATTRIBUTE, (this.layout instanceof RepackagingLayout)

? ((RepackagingLayout) this.layout).getRepackagedClassesLocation() : this.layout.getClassesLocation());

String lib = this.layout.getLibraryDestination("", LibraryScope.COMPILE);

if (StringUtils.hasLength(lib)) {

// 寫入Spring-Boot-Lib = BOOT-INF/lib/

manifest.getMainAttributes().putValue(BOOT_LIB_ATTRIBUTE, lib);

}

return manifest;

}



writeManifest關鍵源碼如下:

public void writeManifest(Manifest manifest) throws IOException {

JarArchiveEntry entry = new JarArchiveEntry("META-INF/MANIFEST.MF");

writeEntry(entry, manifest::write);

}



writeEntry有多個重載方法,最終都走的是下面的重載方法:

private void writeEntry(JarArchiveEntry entry, EntryWriter entryWriter, UnpackHandler unpackHandler)

throws IOException {



String parent = entry.getName();


if (parent.endsWith("/")) {

parent = parent.substring(0, parent.length() - 1);

entry.setUnixMode(UnixStat.DIR_FLAG | UnixStat.DEFAULT_DIR_PERM);

}

else {

entry.setUnixMode(UnixStat.FILE_FLAG | UnixStat.DEFAULT_FILE_PERM);

}

if (parent.lastIndexOf('/') != -1) {

parent = parent.substring(0, parent.lastIndexOf('/') + 1);

if (!parent.isEmpty()) {

writeEntry(new JarArchiveEntry(parent), null, unpackHandler);

}

}



if (this.writtenEntries.add(entry.getName())) {

// 這裡判斷是否允許打開文件,如果允許則往裡面寫入sha1Hash簽名

entryWriter = addUnpackCommentIfNecessary(entry, entryWriter, unpackHandler);

//前面實例化的原始jar的JarArchiveOutputStream對象

//這裡代表寫入entry到輸出流內存中

this.jarOutput.putArchiveEntry(entry);

if (entryWriter != null) {

//將輸出流寫到entryWriter對象

entryWriter.write(this.jarOutput);

}

//關閉資源

this.jarOutput.closeArchiveEntry();

}

}/<code>

writeLoaderClasses源碼如下:

<code>private void writeLoaderClasses(JarWriter writer) throws IOException {

// 自定義佈局時才走這裡,一般很少人這麼閒自己實現calssloader的

if (this.layout instanceof CustomLoaderLayout) {

((CustomLoaderLayout) this.layout).writeLoadedClasses(writer);

}

// layout.isExecutable固定返回true,所有進入這裡

else if (this.layout.isExecutable()) {

writer.writeLoaderClasses();

}/<code>

連環調用源碼如下:

<code>private static final String NESTED_LOADER_JAR = "META-INF/loader/spring-boot-loader.jar";



public void writeLoaderClasses() throws IOException {

writeLoaderClasses(NESTED_LOADER_JAR);


}


public void writeLoaderClasses(String loaderJarResourceName) throws IOException {

// getClass().getClassLoader()返回的是JDK的sun.misc.Launcher$AppClassLoader類加載器

// 獲取META-INF/loader/spring-boot-loader.jar文件

URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName);

try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) {

JarEntry entry;

while ((entry = inputStream.getNextJarEntry()) != null) {

if (entry.getName().endsWith(".class")) {

// 遍歷spring-boot-loader.jar文件

//把所有的.class文件寫入到原始jar包中

writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream));

}

}

}

}/<code>


從備份original文件複製class文件源碼如下:

連帶調用源碼如下:

<code>void writeEntries(JarFile jarFile, EntryTransformer entryTransformer, UnpackHandler unpackHandler)

throws IOException {

Enumeration<jarentry> entries = jarFile.entries();

//遍歷整個備份的original文件

while (entries.hasMoreElements()) {

JarArchiveEntry entry = new JarArchiveEntry(entries.nextElement());

setUpEntry(jarFile, entry);

try (ZipHeaderPeekInputStream inputStream = new ZipHeaderPeekInputStream(jarFile.getInputStream(entry))) {

EntryWriter entryWriter = new InputStreamEntryWriter(inputStream);

JarArchiveEntry transformedEntry = entryTransformer.transform(entry);

if (transformedEntry != null) {

//將所有class類文件以及其他資源文件寫入到原始jar中

//這裡走的是前面分析的帶簽名的writeEntry重裝方法

writeEntry(transformedEntry, entryWriter, unpackHandler);

}

}

}

}/<jarentry>/<code>

寫入lib資源源碼如下:

這個方法前面實例化lib預寫對象時已經看過了,這裡再放出來一遍:

//可以看到寫入是通過外部傳入的JarWriter來執行的

//這個外部JarWriter實際就是原始jar的JarWriter對象

<code>private void write(JarWriter writer) throws IOException {

for (Entry<string> entry : this.libraryEntryNames.entrySet()) {

writer.writeNestedLibrary(entry.getKey().substring(0, entry.getKey().lastIndexOf('/') + 1),

entry.getValue());

}

}/<string>/<code>


整個springboot重構jar包源碼分析到此就算結束了,你get到了嗎?

如果你讀完還一頭霧水沒啥收穫,這裡給出的絕招是:請集中精力,再讀一遍!


分享到:


相關文章: