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

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

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

一、JarLauncher

1、源码解析

1)JarLauncher类结构

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
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 的构造方法被调用。

01
02
03
04
05
06
07
08
09
10
11
12
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 方法源代码如下:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
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 接口的具体定义如下:

01
02
03
04
05
06
07
08
09
10
11
12
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 方法调用的。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 方法。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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抽象类

相关实现源代码如下:

01
02
03
04
05
06
07
08
09
10
11
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 方法实现的。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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 方法。

01
02
03
04
05
06
07
08
09
10
11
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 方法被调用,便真正开始启动应用程序了。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 部分源代码

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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

1
<packaging>war</ packaging>

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<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

01
02
03
04
05
06
07
08
09
10
11
12
<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 并实现其方法

01
02
03
04
05
06
07
08
09
10
11
12
@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 包解压之后,目录结构如下:

1
2
3
4
5
6
7
8
META-INF
MANIFEST.MF
maven
WEB-INF
classes
lib
org
springframework

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

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

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

发表评论

欢迎阅读『SpringBoot打包部署解析:Launcher实现原理|Java、开发语言、框架算法|Nick Tan-梓潼Blog』