您好,登錄后才能下訂單哦!
小編給大家分享一下簡單分析SpringBoot java-jar命令行啟動原理,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討方法吧!
在spring boot里,很吸引人的一個特性是可以直接把應用打包成為一個jar/war,然后這個jar/war是可以直接啟動的,而不需要另外配置一個Web Server。那么spring boot如何啟動的呢?今天我們就來一起探究一下它的原理。首先我們來創建一個基本的spring boot工程來幫助我們分析,本次spring boot版本為 2.2.5.RELEASE。
// SpringBootDemo.java @SpringBootApplication public class SpringBootDemo { public static void main(String[] args) { SpringApplication.run(SpringBootDemo.class); } }
下面是pom依賴:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <finalName>springboot-demo</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
創建完工程后,執行maven的打包命令,會生成兩個jar文件:
springboot-demo.jar
springboot-demo.jar.original
其中springboot-demo.jar.original
是默認的maven-jar-plugin
生成的包。springboot-demo.jar是spring boot maven插件生成的jar包,里面包含了應用的依賴,以及spring boot相關的類。下面稱之為executable jar或者fat jar。后者僅包含應用編譯后的本地資源,而前者引入了相關的第三方依賴,這點從文件大小也能看出。
圖1
關于executable jar,spring boot官方文檔中是這樣解釋的。
Executable jars (sometimes called “fat jars”) are archives containing your compiled classes along with all of the jar dependencies that your code needs to run.
Executable jar(有時稱為“fat jars”)是包含您的已編譯類以及代碼需要運行的所有jar依賴項的歸檔文件。
Java does not provide any standard way to load nested jar files (that is, jar files that are themselves contained within a jar). This can be problematic if you need to distribute a self-contained application that can be run from the command line without unpacking.
Java沒有提供任何標準的方式來加載嵌套的jar文件(即,它們本身包含在jar中的jar文件)。如果您需要分發一個自包含的應用程序,而該應用程序可以從命令行運行而無需解壓縮,則可能會出現問題。
To solve this problem, many developers use “shaded” jars. A shaded jar packages all classes, from all jars, into a single “uber jar”. The problem with shaded jars is that it becomes hard to see which libraries are actually in your application. It can also be problematic if the same filename is used (but with different content) in multiple jars.
為了解決這個問題,許多開發人員使用 shaded jars。 一個 shaded jar 將來自所有jar的所有類打包到一個 uber(超級)jar 中。 shaded jars的問題在于,很難查看應用程序中實際包含哪些庫。 如果在多個jar中使用相同的文件名(但具有不同的內容),也可能會產生問題。
Spring Boot takes a different approach and lets you actually nest jars directly.
Spring Boot采用了另一種方法,實際上允許您直接嵌套jar。
簡單來說,Java標準中是沒有來加載嵌套的jar文件,就是jar中的jar的方式的,為了解決這一問題,很多開發人員采用shaded jars,但是這種方式會有一些問題,而spring boot采用了不同于shaded jars的另一種方式。
Executable Jar 文件結構
那么spring boot具體是如何實現的呢?帶著這個疑問,先來查看spring boot打好的包的目錄結構(不重要的省略掉):
圖6
可以發現,文件目錄遵循了下面的規范:
Application classes should be placed in a nestedBOOT-INF/classes
directory. Dependencies should be placed in a nested BOOT-INF/lib
directory.
應用程序類應該放在嵌套的BOOT-INF/classes目錄中。依賴項應該放在嵌套的BOOT-INF/lib目錄中。
我們通常在服務器中使用java -jar
命令啟動我們的應用程序,在Java官方文檔是這樣描述的:
Executes a program encapsulated in a JAR file. The filename argument is the name of a JAR file with a manifest that contains a line in the form Main-Class:classname that defines the class with the public static void main(String[] args) method that serves as your application's starting point.
執行封裝在JAR文件中的程序。filename參數是具有清單的JAR文件的名稱,該清單包含Main-Class:classname形式的行,該行使用公共靜態void main(String [] args)方法定義該類,該方法充當應用程序的起點。
When you use the -jar option, the specified JAR file is the source of all user classes, and other class path settings are ignored.
使用-jar選項時,指定的JAR文件是所有用戶類的源,而其他類路徑設置將被忽略。
簡單說就是,java -jar 命令引導的具體啟動類必須配置在清單文件 MANIFEST.MF 的 Main-Class 屬性中,該命令用來引導標準可執行的jar文件,讀取的是 MANIFEST.MF文件的Main-Class 屬性值,Main-Class 也就是定義包含了main方法的類代表了應用程序執行入口類。
那么回過頭再去看下之前打包好、解壓之后的文件目錄,找到 /META-INF/MANIFEST.MF 文件,看下元數據:
Manifest-Version: 1.0 Implementation-Title: spring-boot-demo Implementation-Version: 1.0-SNAPSHOT Start-Class: com.example.spring.boot.demo.SpringBootDemo Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Build-Jdk-Spec: 1.8 Spring-Boot-Version: 2.2.5.RELEASE Created-By: Maven Archiver 3.4.0 Main-Class: org.springframework.boot.loader.JarLauncher
可以看到Main-Class是org.springframework.boot.loader.JarLauncher,說明項目的啟動入口并不是我們自己定義的啟動類,而是JarLauncher。而我們自己的項目引導類com.example.spring.boot.demo.SpringBootDemo,定義在了Start-Class屬性中,這個屬性并不是Java標準的MANIFEST.MF文件屬性。
spring-boot-maven-plugin 打包過程
我們并沒有添加org.springframework.boot.loader下的這些類的依賴,那么它們是如何被打包在 FatJar 里面的呢?這就必須要提到spring-boot-maven-plugin插件的工作機制了 。對于每個新建的 spring boot工程,可以在其 pom.xml 文件中看到如下插件:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
這個是 SpringBoot 官方提供的用于打包 FatJar 的插件,org.springframework.boot.loader 下的類其實就是通過這個插件打進去的;
當我們執行package命令的時候會看到下面這樣的日志:
[INFO] --- spring-boot-maven-plugin:2.2.5.RELEASE:repackage (repackage) @ spring-boot-demo ---
[INFO] Replacing main artifact with repackaged archive
repackage目標對應的將執行到org.springframework.boot.maven.RepackageMojo#execute,該方法的主要邏輯是調用了org.springframework.boot.maven.RepackageMojo#repackage
// RepackageMojo.java 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()); } // 繼續跟蹤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); repackager.setLayout(this.layout.layout()); } return repackager; }
repackager設置了 layout方法的返回對象,也就是org.springframework.boot.loader.tools.Layouts.Jar
/** * 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我們可以將之翻譯為文件布局,或者目錄布局,代碼一看清晰明了,同時我們又發現了定義在MANIFEST.MF 文件的Main-Class屬性org.springframework.boot.loader.JarLauncher了,看來我們的下面的重點就是研究一下這個JarLauncher了。
JarLauncher構造過程
因為org.springframework.boot.loader.JarLauncher的類是在spring-boot-loader中的,關于spring-boot-loader,spring boot的github上是這樣介紹的:
Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched usingjava -jar
. Generally you will not need to use spring-boot-loader
directly, but instead work with the Gradle or Maven plugin.
Spring Boot Loader提供了秘密工具,可讓您構建可以使用java -jar啟動的單個jar文件。通常,您不需要直接使用spring-boot-loader,而可以使用Gradle或Maven插件。
但是若想在IDEA中來看源碼,需要在pom文件中引入如下配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> <scope>provided</scope> </dependency>
找到org.springframework.boot.loader.JarLauncher類
// JarLauncher.java public class JarLauncher extends ExecutableArchiveLauncher { // BOOT-INF/classes/ static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; // BOOT-INF/lib/ static final String BOOT_INF_LIB = "BOOT-INF/lib/"; public JarLauncher() { } protected JarLauncher(Archive archive) { super(archive); } @Override protected boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } return entry.getName().startsWith(BOOT_INF_LIB); } // main方法 public static void main(String[] args) throws Exception { new JarLauncher().launch(args); } }
可以發現,JarLauncher定義了BOOT_INF_CLASSES和BOOT_INF_LIB兩個常量,正好就是前面我們解壓之后的兩個文件目錄。JarLauncher包含了一個main方法,作為應用的啟動入口。
從 main 來看,只是構造了一個 JarLauncher對象,然后執行其 launch 方法 。再來看一下JarLauncher的繼承結構:
圖2
構造JarLauncherd對象時會調用父類ExecutableArchiveLauncher的構造方法:
// ExecutableArchiveLauncher.java public ExecutableArchiveLauncher() { try { // 構造 archive 對象 this.archive = createArchive(); } catch (Exception ex) { throw new IllegalStateException(ex); } } // 構造 archive 對象 protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null; // 這里就是拿到當前的 classpath 的絕對路徑 String path = (location != null) ? location.getSchemeSpecificPart() : null; if (path == null) { throw new IllegalStateException("Unable to determine code source archive"); } File root = new File(path); if (!root.exists()) { throw new IllegalStateException("Unable to determine code source archive from " + root); } // 將構造的archive 對象返回 return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); }
Archive
這里又需要我們先來了解一下Archive相關的概念。
public abstract class Archive { public abstract URL getUrl(); public String getMainClass(); public abstract Collection<Entry> getEntries(); public abstract List<Archive> getNestedArchives(EntryFilter filter); }
Archive是在spring boot里抽象出來的用來統一訪問資源的接口。該接口有兩個實現,分別是ExplodedArchive和JarFileArchive。前者是一個文件目錄,后者是一個jar,都是用來在文件目錄和jar中尋找資源的,這里看到JarLauncher既支持jar啟動,也支持文件系統啟動,實際上我們在解壓后的文件目錄里執行 java org.springframework.boot.loader.JarLauncher 命令也是可以正常啟動的。
圖3
在FatJar中,使用的是后者。Archive都有一個自己的URL,比如
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!
Archive類還有一個getNestedArchives方法,下面還會用到這個方法,這個方法實際返回的是springboot-demo.jar/lib下面的jar的Archive列表。它們的URL是:
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-starter-web-2.2.5.RELEASE.jar!
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-starter-2.2.5.RELEASE.jar!
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-2.2.5.RELEASE.jar!
jar:file:/D:/java/workspace/spring-boot-bootstarp-demo/spring-boot-demo/target/springboot-demo.jar!/BOOT-INF/lib/spring-boot-autoconfigure-2.2.5.RELEASE.jar!/
省略......
launch()執行流程
archive構造完成后就該執行JarLauncher的launch方法了,這個方法定義在了父類的Launcher里:
// Launcher.java protected void launch(String[] args) throws Exception { /* * 利用 java.net.URLStreamHandler 的擴展機制注冊了SpringBoot的自定義的可以解析嵌套jar的協議。 * 因為SpringBoot FatJar除包含傳統Java Jar中的資源外還包含依賴的第三方Jar文件 * 當SpringBoot FatJar被java -jar命令引導時,其內部的Jar文件是無法被JDK的默認實現 * sun.net.www.protocol.jar.Handler當做classpath的,這就是SpringBoot的自定義協議的原因。 */ JarFile.registerUrlProtocolHandler(); // 通過 classpath 來構建一個 ClassLoader ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 1 launch(args, getMainClass(), classLoader); // 2 }
重點關注下createClassLoader(getClassPathArchives()) 構建ClassLoader的邏輯,首先調用getClassPathArchives()方法返回值作為參數,該方法為抽象方法,具體實現在子類ExecutableArchiveLauncher中:
// ExecutableArchiveLauncher.java @Override protected List<Archive> getClassPathArchives() throws Exception { List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive)); postProcessClassPathArchives(archives); return archives; }
該方法會執行Archive接口定義的getNestedArchives方法返回的與指定過濾器匹配的條目的嵌套存檔列表。從上文可以發現,這里的archive其實就是JarFileArchive ,傳入的過濾器是JarLauncher#isNestedArchive方法引用
// JarLauncher.java @Override protected boolean isNestedArchive(Archive.Entry entry) { // entry是文件目錄時,必須是我們自己的業務類所在的目錄 BOOT-INF/classes/ if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } // entry是Jar文件時,需要在依賴的文件目錄 BOOT-INF/lib/下面 return entry.getName().startsWith(BOOT_INF_LIB); }
getClassPathArchives方法通過過濾器將BOOT-INF/classes/和BOOT-INF/lib/下的嵌套存檔作為List<Archive>返回參數傳入createClassLoader方法中。
// Launcher.java protected ClassLoader createClassLoader(List<Archive> archives) throws Exception { List<URL> urls = new ArrayList<>(archives.size()); for (Archive archive : archives) { // 前面說到,archive有一個自己的URL的,獲得archive的URL放到list中 urls.add(archive.getUrl()); } // 調用下面的重載方法 return createClassLoader(urls.toArray(new URL[0])); } // Launcher.java protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); }
createClassLoader()方法目的是為得到的URL們創建一個類加載器 LaunchedURLClassLoader,構造時傳入了當前Launcher的類加載器作為其父加載器,通常是系統類加載器。下面重點看一下LaunchedURLClassLoader的構造過程:
// LaunchedURLClassLoader.java public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); }
LaunchedURLClassLoader是spring boot自己定義的類加載器,繼承了JDK的URLClassLoader并重寫了loadClass方法,也就是說它修改了默認的類加載方式,定義了自己的類加載規則,可以從前面得到的 List<Archive>中加載依賴包的class文件了 。
LaunchedURLClassLoader創建完成后,我們回到Launcher中,下一步就是執行launch的重載方法了。
// Launcher.java launch(args, getMainClass(), classLoader);
在此之前,會調用getMainClass方法并將其返回值作為參數。
getMainClass的實現在Launcher的子類ExecutableArchiveLauncher中:
// ExecutableArchiveLauncher.java @Override protected String getMainClass() throws Exception { // 從 archive 中拿到 Manifest文件 Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { // 就是MANIFEST.MF 文件中定義的Start-Class屬性,也就是我們自己寫的com.example.spring.boot.demo.SpringBootDemo這個類 mainClass = manifest.getMainAttributes().getValue("Start-Class"); } if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); } // 返回mainClass return mainClass; }
得到mainClass后,執行launch的重載方法:
// Launcher.java protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { // 將自定義的LaunchedURLClassLoader設置為當前線程上下文類加載器 Thread.currentThread().setContextClassLoader(classLoader); // 構建一個 MainMethodRunner 實例對象來啟動應用 createMainMethodRunner(mainClass, args, classLoader).run(); } // Launcher.java protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner(mainClass, args); }
MainMethodRunner對象構建完成后,調用它的run方法:
// MainMethodRunner.java public void run() throws Exception { // 使用當前線程上下文類加載器也就是自定義的LaunchedURLClassLoader來加載我們自己寫的com.example.spring.boot.demo.SpringBootDemo這個類 Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName); // 找到SpringBootDemo的main方法 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); // 最后,通過反射的方式調用main方法 mainMethod.invoke(null, new Object[] { this.args }); }
至此,我們自己的main方法開始被調用,所有我們自己的應用程序類文件均可通過/BOOT-INF/classes加載,所有依賴的第三方jar均可通過/BOOT-INF/lib加載,然后就開始了spring boot的啟動流程了。
debug技巧
以上就是spring boot通過java -jar命令啟動的原理了,了解了原理以后我們可不可以通過debug來進一步加深一下理解呢?通常我們在IDEA里啟動時是直接運行main方法,因為依賴的Jar都讓IDEA放到classpath里了,所以spring boot直接啟動就完事了,并不會通過上面的方式來啟動。不過我們可以通過配置IDEA的 run/debug configurations 配置 JAR Application 來實現通過Jar方式啟動。
看完了這篇文章,相信你對簡單分析SpringBoot java-jar命令行啟動原理有了一定的了解,想了解更多相關知識,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。