首先,先準備一個jar
包,我這裏準備了一個demo-0.0.1-SNAPSHOT.jar
;先來看看jar
包裏面的目錄結構:
├── BOOT-INF
│ ├── classes
│ │ ├── application.properties
│ │ └── com
│ │ └── sf
│ │ └── demo
│ │ └── DemoApplication.class
│ └── lib
│ ├── spring-boot-2.1.3.RELEASE.jar
│ ├── spring-boot-autoconfigure-2.1.3.RELEASE.jar
│ ├── spring-boot-starter-2.1.3.RELEASE.jar
│ ├── 這裏省略掉很多jar包
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.sf
│ └── demo
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── 省略class
├── archive
│ ├── Archive$Entry.class
│ ├── 省略class
├── data
│ ├── RandomAccessData.class
│ ├── 省略class
├── jar
│ ├── AsciiBytes.class
│ ├── 省略class
└── util
└── SystemPropertyUtils.class
這個文件目錄分爲BOOT-INF/classes
、BOOT-INF/lib
、META-INF
、org
:
-
BOOT-INF/classes
:主要存放應用編譯後的class
文件 -
BOOT-INF/lib
:主要存放應用依賴的jar
包文件 -
META-INF
:主要存放maven
和MANIFEST.MF
文件 -
org
:主要存放springboot
相關的class
文件
當你使用命令java -jar demo-0.0.1-SNAPSHOT.jar
時,它會找到META-INF
下的MANIFEST.MF
文件,可以從文件中發現,其內容中的Main-Class
屬性值爲org.springframework.boot.loader.JarLauncher
,並且項目的引導類定義在Start-Class
屬性中,值爲com.sf.demo.DemoApplication
,該屬性是由springboot
引導程序啓動需要的,JarLauncher
就是對應的jar
文件的啓動器.
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.sf.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.0.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
啓動類org.springframework.boot.loader.JarLauncher
並非是項目中引入的類,而是spring-boot-maven-plugin
插件repackage
追加進去的.
探索JarLauncher
的實現原理
當執行java -jar
命令或執行解壓後的org.springframework.boot.loader.JarLauncher
類時,JarLauncher
會將BOOT-INF/classes
下的類文件和BOOT-INF/lib
下依賴的jar
加入到classpath
下,最後調用META-INF
下的MANIFEST.MF
文件的Start-Class
屬性來完成應用程序的啓動,也就是說它是springboot loader
提供了一套標準用於執行springboot
打包出來的JAR
包.
JarLauncher
重點類的介紹:
-
java.util.jar.JarFile
:JDK
工具類,用於讀取JAR
文件的內容 -
org.springframework.boot.loader.jar.JarFile
:繼承於JDK
工具類JarFile
類並擴展了一些嵌套功能 -
java.util.jar.JarEntry
:JDK
工具類,此類用於表示JAR
文件條目 -
org.springframework.boot.loader.jar.JarEntry
:也是繼承於JDK
工具類JarEntry
類 -
org.springframework.boot.loader.archive.Archive
:spring boot loader
抽象出來的統一訪問資源的接口 -
org.springframework.boot.loader.archive.JarFileArchive
:JAR
文件的實現 -
org.springframework.boot.loader.archive.ExplodedArchive
:文件目錄的實現
在項目裏面添加一個依賴配置,就可以看JarLauncher
的源碼:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>provided</scope>
</dependency>
org.springframework.boot.loader.ExecutableArchiveLauncher
public class JarLauncher extends ExecutableArchiveLauncher {
private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
};
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
// Only needed for exploded archives, regular ones already have a defined order
if (archive instanceof ExplodedArchive) {
String location = getClassPathIndexFileLocation(archive);
return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
}
return super.getClassPathIndex(archive);
}
private String getClassPathIndexFileLocation(Archive archive) throws IOException {
Manifest manifest = archive.getManifest();
Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
}
@Override
protected boolean isPostProcessingClassPathArchives() {
return false;
}
@Override
protected boolean isSearchCandidate(Archive.Entry entry) {
return entry.getName().startsWith("BOOT-INF/");
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
//start
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
org.springframework.boot.loader.Launcher
/**
*
* 啓動程序的基類,該啓動程序可以使用一個或多個支持的完全配置的類路徑來啓動應用程序
*
* @author Phillip Webb
* @author Dave Syer
* @since 1.0.0
*/
public abstract class Launcher {
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
/**
* 啓動應用程序,此方法是子類方法{@code public static void main(String[] args)}調用的初始入口點
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
//①註冊一個自定義URL的jar協議
JarFile.registerUrlProtocolHandler();
}
//②創建指定archive的類加載器
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
//③獲取Start-Class屬性對應的com.sf.demo.DemoApplication
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
//④利用反射調用Start-Class,執行main方法
launch(args, launchClass, classLoader);
}
}
①註冊一個自定義URL
的JAR
協議
org.springframework.boot.loader.jar.JarFile#registerUrlProtocolHandler
spring boot loader
擴展了URL
協議,將包名org.springframework.boot.loader
追加到java
系統屬性java.protocol.handler.pkgs
中,該包下存在協議對應的Handler
類,即org.springframework.boot.loader.jar.Handler
其實現協議爲JAR
.
/**
* 註冊一個'java.protocol.handler.pkgs'屬性,讓URLStreamHandler處理jar的URL
*/
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER,
("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
resetCachedUrlHandlers();
}
org.springframework.boot.loader.jar.JarFile#resetCachedUrlHandlers
/**
* 防止已經使用了jar協議,需要重置URLStreamHandlerFactory緩存的處理程序。
*/
private static void resetCachedUrlHandlers() {
try {
URL.setURLStreamHandlerFactory(null);
}
catch (Error ex) {
// Ignore
}
}
②創建指定archive
的類加載器
org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchivesIterator
@Override
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
Archive.EntryFilter searchFilter = this::isSearchCandidate;
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
if (isPostProcessingClassPathArchives()) {
archives = applyClassPathArchivePostProcessing(archives);
}
return archives;
}
org.springframework.boot.loader.Launcher#createClassLoader(java.util.Iterator<org.springframework.boot.loader.archive.Archive>)
/**
* 創建一個指定的archives的類加載器
* @param archives the archives
* @return the classloader
* @throws Exception if the classloader cannot be created
* @since 2.3.0
*/
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(50);
while (archives.hasNext()) {
Archive archive = archives.next();
urls.add(archive.getUrl());
archive.close();
}
return createClassLoader(urls.toArray(new URL[0]));
}
org.springframework.boot.loader.Launcher#createClassLoader(java.util.Iterator<org.springframework.boot.loader.archive.Archive>)
/**
* 創建一個指定的自定義URL的類加載器
* @param urls the URLs
* @return the classloader
* @throws Exception if the classloader cannot be created
*/
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(isExploded(), urls, getClass().getClassLoader());
}
③獲取Start-Class
屬性對應的com.sf.demo.DemoApplication
org.springframework.boot.loader.ExecutableArchiveLauncher#getMainClass
@Override
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
//從配置文件獲取Start-Class對應的com.sf.demo.DemoApplication
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
④利用反射調用Start-Class
,執行main
方法
/**
* 啓動應用程序
* @param args the incoming arguments
* @param launchClass the launch class to run
* @param classLoader the classloader
* @throws Exception if the launch fails
*/
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
//將當前線程的上下文類加載器設置成LaunchedURLClassLoader
Thread.currentThread().setContextClassLoader(classLoader);
//啓動應用程序
createMainMethodRunner(launchClass, args, classLoader).run();
}
/**
* 構造一個MainMethodRunner類,來啓動應用程序
* @param mainClass the main class
* @param args the incoming arguments
* @param classLoader the classloader
* @return the main method runner
*/
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
org.springframework.boot.loader.MainMethodRunner
/**
* 用來調用main方法的工具類
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
//利用反射啓動應用程序
public void run() throws Exception {
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
mainMethod.invoke(null, new Object[] { this.args });
}
}
我們先了解一下類加載機制:
我們知道雙親委派模型的原則,當一個類加載器收到類加載任務,會先交給其父類加載器去完成,因此最終加載任務都會傳遞到頂層的啓動類加載器,只有當父類加載器無法完成加載任務時,纔會嘗試加載任務。
由於demo-0.0.1-SNAPSHOT.jar
中依賴的各個JDK
包,並不在程序自己的classpath
下,它是存放在JDK
包裏的BOOT-INF/lib
目錄下,如果我們採用雙親委派機制的話,根本獲取不到我們JAR
包的依賴,因此我們需要破壞雙親委派模型,使用自定義類加載機制。
在springboot2
中,LaunchedURLClassLoader
自定義類加載器繼承URLClassLoader
,重寫了loadClass
方法;在JDK
裏面,JAR
的資源分隔符是!/
,但是JDK
中只支持一個!/
,這無法滿足spring boot loader
的需求,so,springboot
擴展了JarFile
,從這裏可以看到org.springframework.boot.loader.jar.JarFile#createJarFileFromEntry
,它支持了多個!/
,表示jar文件嵌套JAR
文件、JAR
文件嵌套Directory
.
org.springframework.boot.loader.LaunchedURLClassLoader
public class LaunchedURLClassLoader extends URLClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
........省略代碼
try {
try {
//嘗試根據類名去定義類所在的包,即java.lang.Package,確保jar文件嵌套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.
throw new AssertionError("Package " + name + " has already been defined but it could not be found");
}
}
return super.loadClass(name, resolve);
}
finally {
Handler.setUseFastConnectionExceptions(false);
}
}
}
org.springframework.boot.loader.LaunchedURLClassLoader#definePackageIfNecessary
/**
* 在進行調用findClass方法之前定義一個包,確保嵌套jar與包關聯
* @param className the class name being found
*/
private void definePackageIfNecessary(String className) {
int lastDot = className.lastIndexOf('.');
if (lastDot >= 0) {
String packageName = className.substring(0, lastDot);
if (getPackage(packageName) == null) {
try {
definePackage(className, packageName);
}
catch (IllegalArgumentException ex) {
// Tolerate race condition due to being parallel capable
if (getPackage(packageName) == null) {
// This should never happen as the IllegalArgumentException
// indicates that the package has already been defined and,
// therefore, getPackage(name) should not have returned null.
throw new AssertionError(
"Package " + packageName + " has already been defined but it could not be found");
}
}
}
}
}
總結:
1、springboot
擴展了JDK
的URL
協議;
2、springboot
自定義了類加載器LaunchedURLClassLoader
;
3、Launcher
利用反射調用StartClass#main
方法(org.springframework.boot.loader.MainMethodRunner#run
);
4、springboot1
和springboot2
主要區別是在啓動應用程序時,springboot1
會啓動一個線程去反射調用,springboot2
直接調用;
參考資料:
https://www.yht7.com/news/18153
https://segmentfault.com/a/1190000016192449
https://cloud.tencent.com/developer/article/1469863
https://www.cnblogs.com/xxzhuang/p/11194559.html
http://www.10qianwan.com/articledetail/577937.html
https://blog.csdn.net/shenchaohao12321/article/details/103543446
https://fangjian0423.github.io/2017/05/31/springboot-executable-jar/
如果文章存在問題、不妥的地方或者有疑惑麻煩給我留言,大家一起學習,感謝您~
歡迎關注我的微信公衆號,裏面有很多幹貨,各種面試題