1.如何使spring boot jar通过java -jar xxx.jar 启动
1.1spring boot项目添加spring-boot-maven-plugin插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
注意:此处需要添加executions才行,网上许多都没有添加,这是不可行的。另外此处的springboot项目是没有依赖spring-boot-starter-parent项目的,如果依赖了spring-boot-starter-parent则不需要添加上述插件,原因是spring-boot-starter-parent中已经添加了。
--下属代码来源于spring-boot-starter-parent.xxx.pom
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
此时通过maven命令打出的jar包即可通过java -jar命令运行。
2.springboot jar包分析
2.1解压上述步骤1打好的jar包
如:spring-security-1.0-SNAPSHOT.jar
解压后文件夹如下:
其中BOOT-INF是本项目自身代码、配置文件等以及依赖的jar包;META-INF是程序的入口,包含的MANIFEST.MF用于描述jar包的信息;org是spring boot loader相关的代码;
2.2 MANIFEST.MF文件
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: zhang
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.zyf.eflying.security.SpringSecutiryApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.4.5
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_192
Main-Class: org.springframework.boot.loader.JarLauncher
从上述MANIFEST.MF中可以看到Main-Class及Start-Class配置,其中Main-Class指定jar文件的入口类JarLauncher,当使用java -jar执行jar包的时候会调用JarLauncher的main方法,而不是我们编写的SpringSecurityApplication。
2.3 java -jar做了什么
官网对java -jar的解释如下:
If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.
如果指定了-jar选项,它的参数是包含应用程序的类和资源文件的JAR文件的名称。启动类必须由其源代码中的Main-Class清单头指明。
由此可见:当使用java -jar 启动springboot jar包时是去找Manifast.MF文件中的Main-Class指定的类来启动项目。
有疑问?我们的springboot项目自己编写的启动类是com.zyf.eflying.security.SpringSecutiryApplication,而在ManiFest.MF文件中Start-Class指定的才是我们自己的,那么为什么官方只提到了Main-Class,而没有提及Start-Class,并且java -jar启动类是Main-Class指定的JarLauncher,原理是怎样的的呢。
2.4 JarLauncher的执行流程
org.springframework.boot.loader.JarLauncher#main
org.springframework.boot.loader.Launcher#launch(java.lang.String[])
org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader) 中的第二个参数mainClass-》org.springframework.boot.loader.ExecutableArchiveLauncher#getMainClass
org.springframework.boot.loader.Launcher#createMainMethodRunner
org.springframework.boot.loader.MainMethodRunner#run
2.4.1添加spring-boot-loader依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
<version>2.1.16.RELEASE</version>
</dependency>
2.4.2 源码
结构图:
Main-Class入口类:org.springframework.boot.loader.JarLauncher
package org.springframework.boot.loader;
import org.springframework.boot.loader.archive.Archive;
/**
* {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
* included inside a {@code /BOOT-INF/lib} directory and that application classes are
* included inside a {@code /BOOT-INF/classes} directory.
*
* {@link Launcher}用于基于JAR的档案。该启动程序假设依赖jar包含在{@code BOOT-INFlib}目录中,并且应用程序类包含在{@code BOOT-INFclasses}目录中。
*
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
//main方法入口, 构造JarLauncher,然后调用它的launch方法。参数是控制台传递的
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
启动类基类:org.springframework.boot.loader.Launcher-》org.springframework.boot.loader.Launcher#launch(java.lang.String[])
/**
* Base class for launchers that can start an application with a fully configured
* classpath backed by one or more {@link Archive}s.
* 启动器的基类,可以用一个或多个{@link Archive}支持的完整配置的类路径启动应用程序。
* @author Phillip Webb
* @author Dave Syer
* @since 1.0.0
*/
public abstract class Launcher {
/**
* Launch the application. This method is the initial entry point that should be
* called by a subclass {@code public static void main(String[] args)} method.
* 启动应用程序。这个方法是一个初始入口点,它应该被子类{@code public static void main(String[] args)}方法调用。
* @param args the incoming arguments
* @throws Exception if the application fails to launch
*/
protected void launch(String[] args) throws Exception {
//在系统属性中设置注册了自定义的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 = createClassLoader(getClassPathArchives());
// getMainClass方法会去项目自身的Archive中的Manifest中找出key为Start-Class的类
// 调用重载方法launch
launch(args, getMainClass(), classLoader);
}
......
......
}
可执行存档启动类的基类:org.springframework.boot.loader.ExecutableArchiveLauncher -》org.springframework.boot.loader.ExecutableArchiveLauncher#getMainClass
/**
* Base class for executable archive {@link Launcher}s.
* 可执行存档{@link启动器}的基类。
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected ExecutableArchiveLauncher(Archive archive) {
this.archive = archive;
}
protected final Archive getArchive() {
return this.archive;
}
@Override
protected String getMainClass() throws Exception {
//跟代码this.archive.getManifest()发现对应的是org.springframework.boot.loader.archive.JarFileArchive#getManifest
//继续跟进入org.springframework.boot.loader.jar.JarFile#getManifest
//最后org.springframework.boot.loader.jar.JarFile#getManifest对应的是META-INF/MANIFEST.MF文件的内容
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
//获取META-INF/MANIFEST.MF文件内容中的Start-Class,这才是我们自己写的启动类
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
......
......
}
到此可以看出org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)中的mainClass参数正是我们自己写的启动类,也就是META-INF/MANIFEST.MF文件中Start-Class指定的类。
Launcher的launch方法:org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)
/**
* 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
*/
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
org.springframework.boot.loader.Launcher#createMainMethodRunner
/**
* Create the {@code MainMethodRunner} used to launch the application.
* @param mainClass the main class
* @param args the incoming arguments
* @param classLoader the classloader
* @return the main method runner
*/
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
MainMethodRunner:org.springframework.boot.loader.MainMethodRunner-》org.springframework.boot.loader.MainMethodRunner#run
/**
* Utility class that is used by {@link Launcher}s to call a main method. The class
* containing the main method is loaded using the thread context class loader.
* 被{@link Launcher}用来调用main方法的实用程序类。包含main方法的类是使用线程上下文类装入器装入的。
* @author Phillip Webb
* @author Andy Wilkinson
* @since 1.0.0
*/
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
/**
* Create a new {@link MainMethodRunner} instance.
* @param mainClass the main class
* @param args incoming arguments
*/
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 });
}
}
上述关键代码执行流程简化如下图:
至此,SpringBoot的jar独立运行的基本原理已然清晰。