文章内容
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 包的形式来进行打包部署。