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

2022-10-28 16:26:26 浏览数 (1)

Launcher实现原理

在上节内容中,我们得知 jar 包 Main-Class 指定入口程序为 Spring Boot 提供的 L auncher(JarL auncher),并不是我们在 Spring Boot 项目中所写的入口类。那么,Launcher 类又是如何实现项目的启动呢?本节带大家了解其相关原理。

Launcher 类的具体实现类有 3 个: JarL auncher、Warl _auncher 和 PropertiesLauncher,我们这里主要讲解 JarLauncher 和 WarLauncher。首先,以 JarL auncher 为例来解析说明Spring Boot 基于 Launcher 来实现的启动过呈。

JarLauncher

在了解 JarL .auncher 的实现原理之前,先来看看 JarL auncher 的源码。

代码语言:javascript复制
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(B0OT_ _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/ib/) 。

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

代码语言:javascript复制
public abstract class ExecutableArchiveL auncher extends L auncher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
} catch (Exception ex) {
throw new IllegalStateException(ex);}
}
}

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

代码语言:javascript复制
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() : n
ul1;
if (path == null) {
throw new IllegalStateException("Unable to determine code source ar
chive");
//获得路径之后,创建对应的文件,并检查是否存在
File root = new File(path);
if (!root . exists()) {
throw new IllegalStateException("Unable to determine code source ar
chive from”  root);
//如果是目录,则创建 ExplodedArchive, 否则创建 JarF ileArchive
return (root. isDirectory() ? new ExplodedArchive(root) : new JarFileArc
hive(root));
}

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

关于 Archive,它在 Spring Boot 中是一个抽象的概念, Archive 可以是一 个jar (JarFileArchive) ,也可以是一个文件目录(ExplodedArchive) ,上面的代码已经进行了很好地证明。你可以理解为它是一个抽象出来的统一 -访问资源的层。Archive 接口的具体定义如下。

代码语言:javascript复制
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 类型的资源了。

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

代码语言:javascript复制
protected void launch(String[] args) throws Exception {
//注册一个“java. protocol . handler. pkgs”属性,以便定位 URLStreamHandler 来处理 jar
URL
JarFile. registerUrlProtocolHandler();
//获取 Archive, 并通过 Archive 的 URL 获得 CLassL oader(这里为 aunchedURLClassLo
ader)
ClassLoader classLoader = createClassLoader(getClassPathArchives());
//启动应用程序(创建 MainMethodRunner 类并调用其 run 方法)
launch(args,getMainClass(), classLoader);
}

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

代码语言:javascript复制
public class JarFile extends java.util. jar.JarFile {
private static final String PROTOCOL HANDLER = "java. protocol . handler . pkg
s";
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_ PA
CKAGE
: handlers   "|"   HANDLERS_ PACKA
GE));
resetCachedUrlHandlers();
private static void resetCachedUrlHandlers() {
try {URL. setURLStreamHandlerF actory(null);
} catch (Error ex) {
//忽咯异常处理
}
}}

JarFile 的 registerUrlProtocolHandler 方法利用了 ava.net.URLStreamHandler 扩展机制

其实现由 URL #getURL StreamHandler(String) 提供,该方法返回一个 URLStreamHandler类的实现类。针对不同的协议,通过实现 URL StreamHandler 来进行扩展。JDK 默认支持了文件(ile) 、HTTP、JAR 等协议。

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

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

代码语言:javascript复制
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() 之后,执行 createClassL oader 方法创建ClassLoader。

该方法的参数是通过ExecutableArchiveL auncher实现getClassPathArchives方法获得的。相关实现源代码如下。

代码语言:javascript复制
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;
}
}

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

代码语言:javascript复制
public abstract class Launcher {protected ClassLoader createClassLoader(List<Archive> archives) throws Ex
ception {
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 方法。

代码语言:javascript复制
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 classLoade
r) {
return new MainMethodRunner(mainClass, args);
}
}

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

MainMethodRunner 类的源码如下。

代码语言:javascript复制
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/classe 加载,所有依赖的第三方 jar 均可通过/BOOT-INF/lib 加载。

WarL auncher

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

代码语言:javascript复制
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- provide
d/";
@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 {
nev
WarLauncher(). launch(args);
}
}

JarL auncher 在 构 建 L auncherURLClassLoader 时 搜 索 BOOT-INF/classes 目 录 及BOOT-INF/lib 目 录 下 的 jar 。而 通 过 上 述 代 码 可 以 看 出 , WarL auncher 在 构 建LauncherURLClass-Loader 时 搜 索 的 是 WEB-INFO/classes 目 录 及 WEB-INFO/ib 和WEB-INFO/ib-provided 目录下的 jar。

下面,我们通过对 jar 打包形式的 Spring Boot 项目进行修改,变成可打包成 war 的项目。

然后,再看一下打包成的 war 的目 录结构。第一步,修改 pom.xmI 中的 packaging 为 war。

代码语言:javascript复制
<packaging>war</ packaging>

第二步,在 spring-boot-starter-web 依赖中排除 tomcat,并新增 servlet-api 依赖,这里采用的是 Servlet 2.5 版本。

代码语言:javascript复制
<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>

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

代码语言:javascript复制
<build>
<plugins>
<plugin>
<groupId>org . apache . maven. plugins</groupId>
<artifactId>maven-war-plugin</ artifactId>
<version>2.6</version>
<configuration>
<fai. lOnMiss ingWebXml>false</ failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>

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

代码语言:javascript复制
@SpringBootApplicationpublic class SpringBootApp extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication. run(SpringBootApp.class, args);
@Override
protected SpringApplicationBuilder configure(SpringApplicat ionBuilder bui
lder) {
return builder. sources (SpringBootApp.class);}
}

执行,maven clean package 即可得到对应的 war 包。同样,这里会生成两个 war 包文件:一个后缀为.war 的可独立部署的文件,一个 war.original 文件,具体命名形式参考 jar 包。

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

代码语言:javascript复制
META-INF
MANIFEST.MF
maven
WEB-INF
classes
lib
org
springframework

最后,war 包文件既能被 WarL auncher 启动,又能兼容 Servlet 容器。其实,jar 包和 war并无本质区别,因此,如果无特殊情况,尽量采用 jar 包的形式来进行打包部署。

小结

本章主要介绍了 Spring Boot 生成的 jar 包文件结构、生成方式、启动原理等内容,同时也引入了不少新概念,比如 Active、Fat jar 等。由于篇幅所限,关于 Spring Boot 中对实现 Jarin Jar 的 JAR 协议扩展不再展开,感兴趣的读者可查看代码进行学习。

本文给大家讲解的内容是SpringBoot打包部署解析:Launcher实现原理

  1. 下篇文章给大家讲解的是Spring Boot 应用监控解析;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

0 人点赞