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

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 的源码。

public class JarLauncher extends ExecutableArchiveLauncherstatic final String BOOT_ INF_ CLASSES = "BOOT- INF/classes/";static final String BOOT_ INF_ LIB = "B0OOT-INF/lib/";//省略构造方法@Overrideprotected 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() : 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, 否则创建 JarF ileArchivereturn (root. isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));}

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

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

public interface Archive extends Iterable {//获取该归档的 urlURL getUrl() throws MalformedURL Exception;// 获取 jar!/META- INF/MANIFEST.MF 或[ArchiveDir]/META- INF/MANIFEST.MFManifest getManifest() throws IOException;//获取 jar!/B0OT- INF/lib/*. jar 或[ArchiveDir]/BOOT- INF/Lib/*. jarList 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 来处理 jarURLJarFile. registerUrlProtocolHandler();//获取 Archive, 并通过 Archive 的 URL 获得 CLassL oader(这里为 aunchedURLClassLoader)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 . 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. 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;@Overrideprotected List getClassPathArchives() throws Exception {List 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 archives) throws Exception {List 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);}}

当 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 加载。

WarL auncher

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- provided/";@Overridepublic 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 {nevWarLauncher(). 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。

war packaging>

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

org . springframework. bootspring- boot-starter-weborg . springframework. bootspring-boot- starter- tomcat exclusions>javax. servletservlet-api artifactId>2. 5 dependency> dependencies>

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

org . apache . maven. pluginsmaven-war-plugin artifactId>2.6false failOnMissingWebXml>

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

@SpringBootApplicationpublic class SpringBootApp extends SpringBootServletInitializer {public static void main(String[] args) {SpringApplication. run(SpringBootApp.class, args);@Overrideprotected SpringApplicationBuilder configure(SpringApplicat ionBuilder builder) {return builder. sources (SpringBootApp.class);}}

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

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

META-INFMANIFEST.MFmavenWEB-INFclassesliborgspringframework

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

61c4c69ed5def89ab343cd4a2f093c5f.png

小结

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

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

  1. 下篇文章给大家讲解的是Spring Boot 应用监控解析;
  2. 觉得文章不错的朋友可以转发此文关注小编;
  3. 感谢大家的支持!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值