SpringBoot打包部署解析:Launcher实现原理

jar包 Main-Class 指定入口程序为 Spring Boot 提供的 Launcher(JarLauncher),并不是我们在 Spring Boot 项目中所写的入口类。那么,Launcher 类又是如何实现项目的启动呢?

Launcher 类的具体实现类有 3 个:JarL auncher、Warl _auncher 和 PropertiesLauncher。

一、JarLauncher

1、源码解析

1)JarLauncher类结构

public class JarLauncher extends ExecutableArchiveLauncher {

    static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    static final String BOOT_INF_LIB = "B0OOT-INF/lib/";
    
    //省略构造方法
    @Override
    protected boolean isNestedArchive(Archive. Entry entry) {
        if (entry.isDirectory())
            return entry.getName().equals(BOOT_INF_CLASSES);
            
        return entry.getName().startsWith(BOOT_INF_LIB);
    }
    
    public static void main(String[] args) throws Exception {
        new JarLauncher(). launch(args);
    }
}

JarLauncher 类结构非常简单,它继承了抽象类 ExecutableArchiveLauncher,而抽象类又继承了抽象类 Launcher。

JarLauncher 中定义了两个常量:BOOT_ INF_CLASSES和BOOT_INF_LIB,它们分别定义了业务代码存放在 jar包中的位置( BOOT-INF/classes/)和依赖 jar包所在的位置(BOOT-INF/lib/) 。

2) 父类 ExecutableArchiveLauncher

JarLauncher 中提供了一个 main 方法,即入口程序的功能,在该方法中首先创建了 JarLauncher 对象,然后调用其 launch 方法。当创建子类对象时,会先调用父类的构造方法。因此,父类 ExecutableArchiveLauncher 的构造方法被调用。

public abstract class ExecutableArchiveLauncher extends Launcher {

    private final Archive archive;
    
    public ExecutableArchiveLauncher() {
        try {
            this.archive = createArchive();
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}

在 ExecutableArchiveLauncher 的构造方法中仅实现了父类 Launcher 的 createArchive 方法的调用和异常的抛出。

Launcher 类中 createArchive 方法源代码如下:

protected final Archive createArchive() throws Exception {

    //通过获得当前 Class 类的信息,查找到当前归档文件的路径
    ProtectionDomain protectionDomain = getClass().getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URI location = (codeSource != nu1l) ? codeSource.getLocation().toURI() : null;
    String path = (location != null) ? location.getSchemeSpecificPart() : nul1;
    
    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);
    }
    
    //如果是目录,则创建 ExplodedArchive, 否则创建 JarFileArchive
    return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}

在 createArchive 方法中,根据当前类信息获得当前归档文件的路径(即打包后生成的可执行的spring-learn-0.0.1-SNAPSHOT.jar) ,并检查文件路径是否存在。如果存在且是文件夹,则创建 ExplodedArchive 的对象, 否则创建 JarFileArchive 的对象。

3) Archive接口

关于 Archive,它在 Spring Boot 中是一个抽象的概念, Archive 可以是一 个jar (JarFileArchive) ,也可以是一个文件目录(ExplodedArchive),可以理解为它是一个抽象出来的统一访问资源的层。

Archive 接口的具体定义如下:

public interface Archive extends Iterable<Archive.Entry> {

    //获取该归档的 url
    URL getUrl() throws MalformedURL Exception;
    
    // 获取 jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    
    //获取 jar!/B0OT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/Lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
    
}

通过 Archive 接口中定义的方法可以看出,Archive 不仅提供了获得归档自身 URL 的方法,也提供了获得该归档内部 jar 文件列表的方法,而 jar 内部的 jar 文件依旧会被 Spring Boot认为是一个 Archive。

通常,jar 里的资源分隔符是!/,在 JDK 提供的 JarFile URL 只支持一层“!””,而Spring Boot扩展了该协议,可支持多层”!/”。 因此,在 Spring Boot 中也就可以表示 jar in jar、jar indirectory、fat jar 类型的资源了。

4)JarLauncher launch方法

回到 JarLauncher 的入口程序,当创建 JarLauncher 对象,获得了当前归档文件的Archive,下一步便是调用 launch 方法,该方法由 Launcher 类实现。Launcher 中的这个launch 方法就是启动应用程序的入口,而该方法的定义是为了让子类的静态 main 方法调用的。

protected void launch(String[] args) throws Exception {

    //注册一个“java.protocol.handler.pkgs”属性,以便定位 URLStreamHandler 来处理 jar
    JarFile.registerUrlProtocolHandler();
    
    //获取 Archive, 并通过 Archive 的 URL 获得 CLassL oader(这里为 aunchedURLClassLoader)
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    //启动应用程序(创建 MainMethodRunner 类并调用其 run 方法)
    launch(args, this.getMainClass(), classLoader);
}

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        this.createMainMethodRunner(mainClass, args, classLoader).run();
    }

    protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
        return new MainMethodRunner(mainClass, args);
    }
}

5)JarFile类

在 launch 方 法 中 都 具 体 做 了 什 么 操 作 , 首 先 调 用 了 JarFile 的registerUrIlProtocol-Handler 方法。

public class JarFile extends java.util.jar.JarFile {

    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();
    }
        
    private static void resetCachedUrlHandlers() {
        try {
            URL.setURLStreamHandlerFactory(null);
        } catch (Error ex) {
            //忽咯异常处理
        }
    }
}

JarFile 的registerUrlProtocolHandler 方法利用了 java.net.URLStreamHandler 扩展机制。其实现由 URL #getURLStreamHandler(String) 提供,该方法返回一个 URLStreamHandler类的实现类。针对不同的协议,通过实现 URL StreamHandler 来进行扩展。JDK 默认支持了文件(file) 、HTTP、JAR 等协议。

关于实现 URL StreamHandler 类来扩展协议,JVM 有固定的要求:

第一、子类的类名必须是 Handler,最后一级包名必须是协议的名称。 比如,自定义了 Http 的协议实现 , 则类名必然为 xx.http.Handler, 而 JDK 对 http 实现为:sun.net.protocol.http.Handler

第 二、JVM启动时 , 通常需要配置 Java 系统属性 ava.protocol.handler.pkgs , 追加 URLStreamHandler 实现类的 package。 如果有多个实现类(package) ,则用”l 隔开。

JarFile#registerUrlProtocolHandler(String) 方法就是将 org.springframework.boot.loader追加到 Java 系统属性 ava.protocol.handler.pkgs 中。

执行完JarFile.registerUrlProtocolHandler() 之后,执行 createClassLoader 方法创建ClassLoader。

该方法的参数是通过ExecutableArchiveLauncher实现getClassPathArchives方法获得的。

6) ExecutableArchiveLauncher抽象类

相关实现源代码如下:

public abstract class ExecutableArchiveLauncher extends Launcher {

    private final Archive archive;
    
    @Override
    protected List<Archive> getClassPathArchives() throws Exception {
        List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
        postProcessClassPathArchives(archives);
        return archives;
    }
}

7)Launcher抽象类

在 getClassPathArchives 方法中通过调用当前 archive 的 getNestedArchives 方法, 找到/BOOT-INF/lib 下 jar 及/BOOT-INF/classes 目录所对应的 archive,通过这些 archive 的 URL生成 LaunchedURL.ClassLoader.创建 L aunchedURLClassLoader 是由 Launcher 中重载的 createClassLoader 方法实现的。

public abstract class Launcher {

    protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
        List<URL> urls = new ArrayList<>(archives.size());
        
        for (Archive archive : archives) {
            urls.add(archive.getUr1());
        }
        
        return createClassLoader(urls.toArray(new URL[0]));
    }
        
    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
    }
}

Launcher#launch 方法的最后一步,是将 ClassLoader( LaunchedURLClassLoader)设置为线程上下文类加载器,并创建 MainMethodRunner 对象, 调用其 run 方法。

public abstract class Launcher {

    protected void launch(String[] args, String mainClass, ClassLoader classLoader ) throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        createMainMethodRunner(mainClass, args, classLoader).run();
    }
        
    protected MainMethodRunner createMainMethodRunner(String mainClass, Strin3[] args, ClassLoader classLoader) {
        return new MainMethodRunner(mainClass, args);
    }
}

8) MainMethodRunner类

当 MainMethodRunner 的 run 方法被调用,便真正开始启动应用程序了。

public class MainMethodRunner {

    private final String mainClassName;
    private final String[] args;
    
    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args != null) ? args.clone() : null;
    }
    public void run() throws Exception {
        Class<?> mainClass = Thread. currentThread().getContextClassLoader().loadClass(this.mainClassName);
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        mainMethod.invoke(null, new object[] { this.args });
    }
}

上述代码中属性 mainClass 参数便是在 Manifest.MF 文件中我们自定义的 Spring Boot 的入口类,即 Start-class 属性值。在 MainMethodRunner 的 run 方法中,通过反射获得入口类的 main 方法并调用。

至此,Spring Boot 入口类的 main 方法正式执行,所有应用程序类文件均可通过/BOOT-INF/classes 加载,所有依赖的第三方 jar 均可通过/BOOT-INF/lib 加载。

二、WarLauncher

WarLauncher 与 Jarl auncher 都继承自抽象类 ExecutableArchiveL auncher,它们的实现方式和流程基本相同,差异很小。主要的区别是 war 包中的目录文件和 jar 包路径不同。

1、WarLauncher 部分源代码

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/";
    
    @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 在构建 L auncherURLClassLoader 时搜索 BOOT-INF/classes 目录及BOOT-INF/lib目录下的 jar。 而通过上述代码可以看出,WarLauncher 在构建LauncherURLClass-Loader 时搜索的是 WEB-INFO/classes 目录及 WEB-INFO/lib 和WEB-INFO/lib-provided 目录下的 jar。

2、war 的目录结构

对 jar 打包形式的 Spring Boot 项目进行修改,变成可打包成 war 的项目。查看打包成的 war 的目录结构。

1)第一步,修改 pom.xmI 中的 packaging 为 war

<packaging>war</ packaging>

2)第二步,在 spring-boot-starter-web 依赖中排除 tomcat,并新增 servlet-api 依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
    </dependency>
</dependencies>

3) 第三步,在 build 配置中将插件替换为maven-war-plugin

<build>
    <plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</ artifactId>
        <version>2.6</version>
        <configuration>
            <failOnMissingWebXml>false</failOnMissingWebXml>
        </configuration>
        </plugin>
    </plugins>
</build>

4) 第四步,让 Spring Boot 入口类继承SpringBootServletlnitializer 并实现其方法

@SpringBootApplication
public class SpringBootApp extends SpringBootServletInitializer {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootApp.class, args);
    }
    
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(SpringBootApp.class);
    }
}

5) 执行,maven clean package 即可得到对应的 war 包

同样,这里会生成两个 war 包文件:一个后缀为.war 的可独立部署的文件,一个 war.original 文件,具体命名形式参考 jar 包 。

对 war 包解压之后,目录结构如下:

META-INF
MANIFEST.MF
maven
WEB-INF
classes
lib
org
springframework

6)最后,war 包文件既能被 WarLauncher 启动,又能兼容 Servlet 容器。

其实,jar 包和 war并无本质区别,因此,如果无特殊情况,尽量采用 jar 包的形式来进行打包部署。

SpringBoot打包部署解析:Launcher实现原理插图

发表评论