一、场景介绍
-
spring-boot
项目搭建以后启动方式一般有两种:-
源码方式启动
@SpringBootApplication public class ServiceApplication { public static void main(String[] args) { SpringApplication.run(ServiceApplication.class, args); CommonUtil.info(); } }
-
命令方式启动
java -jar example-service.jar
-
-
采用
spring-boot-maven
插件打包且包含主启动类的spring-boot
项目,为什么通过main
方法就能启动呢,让我们一起来探讨吧?
二、项目搭建
-
新建
Maven
父子工程example - example-common - CommonUtil.java - pom.xml - example-service - ServiceApplication.java - pom.xml - pom.xml
P.S
-
example
为example-common
和example-service
的父工程 -
example-service
依赖example-common
模块 -
example-service
模块包含主启动类,为标准都spring boot
工程,example-common
模块无启动类 -
父工程
pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 模块坐标信息 GAV --> <groupId>com.rambo</groupId> <artifactId>example</artifactId> <packaging>pom</packaging> <version>V1.0.0.1</version> <name>${project.artifactId}</name> <description>父子模块示例工程 —— 基础父工程</description> <!-- 子模块列表 --> <modules> <module>example-common</module> <module>example-service</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <spring-boot.version>2.5.2</spring-boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <finalName>${project.artifactId}</finalName> </build> </project>
-
example-common
模块pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 父工程 GAV --> <parent> <artifactId>example</artifactId> <groupId>com.rambo</groupId> <version>V1.0.0.1</version> </parent> <!-- 本工程模块 AV --> <artifactId>example-common</artifactId> <version>V1.0.0.1</version> <packaging>jar</packaging> <name>${project.artifactId}</name> <description>无启动类的示例通用模块 —— 通用工具</description> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>
-
example-service
模块pom
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <!-- 父工程 GAV --> <parent> <artifactId>example</artifactId> <groupId>com.rambo</groupId> <version>V1.0.0.1</version> </parent> <!-- 本工程模块 AV --> <artifactId>example-service</artifactId> <version>V1.0.0.1</version> <packaging>jar</packaging> <name>example-service</name> <description>有启动类的示例服务模块 —— 示例服务</description> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 示例通用工具模块 --> <dependency> <groupId>com.rambo</groupId> <artifactId>example-common</artifactId> <version>V1.0.0.1</version> </dependency> <!-- 方便解读编译后的字节码 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <!-- spring boot maven 打包插件 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
-
-
example-common
和example-service
的示例代码如下-
example-common
模块public class CommonUtil { public static void info() { System.out.println("This is info from CommonUtil.class"); } }
-
example-service
模块@SpringBootApplication public class ServiceApplication { public static void main(String[] args) { SpringApplication.run(ServiceApplication.class, args); CommonUtil.info(); } }
-
三、解体 JAR 包
-
在父工程的
pom
文件同级执行Maven
打包命令-
打包命令
mvn package
-
example-service
模块打出来的JAR
包# 该 JAR 也成为 Fat JAR,采用 ZIP 压缩格式存储 example-service.jar example-service.jar.original
-
-
解压
example-service.jar
到指定文件夹分析打出来的内容unzip example-service.jar -d example-service
-
BOOT-INFO/class
目录存放应用编译后的class
文件 -
BOOT-INFO/lib
目录存放应用依赖的JAR
包 -
META-INF/
目录存放应用相关的元信息,如:MANIFEST.MF
-
org/
目录存放Spring Boot
相关的class
文件 -
example-service
模块的MANIFEST.MF
文件内容如下Manifest-Version: 1.0 Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Archiver-Version: Plexus Archiver Built-By: rambo Spring-Boot-Layers-Index: BOOT-INF/layers.idx Start-Class: com.rambo.service.ServiceApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Version: 2.5.2 Created-By: Apache Maven 3.6.3 Build-Jdk: 1.8.0_271 Main-Class: org.springframework.boot.loader.JarLauncher
-
-
启动测试
-
在
example-service.jar
运行java -jar example-service.jar
~/WorkSpace/example/example-service/target/ java -jar example-service.jar . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.2) 2021-07-13 16:42:38.813 INFO 38536 --- [ main] com.rambo.service.ServiceApplication : Starting ServiceApplication using Java 1.8.0_271 on Rambos-MacBook-Pro.local with PID 38536 (/Users/rambo/WorkSpace/example/example-service/target/example-service.jar started by rambo in /Users/rambo/WorkSpace/example/example-service/target) 2021-07-13 16:42:38.815 INFO 38536 --- [ main] com.rambo.service.ServiceApplication : No active profile set, falling back to default profiles: default 2021-07-13 16:42:39.950 INFO 38536 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9999 (http) 2021-07-13 16:42:39.967 INFO 38536 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2021-07-13 16:42:39.967 INFO 38536 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.48] 2021-07-13 16:42:40.040 INFO 38536 --- [ main] o.a.c.c.C.[.[localhost].[/example] : Initializing Spring embedded WebApplicationContext 2021-07-13 16:42:40.040 INFO 38536 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1176 ms 2021-07-13 16:42:40.660 INFO 38536 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9999 (http) with context path '/example' 2021-07-13 16:42:40.678 INFO 38536 --- [ main] com.rambo.service.ServiceApplication : Started ServiceApplication in 3.217 seconds (JVM running for 3.66) This is info from CommonUtil.class
-
解压后执行主启动类
Jarlaunch
java org.springframework.boot.loader.JarLauncher
~/WorkSpace/example/example-service/target/example-service/ java org.springframework.boot.loader.JarLauncher . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.5.2) 2021-07-13 16:43:55.120 INFO 38587 --- [ main] com.rambo.service.ServiceApplication : Starting ServiceApplication using Java 1.8.0_271 on Rambos-MacBook-Pro.local with PID 38587 (/Users/rambo/WorkSpace/example/example-service/target/example-service/BOOT-INF/classes started by rambo in /Users/rambo/WorkSpace/example/example-service/target/example-service) 2021-07-13 16:43:55.122 INFO 38587 --- [ main] com.rambo.service.ServiceApplication : No active profile set, falling back to default profiles: default 2021-07-13 16:43:55.982 INFO 38587 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 9999 (http) 2021-07-13 16:43:55.994 INFO 38587 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2021-07-13 16:43:55.995 INFO 38587 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.48] 2021-07-13 16:43:56.071 INFO 38587 --- [ main] o.a.c.c.C.[.[localhost].[/example] : Initializing Spring embedded WebApplicationContext 2021-07-13 16:43:56.071 INFO 38587 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 879 ms 2021-07-13 16:43:56.438 INFO 38587 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9999 (http) with context path '/example' 2021-07-13 16:43:56.451 INFO 38587 --- [ main] com.rambo.service.ServiceApplication : Started ServiceApplication in 1.749 seconds (JVM running for 2.102) This is info from CommonUtil.class
-
四、原理分析
-
MANIFEST.MF
文件分析Main-Class: org.springframework.boot.loader.JarLauncher
- 通过
spring-boot-maven
插件打包出来的项目,主启动类设置为JarLauncher
或者WarLauncher
Spring-Boot-Classes: BOOT-INF/classes/
- 通过
Spring-Boot-Classes
来定义应用代码的二进制文件路径
Spring-Boot-Lib: BOOT-INF/lib/
- 通过
Spring-Boot-Lib
来定义应用程序所需依赖位置
Start-Class: com.rambo.service.ServiceApplication
- 项目编写的启动类通过
Start-Class
来定义,并指定主启动类的包全路径
- 通过
-
代理启动类分析
JarLauncher 、WarLauncher 、PropertiesLauncher代表了Spring Boot的三种代理启动方式
-
JarLauncher
启动类分析-
JarLauncher.java
public static void main(String[] args) throws Exception { // 构造 JarLauncher 对象,同时执行父类 ExecutableArchiveLauncher 的构造方法,再调用父类的 launch 方法 (new JarLauncher()).launch(args); } // 获取 classpath.idx 中记录的依赖信息 protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException { if (archive instanceof ExplodedArchive) { String location = this.getClassPathIndexFileLocation(archive); return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location); } else { return super.getClassPathIndex(archive); } }
-
ExecutableArchiveLauncher.java
// 父类构造函数 public ExecutableArchiveLauncher() { try { this.archive = this.createArchive(); this.classPathIndex = this.getClassPathIndex(this.archive); } catch (Exception var2) { throw new IllegalStateException(var2); } } // Archive的getMainClass方法 protected String getMainClass() throws Exception { Manifest manifest = this.archive.getManifest(); String mainClass = null; if (manifest != null) { mainClass = manifest.getMainAttributes().getValue("Start-Class"); } if (mainClass == null) { throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this); } else { return mainClass; } }
-
Launcher.java
// 判断文件路径和归档文件是否正确 protected final Archive createArchive() throws Exception { ProtectionDomain protectionDomain = this.getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = codeSource != null ? codeSource.getLocation().toURI() : null; String path = location != null ? location.getSchemeSpecificPart() : null; if (path == null) { throw new IllegalStateException("Unable to determine code source archive"); } else { File root = new File(path); if (!root.exists()) { throw new IllegalStateException("Unable to determine code source archive from " + root); } else { return (Archive)(root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); } } } // 父类 launch 方法 protected void launch(String[] args) throws Exception { if (!this.isExploded()) { // 在系统属性中设置注册了自定义的URL处理器:org.springframework.boot.loader.jar.Handler。如果URL中没有指定处理器,会去系统属性中查询 JarFile.registerUrlProtocolHandler(); } // getClassPathArchives方法在会去找lib目录下对应的第三方依赖JarFileArchive,同时也会项目自身的JarFileArchive // 根据getClassPathArchives得到的JarFileArchive集合去创建类加载器ClassLoader。这里会构造一个LaunchedURLClassLoader类加载器,这个类加载器继承URLClassLoader,并使用这些JarFileArchive集合的URL构造成URLClassPath // LaunchedURLClassLoader类加载器的父类加载器是当前执行类JarLauncher的类加载器 ClassLoader classLoader = this.createClassLoader(this.getClassPathArchivesIterator()); String jarMode = System.getProperty("jarmode"); String launchClass = jarMode != null && !jarMode.isEmpty() ? "org.springframework.boot.loader.jarmode.JarModeLauncher" : this.getMainClass(); // 执行重载方法 this.launch(args, launchClass, classLoader); } // launch 重载方法 protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception { Thread.currentThread().setContextClassLoader(classLoader); this.createMainMethodRunner(launchClass, args, classLoader).run(); } // protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner(mainClass, args); }
-
MainMethodRunner.java
public MainMethodRunner(String mainClass, String[] args) { this.mainClassName = mainClass; this.args = args != null ? (String[])args.clone() : null; } public void run() throws Exception { Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader()); Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); mainMethod.setAccessible(true); mainMethod.invoke((Object)null, this.args); }
-
-
总结
-
spring-boot-maven
插件,在打包过程中,将Spring Boot
定义的一套规则导入到了打出来的JAR
包中 -
JAR
包的URL
路径使用自定义的规则并且这个规则需要使用org.springframework.boot.loader.jar.Handler
处理器处理 -
它的
Main-Class
使用JarLauncher
,如果是war
包,使用WarLauncher
执行 -
这些
Launcher
内部都会另起一个线程启动自定义的SpringApplication
类 -
这些特性通过
spring-boot-maven-plugin
插件打包完成 -
SpringBoot
通过扩展JarFile
、JarURLConnection
及URLStreamHandler
,实现了jar in jar
中资源的加载 -
SpringBoot
通过扩展URLClassLoader--LauncherURLClassLoader
,实现了jar in jar
中class
文件的加载 -
JarLauncher
通过加载BOOT-INF/classes
目录及BOOT-INF/lib
目录下jar
文件,实现了fat jar
的启动 -
WarLauncher
通过加载WEB-INF/classes
目录及WEB-INF/lib
和WEB-INF/lib-provided
目录下的jar
文件,实现了war
文件的直接启动及web
容器中的启动
-