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

在上篇文章《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:JarLauncherWarLauncherPropertiesLaunchertomcat

launcher

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

Archive

  • 歸檔文件
  • 一般爲tar/zip等格式壓縮包
  • jar爲zip格式歸檔文件

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;
}

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容器中的啓動

訂閱號

相關文章
相關標籤/搜索