概述
Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的将我们的 Spring Boot 项目打成 jar 包或者 war 包。
考虑到部署的便利性,我们绝大多数(99.99%)的场景下,都会选择打成 jar 包,这样一来,我们就无需将项目部署于 Tomcat、Jetty 等 Servlet 容器中。
那么,通过 Spring Boot 插件生成的 jar 包是如何运行,并启动 Spring Boot 应用的呢?这个就是本文的目的,我们一起来弄懂 Spring Boot jar 包的运行原理。
这里,我通过 Spring Boot Maven Plugin 生成了一个 jar 包,其里面的结构如下所示:
BOOT-INF 目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中 classes 目录下面就是编译后的 .class 文件,包括项目中的配置文件等,lib 目录下就是我们引入的第三方依赖META-INF 目录,通过 MANIFEST.MF 文件提供 jar 包的元数据,声明 jar 的启动类等信息。每个 Java jar 包应该是都有这个文件的,参考 Oracle 官方对于 jar 的说明,里面有一个 Main-Class 配置用于指定启动类org.springframework.boot.loader 目录,也就是 Spring Boot 的 spring-boot-loader 工具模块,它就是 java -jar xxx.jar 启动 Spring Boot 项目的秘密所在,上面的 Main-Class 指定的就是该工具模块中的一个类MANIFEST.MF
META-INF/MANIFEST.MF 文件如下:
参考 Oracle 官方对该的说明:
Main-Class:Java 规定的 jar 包的启动类,这里设置为 spring-boot-loader 项目的 JarLauncher 类,进行 Spring Boot 应用的启动Start-Class:Spring Boot 规定的主启动类,这里通过 Spring Boot Maven Plugin 插件打包时,会设置为我们定义的 Application 启动类为什么不直接将我们的 Application 启动类设置为 Main-Class 启动呢?
因为通过 Spring Boot Maven Plugin 插件打包后的 jar 包,我们的 .class 文件在 BOOT-INF/classes/ 目录下,在 Java 默认的 jar 包加载规则下找不到我们的 Application 启动类,也就需要通过 JarLauncher 启动加载。当然,还有一个原因,Java 规定可执行器的 jar 包禁止嵌套其它 jar 包,在 BOOT-INF/lib 目录下有我们 Spring Boot 应用依赖的所有第三方 jar 包,因此spring-boot-loader 项目自定义实现了 ClassLoader 实现类 LaunchedURLClassLoader,支持加载 BOOT-INF/classes 目录下的 .class 文件,以及 BOOT-INF/lib 目录下的 jar 包。
接下来,我们一起来看看 Spring Boot 的 JarLauncher 这个类
1. JarLauncher
类图:
上面的 WarLauncher 是针对 war 包的启动类,和 JarLauncher 差不多,感兴趣的可以看一看,这里我们直接来看到 JarLauncher 这个类
可以看到它有个 main(String[]) 方法,前面说到的 META-INF/MANIFEST.MF 文件中的 Main-Class 配置就是指向了这个类,也就会调用这里的 main 方法,会做下面两件事:
创建一个 JarLauncher 实例对象,在 ExecutableArchiveLauncher 父类中会做以下事情:
会为当前应用创建一个 Archive 对象,可用于解析 jar 包(当前应用)中所有的信息,可以把它理解为一个“根”对象,可以通过它获取我们所需要的类信息调用 JarLauncher#launch(String[]) 方法,也就是调用父类 Launcher 的这个方法2. Launcher
org.springframework.boot.loader.Launcher,Spring Boot 应用的启动器
2. launch 方法
会做以下几件事:
调用 JarFile#registerUrlProtocolHandler() 方法,注册 URL(jar)协议的处理器,主要是使用自定义的 URLStreamHandler 处理器处理 jar 包调用 getClassPathArchives() 方法,先从 archive(当前 jar 包应用)解析出所有的 JarFileArchive,这个 archive 就是在上面创建 JarLauncher 实例对象过程中创建的调用 createClassLoader(List<Archive>) 方法,创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 包中所有的类,包括依赖的第三方包调用 getMainClass() 方法,获取当前应用的启动类(你自己写的那个 main 方法所在的 Class 类对象)调用 launch(...) 方法,执行你的项目中那个启动类的 main 方法(反射)你可以理解为会创建一个自定义的 ClassLoader 类加载器,主要可加载 BOOT-INF/classes 目录下的类,以及 BOOT-INF/lib 目录下的 jar 包中的类,然后调用你 Spring Boot 应用的启动类的 main 方法
接下来我们逐步分析上面的每个步骤
2.1 registerUrlProtocolHandler 方法
备注:注册 URL(jar)协议的处理器
这个方法在 org.springframework.boot.loader.jar.JarFile 中,这个类是 java.util.jar.JarFile 的子类,对它进行扩展,提供更多的功能,便于操作 jar 包
方法的处理过程如下:
获取系统变量中的 java.protocol.handler.pkgs 配置的 URLStreamHandler 路径将 Spring Boot 自定义的 URL 协议处理器路径(org.springframework.boot.loader)添加至系统变量中JVM 启动时会获取 java.protocol.handler.pkgs 属性,多个用 | 分隔,以他们作为包名前缀,然后使用 包名前缀.协议名.Handler 作为该协议的实现那么这里就会将 org.springframework.boot.loader.jar.Handler 作为 jar 包协议的实现,用于处理 jar 包重置已缓存的 URLStreamHandler 处理器们,避免重复创建
2.2 getClassPathArchives 方法
备注:从 archive(当前 jar 包应用)解析出所有的 JarFileArchive
该方法在 org.springframework.boot.loader.ExecutableArchiveLauncher 子类中实现,如下:
过程如下:
创建一个 Archive.EntryFilter 实现类,用于判断 Archive.Entry 是否匹配,过滤掉 jar 包(当前应用)以外的东西
2.从 archive(当前 jar 包)解析出所有 Archive 条目信息,这个 archive 在上面 1. JarLauncher讲到过,创建 JarLauncher 实例化对象的时候会初始化 archive,是一个 JarFileArchive 对象,也就是我们打包后的 jar 包,那么接下来需要从中解析出所有的 Archive 对象
返回 jar 包(当前应用)找到的所有 JarFileArchive:BOOT-INF/classes/ 目录对应一个 JarFileArchive(因为就是当前 Spring Boot 应用编译后的内容)BOOT-INF/lib/ 目录下的每个 jar 包对应一个 JarFileArchive返回从 jar 包中找到的所有 JarFileArchive这一步骤就是从 jar 包中解析出我们需要的东西来,如上描述,每个 JarFileArchive 会对应一个 JarFile 对象
2.3 createClassLoader 方法
备注:创建 Spring Boot 自定义的 ClassLoader 类加载器,可加载当前 jar 中所有的类
该过程如下:
获取所有 JarFileArchive 对应的 URL创建 Spring Boot 自定义的 ClassLoader 类加载器,并设置父类加载器为当前线程的类加载器可以看到 LaunchedURLClassLoader 为自定义类加载器,这样就能从我们 jar 包中的 BOOT-INF/classes/ 目录下和 BOOT-INF/lib/ 目录下的所有三方依赖包中加载出 Class 类对象
2.4 getMainClass 方法
备注:获取当前应用的启动类(你自己写的那个 main 方法)
过程如下:
获取 jar 包(当前应用)的 Manifest 对象,也就是 META-INF/MANIFEST.MF 文件中的属性获取启动类(当前应用自己的启动类),也就是 Start-Class 配置,并返回可以看到,这一步就是找到你 Spring Boot 应用的启动类,前面 ClassLoader 类加载器都准备好了,那么现在不就可以直接调用这个类的 main 方法来启动应用了
2.5 launch 方法
备注:执行你的 Spring Boot 应用的启动类的 main 方法
整个过程很简单,先设置当前线程的 ClassLoader 为刚创建的类加载器,然后创建一个 MainMethodRunner 对象(main 方法执行器),执行你的 main 方法(反射),启动 Spring Boot 应用
这里就是通过反射调用你的 Spring Boot 应用的启动类的 main 方法
LaunchedURLClassLoader
org.springframework.boot.loader.LaunchedURLClassLoader 是 spring-boot-loader 中自定义的类加载器,实现对 jar 包中 BOOT-INF/classes 目录下的类和 BOOT-INF/lib 下第三方 jar 包中的类的加载。
上面的代码就不一一讲述了,LaunchedURLClassLoader 重写了 ClassLoader 的 loadClass(String, boolean) 加载 Class 类对象方法,在加载对应的 Class 类对象之前新增了一部分逻辑,会尝试从 jar 包中定义 Package 包对象,这样就能加载到对应的 Class 类对象。
总结
Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的将我们的 Spring Boot 项目打成 jar 包,jar 包中主要分为三个模块:
BOOT-INF 目录,里面保存了我们自己 Spring Boot 项目编译后的所有文件,其中 classes 目录下面就是编译后的 .class 文件,包括项目中的配置文件等,lib 目录下就是我们引入的第三方依赖META-INF 目录,通过 MANIFEST.MF 文件提供 jar 包的元数据,声明 jar 的启动类等信息。每个 Java jar 包应该是都有这个文件的,参考 Oracle 官方对于 jar 的说明,里面有一个 Main-Class 配置用于指定启动类org.springframework.boot.loader 目录,也就是 Spring Boot 的 spring-boot-loader 子模块,它就是 java -jar xxx.jar 启动 Spring Boot 项目的秘密所在,上面的 Main-Class 指定的就是里面的一个类通过 java -jar 启动应用时,根据 Main-Class 配置会调用 org.springframework.boot.loader.JarLauncher 的main(String[]) 方法;其中会先创建一个自定义的 ClassLoader 类加载器,可从BOOT-INF目录下加载出我们 Spring Boot 应用的 Class 类对象,包括依赖的第三方 jar 包中的 Class 类对象;然后根据 Start-Class 配置调用我们 Spring Boot 应用启动类的 main(String[]) 方法(反射),这样也就启动了应用,至于我们的 main(String[]) 方法中做了哪些事情,也就是后续所讲的内容。
如何获取?
转发分享此文,后台私信小编:“ 资料 ”即可获取。(注:转发分享,感谢大家)