目录
一. 🦁 前言
Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以方便的将 Spring Boot 项目打成
jar
包或者
war
包。
考虑到部署的便利性,我们绝大多数人在 99.99% 的场景下,都会选择打成
jar
包。这样,我们就无需在部署项目的服务器上,配置相应的 Tomcat、Jetty 等 Servlet 容器。
那么,
jar
包是如何运行,并启动 Spring Boot 项目的呢?这个就是本文的目的,一起弄懂 Spring Boot
jar
包的运行原理。
二. 🦁 目录结构
下面,我们来打开一个 Spring Boot
jar
包,看看其里面的结构。如下图所示,一共分成四部分:
META-INF
目录:通过MANIFEST.MF
文件提供jar
包的元数据,声明了jar
的启动类。通常包含一些特定的文件和目录,用于描述存档文件的内容和结构。该目录通常包含以下文件:
- MANIFEST.MF文件:该文件包含关于JAR文件的元数据信息,如版本号、创建者、主类等。
- INDEX.LIST文件:该文件包含JAR文件中所有文件的索引列表。
- SIGNATURE文件:该文件包含JAR文件的数字签名,以确保文件的完整性和安全性。
- *.SF文件:该文件包含JAR文件中每个文件的数字签名,以确保文件的完整性和安全性。
一句话总结:
确保JAR文件的完整性和安全性,并提供关于JAR文件的元数据信息。这些信息对于Java应用程序的部署和运行都是非常重要的。
org
目录:为 Spring Boot 提供的spring-boot-loader 项目,它是
java -jar` 启动 Spring Boot 项目的核心,通过将Spring Boot应用程序打包为自包含的可执行JAR文件来简化Spring Boot应用程序的部署。BOOT-INF/lib
目录:Spring Boot 项目中引入的依赖的jar
包存放之处。spring-boot-loader
项目很大的一个作用,就是解决jar
包里嵌套jar
的情况,如何加载到其中的类。BOOT-INF/classes
目录:我们在 Spring Boot 项目中 Java 类所编译的.class
、配置文件等。
2.1 MANIFEST.MF中的jar启动类
Manifest-Version:1.0Created-By:Maven JAR Plugin3.2.2Build-Jdk-Spec:11Implementation-Title: campusemploydemo
Implementation-Version:0.0.1-SNAPSHOT
Main-Class:org.springframework.boot.loader.JarLauncherStart-Class:com.jc.campusemploydemo.CampusemploydemoApplicationSpring-Boot-Version:2.7.1Spring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
前面说过这个文件是一个清单文件,主要声明jar包的元数据信息(版本号、项目名、主要类、依赖库等)。详细解析如下:
Manifest-Version:清单文件的版本号
Created-By:清单文件的创建者
Main-Class:JAR 文件的主类
Class-Path:JAR 文件所依赖的其他 JAR 文件路径
Start-Class:配置Spring Boot 规定的主启动类
Implementation-Title:实现的名称
Implementation-Version:实现的版本号
Implementation-Vendor:实现的提供者
Specification-Title:规范的名称
Specification-Version:规范的版本号
Specification-Vendor:规范的提供者
通过 MANIFEST.MF 文件,可以方便地查看和管理 JAR
现在我们来看看
Main-Class
和
Start-Class
配置类:
Main-Class
:这里设置为spring-boot-loader
项目的JarLauncher
类,进行 Spring Boot 应用的启动。 JarLauncher:是针对 Spring Bootjar
包的启动类,整体类图如下所示:
其源码如下:
publicclassJarLauncherextendsExecutableArchiveLauncher{staticfinalString BOOT_INF_CLASSES ="BOOT-INF/classes/";staticfinalString BOOT_INF_LIB ="BOOT-INF/lib/";publicJarLauncher(){}protectedJarLauncher(Archive archive){super(archive);}@OverrideprotectedbooleanisNestedArchive(Archive.Entry entry){if(entry.isDirectory()){return entry.getName().equals(BOOT_INF_CLASSES);}return entry.getName().startsWith(BOOT_INF_LIB);}publicstaticvoidmain(String[] args)throwsException{newJarLauncher().launch(args);}}
通过
main(String[] args)
方法,创建 JarLauncher 对象,并调用其
launch(String[] args)
(该方法由父类 Launcher所提供)方法进行启动。整体的启动逻辑如下图所示:
JarFile.registerUrlProtocolHandler():调用 JarFile 的
registerUrlProtocolHandler()
方法,注册 Spring Boot 自定义的 URLStreamHandler实现类,用于
jar
包的加载读取。
createClassLoader(List archives): 创建自定义的 ClassLoader实现类,用于从
jar
包中加载类。
launch(args, getMainClass(), classLoader):执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。
简单来说,就是整一个可以读取
jar
包中类的加载器,保证
BOOT-INF/lib
目录下的类和
BOOT-classes
内嵌的
jar
中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。
2.2 深入剥析
2.2.1 registerUrlProtocolHandler()
privatestaticfinalString PROTOCOL_HANDLER ="java.protocol.handler.pkgs";privatestaticfinalString HANDLERS_PACKAGE ="org.springframework.boot.loader";/**
* Register a {@literal 'java.protocol.handler.pkgs'} property so that a
* {@link URLStreamHandler} will be located to deal with jar URLs.
*/publicstaticvoidregisterUrlProtocolHandler(){// 获得 URLStreamHandler 的路径String handlers =System.getProperty(PROTOCOL_HANDLER,"");// 将 Spring Boot 自定义的 HANDLERS_PACKAGE(org.springframework.boot.loader) 补充上去System.setProperty(PROTOCOL_HANDLER,("".equals(handlers)? HANDLERS_PACKAGE
: handlers +"|"+ HANDLERS_PACKAGE));// 重置已缓存的 URLStreamHandler 处理器们resetCachedUrlHandlers();}/**
* Reset any cached handlers just in case a jar protocol has already been used.
* We reset the handler by trying to set a null {@link URLStreamHandlerFactory} which
* should have no effect other than clearing the handlers cache.
*
* 重置 URL 中的 URLStreamHandler 的缓存,防止 `jar://` 协议对应的 URLStreamHandler 已经创建
* 我们通过设置 URLStreamHandlerFactory 为 null 的方式,清空 URL 中的该缓存。
*/privatestaticvoidresetCachedUrlHandlers(){try{
URL.setURLStreamHandlerFactory(null);}catch(Error ex){// Ignore}}
目的很明确,通过将
org.springframework.boot.loader
包设置到
"java.protocol.handler.pkgs"
环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理
jar:
协议的 URL。
Start-Class
配置项:Spring Boot 规定的主启动类,这里设置为我们定义的 Application 类。
tips:
Main-Class
/
Start-Class
的配置主要是通过Maven插件spring-boot-maven-plugin打包写入了
MANIFEST.MF
文件中,从而让
spring-boot-loader
引导启动 Spring Boot 应用。
2.2.2 createClassLoader(getClassPathArchives())
很明显这个方法是以另一个方法作为参数实现的方法,先来分析下该方法的参数
getClassPathArchives()
、它是由 ExecutableArchiveLauncher 所实现,代码如下:
privatefinalArchive archive;@OverrideprotectedList<Archive>getClassPathArchives()throwsException{// ① 获得所有 ArchiveList<Archive> archives =newArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));// ② 后续处理postProcessClassPathArchives(archives);return archives;}protectedabstractbooleanisNestedArchive(Archive.Entry entry);protectedvoidpostProcessClassPathArchives(List<Archive> archives)throwsException{}
2.2.3 launch(args,getMainClass(),classLoader)
我们先来看看参数的方法:
- getMainClass()
@OverrideprotectedStringgetMainClass()throwsException{//读取文件内容Manifest manifest =this.archive.getManifest();//String mainClass =null;if(manifest !=null){//获取Start-Class属性//
mainClass = manifest.getMainAttributes().getValue("Start-Class");}if(mainClass ==null){thrownewIllegalStateException("No 'Start-Class' manifest entry specified in "+this);}//返回内容return mainClass;}
从
jar
包的
MANIFEST.MF
文件的
Start-Class
配置项,,获得我们设置的 Spring Boot 的主启动类。
2. launch(args,getMainClass(),classLoader)
它是由 Launcher 类所实现,代码如下:
/**
* Launch the application given the archive file and a fully configured
* classloader.
*
* @param args
* the incoming arguments
* @param mainClass
* the main class to run
* @param classLoader
* the classloader
* @throws Exception
* if the launch fails
*/protectedvoidlaunch(String[] args,String mainClass,ClassLoader classLoader)throwsException{// ① 设置类加载器Thread.currentThread().setContextClassLoader(classLoader);// ② 创建一个新的RunnercreateMainMethodRunner(mainClass, args, classLoader).run();}
该方法负责最终的 Spring Boot 应用真正的启动。
①
处:设置createClassLoader创建的 LaunchedURLClassLoader 作为类加载器,从而保证能够从jar
加载到相应的类。②
处,调用createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader)
方法,创建 MainMethodRunner对象,并执行其run()
方法来启动 Spring Boot 应用。
- MainMethodRunner 类
publicclassMainMethodRunner{privatefinalString mainClassName;privatefinalString[] args;/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/publicMainMethodRunner(String mainClass,String[] args){this.mainClassName = mainClass;this.args =(args !=null)? args.clone():null;}publicvoidrun()throwsException{// ① 加载 Spring BootClass<?> mainClass =Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);// ② 反射调用 main 方法Method mainMethod = mainClass.getDeclaredMethod("main",String[].class);
mainMethod.invoke(null,newObject[]{this.args });}}
① 通过 LaunchedURLClassLoader 类加载器,加载到我们设置的 Spring Boot 的主启动类。
② 通过反射调用主启动类的main(String[] args)
方法,启动 Spring Boot 应用。这里也告诉了我们答案,为什么我们通过编写一个带有
main(String[] args)
方法的类,就能够启动 Spring Boot 应用。
- run()
public void run() throws Exception {
//看到这里了
//main[1] print mainClass
Class<?> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
//然后获取主方法
//main[1] print mainMethod
// mainMethod = "public static void zipkin.server.ZipkinServer.main(java.lang.String[])"
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
//触发方法,因为main是静态函数,所以第一个参数为null
mainMethod.invoke(null, new Object[] { this.args });
}
三.总结
SpringBoot Jar包启动原理到这里结束啦,咱们下期见!!!😄
版权归原作者 狮子也疯狂 所有, 如有侵权,请联系我们删除。