在上篇文章《springboot應用啓動原理(一) 將啓動腳本嵌入jar》中介紹了springboot如何將啓動腳本與Runnable Jar整合爲Executable Jar的原理,使得生成的jar/war文件能夠直接啓動
本篇將介紹springboot如何擴展URLClassLoader實現嵌套jar的類(資源)加載,以啓動咱們的應用。java
本篇示例使用 java8 + grdle4.2 + springboot2.0.0.release 環境
首先,從一個簡單的示例開始git
build.gradlegithub
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.javaweb
@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
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)segmentfault
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-Classapi
jar啓動並非經過應用程序入口類,而是經過JarLauncher代理啓動。其實SpringBoot擁有3中不一樣的Launcher:JarLauncher 、WarLauncher 、PropertiesLaunchertomcat
springboot使用Launcher代理啓動,其最重要的一點即是能夠自定義ClassLoader,以實現對jar文件內(jar in jar)或其餘路徑下jar、class或資源文件的加載
關於ClassLoader的更多介紹可參考《深刻理解JVM之ClassLoader》springboot
SpringBoot抽象了Archive的概念,一個Archive能夠是jar(JarFileArchive),能夠是一個文件目錄(ExplodedArchive),能夠抽象爲統一訪問資源的邏輯層。oracle
上例中,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; }
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前,首先了解一下URLStreamHandler
java中定義了URL的概念,並實現多種URL協議(見URL) http 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,到讀取其中的內容,整個過程爲
URLClassLoader能夠經過原始的jar協議,加載jar中從class文件
LaunchedURLClassLoader 經過擴展的jar協議,以實現jar in jar這種狀況下的class文件加載
構建war包很簡單
apply plugin: 'war'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
@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
PropretiesLauncher 的實現與 JarLauncher WarLauncher 的實現極爲類似,經過PropretiesLauncher能夠實現更爲輕量的thin jar,其實現方式可自行查閱源碼