(轉)springboot應用啟動原理(二) 擴展URLClassLoader實現嵌套jar加載


轉:https://segmentfault.com/a/1190000013532009

在上篇文章《springboot應用啟動原理(一) 將啟動腳本嵌入jar》中介紹了springboot如何將啟動腳本與Runnable Jar整合為Executable Jar的原理,使得生成的jar/war文件可以直接啟動
本篇將介紹springboot如何擴展URLClassLoader實現嵌套jar的類(資源)加載,以啟動我們的應用。

本篇示例使用 java8 + grdle4.2 + springboot2.0.0.release 環境

首先,從一個簡單的示例開始

build.gradle

 
group 'com.manerfan.spring' version '1.0.0' apply plugin: 'java' apply plugin: 'java-library' sourceCompatibility = 1.8 buildscript { ext { springBootVersion = '2.0.0.RELEASE' } repositories { mavenLocal() maven { name 'aliyun maven central' url 'http://maven.aliyun.com/nexus/content/groups/public' } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' bootJar { launchScript() } repositories { mavenLocal() maven { name 'aliyun maven central' url 'http://maven.aliyun.com/nexus/content/groups/public' } } dependencies { api 'org.springframework.boot:spring-boot-starter-web' }

WebApp.java

 
@SpringBootApplication @RestController public class WebApp { public static void main(String[] args) { SpringApplication.run(WebApp.class, args); } @RequestMapping("/") @GetMapping public String hello() { return "Hello You!"; } }

執行gradle build構建jar包,里面包含應用程序第三方依賴以及springboot啟動程序,其目錄結構如下

 
spring-boot-theory-1.0.0.jar ├── META-INF │   └── MANIFEST.MF ├── BOOT-INF │   ├── classes │   │   └── 應用程序 │   └── lib │   └── 第三方依賴jar └── org └── springframework └── boot └── loader └── springboot啟動程序

查看MANIFEST.MF的內容(MANIFEST.MF文件的作用請自行GOOGLE)

 
Manifest-Version: 1.0 Start-Class: com.manerfan.springboot.theory.WebApp Main-Class: org.springframework.boot.loader.JarLauncher

可以看到,jar的啟動類為org.springframework.boot.loader.JarLauncher,而並不是我們的com.manerfan.springboot.theory.WebApp,應用程序入口類被標記為了Start-Class

jar啟動並不是通過應用程序入口類,而是通過JarLauncher代理啟動。其實SpringBoot擁有3中不同的Launcher:JarLauncherWarLauncherPropertiesLauncher

 

springboot使用Launcher代理啟動,其最重要的一點便是可以自定義ClassLoader,以實現對jar文件內(jar in jar)或其他路徑下jar、class或資源文件的加載
關於ClassLoader的更多介紹可參考《深入理解JVM之ClassLoader》

Archive

  • 歸檔文件
  • 通常為tar/zip等格式壓縮包
  • jar為zip格式歸檔文件

SpringBoot抽象了Archive的概念,一個Archive可以是jar(JarFileArchive),可以是一個文件目錄(ExplodedArchive),可以抽象為統一訪問資源的邏輯層。

上例中,spring-boot-theory-1.0.0.jar既為一個JarFileArchive,spring-boot-theory-1.0.0.jar!/BOOT-INF/lib下的每一個jar包也是一個JarFileArchive
將spring-boot-theory-1.0.0.jar解壓到目錄spring-boot-theory-1.0.0,則目錄spring-boot-theory-1.0.0為一個ExplodedArchive

 
public interface Archive extends Iterable<Archive.Entry> { // 獲取該歸檔的url URL getUrl() throws MalformedURLException; // 獲取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF Manifest getManifest() throws IOException; // 獲取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar List<Archive> getNestedArchives(EntryFilter filter) throws IOException; }

JarLancher

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a /BOOT-INF/lib directory and that application classes are included inside a /BOOT-INF/classes directory.

按照定義,JarLauncher可以加載內部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的應用class

其實JarLauncher實現很簡單

 
public class JarLauncher extends ExecutableArchiveLauncher { public JarLauncher() {} public static void main(String[] args) throws Exception { new JarLauncher().launch(args); } }

其主入口新建了JarLauncher並調用父類Launcher中的launch方法啟動程序
再創建JarLauncher時,父類ExecutableArchiveLauncher找到自己所在的jar,並創建archive

 
public abstract class ExecutableArchiveLauncher extends Launcher { private final Archive archive; public ExecutableArchiveLauncher() { try { // 找到自己所在的jar,並創建Archive this.archive = createArchive(); } catch (Exception ex) { throw new IllegalStateException(ex); } } } public abstract class Launcher { protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource == null ? null : codeSource.getLocation().toURI()); String path = (location == null ? null : location.getSchemeSpecificPart()); 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); } return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); } }

在Launcher的launch方法中,通過以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目錄所對應的archive,通過這些archives的url生成LaunchedURLClassLoader,並將其設置為線程上下文類加載器,啟動應用

 
public abstract class Launcher { protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); // 生成自定義ClassLoader ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 啟動應用 launch(args, getMainClass(), classLoader); } protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { // 將自定義ClassLoader設置為當前線程上下文類加載器 Thread.currentThread().setContextClassLoader(classLoader); // 啟動應用 createMainMethodRunner(mainClass, args, classLoader).run(); } } public abstract class ExecutableArchiveLauncher extends Launcher { protected List<Archive> getClassPathArchives() throws Exception { // 獲取/BOOT-INF/lib下所有jar及/BOOT-INF/classes目錄對應的archive List<Archive> archives = new ArrayList<>( this.archive.getNestedArchives(this::isNestedArchive)); postProcessClassPathArchives(archives); return archives; } } public class MainMethodRunner { // Start-Class in MANIFEST.MF private final String mainClassName; private final String[] args; public MainMethodRunner(String mainClass, String[] args) { this.mainClassName = mainClass; this.args = (args == null ? null : args.clone()); } public void run() throws Exception { // 加載應用程序主入口類 Class<?> mainClass = Thread.currentThread().getContextClassLoader() .loadClass(this.mainClassName); // 找到main方法 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); // 調用main方法,並啟動 mainMethod.invoke(null, new Object[] { this.args }); } }

至此,才執行我們應用程序主入口類的main方法,所有應用程序類文件均可通過/BOOT-INF/classes加載,所有依賴的第三方jar均可通過/BOOT-INF/lib加載

LaunchedURLClassLoader

在分析LaunchedURLClassLoader前,首先了解一下URLStreamHandler

URLStreamHandler

java中定義了URL的概念,並實現多種URL協議(見URLhttp file ftp jar 等,結合對應的URLConnection可以靈活地獲取各種協議下的資源

 
public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException

對於jar,每個jar都會對應一個url,如
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/

jar中的資源,也會對應一個url,並以'!/'分割,如
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

對於原始的JarFile URL,只支持一個'!/',SpringBoot擴展了此協議,使其支持多個'!/',以實現jar in jar的資源,如
jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

自定義URL的類格式為[pkgs].[protocol].Handler,在運行Launcher的launch方法時調用了JarFile.registerUrlProtocolHandler()以注冊自定義的 Handler

 
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs"; private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader"; public static void registerUrlProtocolHandler() { String handlers = System.getProperty(PROTOCOL_HANDLER, ""); System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); resetCachedUrlHandlers(); }

在處理如下URL時,會循環處理'!/'分隔符,從最上層出發,先構造spring-boot-theory.jar的JarFile,再構造spring-aop-5.0.4.RELEASE.jar的JarFile,最后構造指向SpringProxy.class的
JarURLConnection ,通過JarURLConnection的getInputStream方法獲取SpringProxy.class內容

jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

從一個URL,到讀取其中的內容,整個過程為

  • 注冊一個Handler處理‘jar:’這種協議
  • 擴展JarFile、JarURLConnection,處理jar in jar的情況
  • 循環處理,找到內層資源
  • 通過getInputStream獲取資源內容

URLClassLoader可以通過原始的jar協議,加載jar中從class文件
LaunchedURLClassLoader 通過擴展的jar協議,以實現jar in jar這種情況下的class文件加載

WarLauncher

構建war包很簡單

  1. build.gradle中引入插件 apply plugin: 'war'
  2. build.gradle中將內嵌容器相關依賴設為providedprovidedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
  3. 修改WebApp內容,重寫SpringBootServletInitializer的configure方法
 
@SpringBootApplication @RestController public class WebApp extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(WebApp.class, args); } @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(WebApp.class); } @RequestMapping("/") @GetMapping public String hello() { return "Hello You!"; } }

構建出的war包,其目錄機構為

 
spring-boot-theory-1.0.0.war ├── META-INF │ └── MANIFEST.MF ├── WEB-INF │ ├── classes │ │ └── 應用程序 │ └── lib │ └── 第三方依賴jar │ └── lib-provided │ └── 與內嵌容器相關的第三方依賴jar └── org └── springframework └── boot └── loader └── springboot啟動程序

MANIFEST.MF內容為

 
Manifest-Version: 1.0 Start-Class: com.manerfan.springboot.theory.WebApp Main-Class: org.springframework.boot.loader.WarLauncher

此時,啟動類變為了org.springframework.boot.loader.WarLauncher,查看WarLauncher實現,其實與JarLauncher並無太大差別

 
public class WarLauncher extends ExecutableArchiveLauncher { private static final String WEB_INF = "WEB-INF/"; private static final String WEB_INF_CLASSES = WEB_INF + "classes/"; private static final String WEB_INF_LIB = WEB_INF + "lib/"; private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/"; public WarLauncher() { } @Override public boolean isNestedArchive(Archive.Entry entry) { if (entry.isDirectory()) { return entry.getName().equals(WEB_INF_CLASSES); } else { return entry.getName().startsWith(WEB_INF_LIB) || entry.getName().startsWith(WEB_INF_LIB_PROVIDED); } } public static void main(String[] args) throws Exception { new WarLauncher().launch(args); } }

差別僅在於,JarLauncher在構建LauncherURLClassLoader時,會搜索BOOT-INF/classes目錄及BOOT-INF/lib目錄下jar,WarLauncher在構建LauncherURLClassLoader時,則會搜索WEB-INFO/classes目錄及WEB-INFO/lib和WEB-INFO/lib-provided兩個目錄下的jar

如此依賴,構建出的war便支持兩種啟動方式

  • 直接運行./spring-boot-theory-1.0.0.war start
  • 部署到Tomcat容器下

PropertiesLauncher

PropretiesLauncher 的實現與 JarLauncher WarLauncher 的實現極為相似,通過PropretiesLauncher可以實現更為輕量的thin jar,其實現方式可自行查閱源碼

總結

  • SpringBoot通過擴展JarFile、JarURLConnection及URLStreamHandler,實現了jar in jar中資源的加載
  • SpringBoot通過擴展URLClassLoader--LauncherURLClassLoader,實現了jar in jar中class文件的加載
  • JarLauncher通過加載BOOT-INF/classes目錄及BOOT-INF/lib目錄下jar文件,實現了fat jar的啟動
  • WarLauncher通過加載WEB-INF/classes目錄及WEB-INF/lib和WEB-INF/lib-provided目錄下的jar文件,實現了war文件的直接啟動及web容器中的啟動


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM