眾所周知拓售,Springboot的FAT JAR機制大大的簡化了應(yīng)用的打包和啟動剑令,并且統(tǒng)一了不同stack(command, web, batch)的打包和啟動方法侦厚,使得一個應(yīng)該的開發(fā)和部署都變得簡單了邑遏,本文想在這里解析一下FAT JAR的方式下穆端,Springboot的類加載機制忠售。
Springboot FAT JAR的結(jié)構(gòu)
我們知道Springboot提供了spring-boot-maven-plugin這個maven plugin在build時生成FAT jar传惠,如果我們build了一個springboot的應(yīng)用,去target folder下可以看見兩個名字非常類似的文件: abc-0.0.1-SNAPSHOT.jar 和abc-0.0.1-SNAPSHOT.jar.original,這兩個文件的大小相差非常明顯稻扬,我們解壓開.original文件可以發(fā)現(xiàn)如下結(jié)構(gòu)卦方,這個就是我們應(yīng)用中所有本地文件(代碼和資源文件),而不包含第三方的依賴等等泰佳。
如果我們解壓打開abc-0.0.1-SNAPSHOT.jar (FAT JAR)盼砍,目錄結(jié)構(gòu)如下, 本地的文件都在BOOT-INF/classes下,但是還多了:
BOOT-INF/lib - 存放所有dependences的JAR
org/Springframework - 存放springboot相關(guān)的class
目錄結(jié)構(gòu)有不少的變化逝她,可以認為在執(zhí)行spring-boot-maven-plugin后浇坐,abc-0.0.1-SNAPSHOT.jar.original被重新打包成了abc-0.0.1-SNAPSHOT.jar這個FAT JAR。
在BOOT-INF/lib下有一個dependency需要花功夫研究: spring-boot-loader黔宛,這個JAR保證了為什么通過java -jar命令能夠執(zhí)行FAT JAR從而啟動Springboot應(yīng)用近刘。
spring-boot-loader 做了什么
既然把所有的依賴,class和資源文件都包含在一個JAR內(nèi),那就必須要解決啟動時如何去load這些claas和資源文件觉渴,為了一探究竟介劫,我們需要把spring-boot-loader的source code拉下來,一個簡單的方法就是把spring-boot-loader作為依賴加到應(yīng)用的pom.xml中案淋,當然scope可以設(shè)置成provided座韵,因為FAT JAR中一定會包含這個依賴。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<scope>Provided</scope>
</dependency>
這樣我們就能很方便的拿到spring-boot-loader的source code踢京。通過FAT JAR中的MANIFEST.MF誉碴,我們能快速找到真正的Springboot bootstrap方法應(yīng)該是org.springframework.boot.loader.JarLauncher#main,真正的code在基類Launcher#launch中漱挚。
public abstract class Launcher {
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
可以發(fā)現(xiàn)這里創(chuàng)建了一個新的classloader - LaunchedURLClassLoader翔烁,具體是渺氧,
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
}
并把classload 加入thread 的context中旨涝。
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(launchClass, args, classLoader).run();
}
為什么需要LaunchedURLClassLoader呢?設(shè)想一下侣背,現(xiàn)在FAT JAR中依賴的各個jar文件其實并不在運行時應(yīng)用的classpath下白华,也就是根據(jù)類加載的雙親委派機制,這些依賴沒辦法被默認的任何一個classloader加載贩耐,Springboot為了解決這個問題弧腥,自定義了類加載機制。
P.S 不同的內(nèi)置classloader的scope 如下:
Bootstrap ClassLoader(加載JDK的/lib目錄下的類)
Extension ClassLoader(加載JDK的/lib/ext目錄下的類)
Application ClassLoader(程序自己classpath下的類)
LaunchedURLClassLoader做了什么
LaunchedURLClassLoader繼承了java.net.URLClassLoader潮太,自己實現(xiàn)了loadClass方法管搪。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
try {
Class<?> result = loadClassInLaunchedClassLoader(name);
if (resolve) {
resolveClass(result);
}
return result;
}
catch (ClassNotFoundException ex) {
}
}
if (this.exploded) {
return super.loadClass(name, resolve);
}
Handler.setUseFastConnectionExceptions(true);
try {
try {
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);
}
}
definePackageIfNecessary 確保了Jar in Jar里的class manifest能夠和package關(guān)聯(lián)起來。
private void definePackage(String className, String packageName) {
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
String packageEntryName = packageName.replace('.', '/') + "/";
String classEntryName = className.replace('.', '/') + ".class";
for (URL url : getURLs()) {
try {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
JarFile jarFile = ((JarURLConnection) connection).getJarFile();
if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
&& jarFile.getManifest() != null) {
definePackage(packageName, jarFile.getManifest(), url);
return null;
}
}
}
catch (IOException ex) {
// Ignore
}
}
return null;
}, AccessController.getContext());
}
catch (java.security.PrivilegedActionException ex) {
// Ignore
}
}
可以看到最終load class還是調(diào)了super.loadClass铡买,也就是java.lang.ClassLoader#loadClass更鲁,這其實又回到了雙親委派機制,最后讓Application Classloader來load奇钞。
LaunchedURLClassLoader的作用是在FAT JAR(Jar in Jar)這樣的目錄結(jié)構(gòu)中澡为,能夠找到要load的class(依賴中的類或者應(yīng)用自己的類),并且load他們景埃。
我們看看這個class load是怎么load 我們在springboot應(yīng)用中定義的main class媒至,也就是應(yīng)用的入口程序的。
在org.springframework.boot.loader.MainMethodRunner中谷徙,通過LaunchedURLClassLoader load并且通過反射調(diào)用了main 方法拒啰。
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 });
}
Java -Jar 和IDE里啟動Sprintboot 有什么區(qū)別
Java -Jar是以FAT JAR的方式用LaunchedURLClassLoader來load class。而在IDE中則是直接以ApplicationClassLoader來load的完慧。這種差別會導致調(diào)用classloader.getResourceAsStream()得到不一樣的結(jié)果谋旦,這是因為FAT JAR啟動時,LaunchedURLClassLoader的load的urls并沒有FAT JAR本身,如abc-0.0.1-SNAPSHOT.jar, 但是應(yīng)用中的src/main/resources/META-INF/resources目錄被打包到了FAT JAR里蛤织,也就是abc-0.0.1-SNAPSHOT.jar!/META-INF/resources赴叹,這樣這些resource也就不會被訪問到了
這也就是為什么有時候在IDE里能讀到的resource在Run FAT JAR的情況下讀不到了,Springboot也給了多種方式來正確的load resource: https://www.baeldung.com/spring-load-resource-as-string