在上节内容中,我们得知 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 来实现的启动过呈。
在了解 JarL .auncher 的实现原理之前,先来看看 JarL auncher 的源码。
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 的构造方法被调用。
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 方法源代码如下。
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 接口的具体定义如下。
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 方法调用的。
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 方法。
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 实 现 为 :
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方法获得的。相关实现源代码如下。
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 方法实现的,代码如下。
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 方法。
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 类的源码如下。
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 加载。
WarLauncher 与 Jarl auncher 都继承自抽象类 ExecutableArchiveL auncher,它们的实现方式和流程基本相同,差异很小。主要的区别是 war 包中的目录文件和 jar 包路径不同。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- 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。
<packaging>war</ packaging>
第二步,在 spring-boot-starter-web 依赖中排除 tomcat,并新增 servlet-api 依赖,这里采用的是 Servlet 2.5 版本。
<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.
<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 并实现其方法。
@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 包解压之后,目录结构如下。
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 协议扩展不再展开,感兴趣的读者可查看代码进行学习。
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。