前言
SpringBoot框架已经成为很多公司的标配,得益于其快速配置和容易上手,将程序员从繁杂的项目配置工作中解脱出来,将精力更多的集中在业务中,而深受猿媛们的喜爱。通过使用java -jar
命令直接运行打包后的SpringBoot项目,不用再搭建额外的tomcat等web容器便可以启动一个web项目。不知你有没有思考过,打包后的SpringBoot项目为什么可以直接运行呢?
运行命令做了什么
要想搞清楚这个问题,就要看看java -jar
命令到底做了哪些事情。通过阅读Oracle官方文档可以找到该命令的描述。
对于java [options] -jar filename [args]
有:
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 文件的名称。
启动类必须由其源代码中 manifest 的 Main-Class 指示。
说白了就是这个命令会去找 jar 文件中的 MANIFEST.MF 文件,MANIFEST.MF 文件中指定了真正的启动类。
打包命令做了什么
当我们将 spring-boot-maven-plugin 打包插件添加到项目中,运行打包命令后,打包时插件会把依赖的 Jar 文件一起打包进去,并在META-INF目录下生成一个MANIFEST.MF文件,这个文件里面包含了Start-Class和Main-Class。
打包文件结构如下:
BOOT-INF
│ ├── classes
│ │ ├── 项目文件
│ └── lib
│ ├── 第三方依赖的 jar
├── META-INF
│ ├── MANIFEST.MF
│ ├── app.properties
│ ├── maven
│ └── spring-configuration-metadata.json
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
├── archive
│ ├── Archive$Entry.class
│ ├── Archive$EntryFilter.class
│ ├── Archive.class
│ ├── ExplodedArchive$1.class
│ ├── ExplodedArchive$FileEntry.class
│ ├── ExplodedArchive$FileEntryIterator$EntryComparator.class
│ ├── ExplodedArchive$FileEntryIterator.class
│ ├── ExplodedArchive.class
│ ├── JarFileArchive$EntryIterator.class
│ ├── JarFileArchive$JarFileEntry.class
│ └── JarFileArchive.class
├── data
│ ├── RandomAccessData.class
│ ├── RandomAccessDataFile$1.class
│ ├── RandomAccessDataFile$DataInputStream.class
│ ├── RandomAccessDataFile$FileAccess.class
│ └── RandomAccessDataFile.class
├── jar
│ ├── AsciiBytes.class
│ ├── Bytes.class
│ ├── CentralDirectoryEndRecord$1.class
│ ├── CentralDirectoryEndRecord$Zip64End.class
│ ├── CentralDirectoryEndRecord$Zip64Locator.class
│ ├── CentralDirectoryEndRecord.class
│ ├── CentralDirectoryFileHeader.class
│ ├── CentralDirectoryParser.class
│ ├── CentralDirectoryVisitor.class
│ ├── FileHeader.class
│ ├── Handler.class
│ ├── JarEntry.class
│ ├── JarEntryFilter.class
│ ├── JarFile$1.class
│ ├── JarFile$2.class
│ ├── JarFile$JarFileType.class
│ ├── JarFile.class
│ ├── JarFileEntries$1.class
│ ├── JarFileEntries$EntryIterator.class
│ ├── JarFileEntries.class
│ ├── JarURLConnection$1.class
│ ├── JarURLConnection$2.class
│ ├── JarURLConnection$CloseAction.class
│ ├── JarURLConnection$JarEntryName.class
│ ├── JarURLConnection.class
│ ├── StringSequence.class
│ └── ZipInflaterInputStream.class
└── util
└── SystemPropertyUtils.class
MANIFEST.MF 文件内容如下:
Manifest-Version: 1.0
Implementation-Title: app-operation-service
Implementation-Version: 1.0.0-RELEASE
Archiver-Version: Plexus Archiver
Built-By: ambition
Implementation-Vendor-Id: com.ambition
Class-Path: lib/spring-boot-starter-2.2.5.RELEASE.jar
Spring-Boot-Version: 2.2.5.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.ambition.operation.AppOperationApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.2
Build-Jdk: 1.8.0_211
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
ot-starter-parent/app-operation/app-operation-service
可以看到里面有一个 Main-Class 和一个 Start-Class,其中 Start-Class 是我们项目应用的启动类,而 Main-Class 应该就是上面官方文档中提到的真正的启动类。那么问题又来了,我现在知道了java -jar
命令会执行org.springframework.boot.loader.JarLauncher
这个启动类,但是从结果来看好像是项目的启动类com.ambition.operation.AppOperationApplication
被执行了,这个过程是怎样的呢?
看看代码吧
前因后果都知道了,现在来看看运行时代码层面做的事情,JarLauncher
的源码:
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);
}
protected boolean isNestedArchive(Entry entry) {
return entry.isDirectory() ? entry.getName().equals("BOOT-INF/classes/") : entry.getName().startsWith("BOOT-INF/lib/");
}
public static void main(String[] args) throws Exception {
(new JarLauncher()).launch(args);
}
}
ExecutableArchiveLauncher
的源码:
public abstract class ExecutableArchiveLauncher extends Launcher {
private final Archive archive;
public ExecutableArchiveLauncher() {
try {
this.archive = this.createArchive();
} catch (Exception var2) {
throw new IllegalStateException(var2);
}
}
protected ExecutableArchiveLauncher(Archive archive) {
this.archive = archive;
}
protected final Archive getArchive() {
return this.archive;
}
protected String getMainClass() throws Exception {
// 获取文件
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 找到文件中指定的 Start-Class,也就是我们项目的启动类
mainClass = manifest.getMainAttributes().getValue("Start-Class");
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
} else {
return mainClass;
}
}
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList(this.archive.getNestedArchives(this::isNestedArchive));
this.postProcessClassPathArchives(archives);
return archives;
}
protected abstract boolean isNestedArchive(Entry entry);
protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
}
}
Launcher
的源码:
public abstract class Launcher {
public Launcher() {
}
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = this.createClassLoader(this.getClassPathArchives());
/**
* 第一个参数是启动参数
* 第二个参数是我们之前获取的项目启动类
* 第三个参数是类加载器
*/
this.launch(args, this.getMainClass(), classLoader);
}
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList(archives.size());
Iterator var3 = archives.iterator();
while(var3.hasNext()) {
Archive archive = (Archive)var3.next();
urls.add(archive.getUrl());
}
return this.createClassLoader((URL[])urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, this.getClass().getClassLoader());
}
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
this.createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
protected abstract String getMainClass() throws Exception;
protected abstract List<Archive> getClassPathArchives() throws Exception;
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));
}
}
}
}
熟悉的两个常量,这不就是我们打包后的文件目录吗,classes 目录里面是我们项目编译后的文件,lib 目录里面是我们项目依赖的第三方文件。
跟一下JarLauncher
中main
方法的代码,最后调用的是父类Launcher
的launch
方法,构造MainMethodRunner
对象后调用它的run
方法启动。
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = args != null ? (String[])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((Object)null, this.args);
}
}
就很直观了,加载文件中指定的 Start-Class,也就是我们项目的启动类,获取主方法并反射调用。
当然这只是很简单的看了一下代码,里面还有很多细节大家感兴趣的可以自己去探索,包括如何获取文件,如何处理 jar 里面的 jar 等等,因为 Java 没有提供任何标准的方式来加载嵌套的 jar 文件,可以为我们日常的工作提供一些思路。
结论
当我们在项目中加了 spring-boot-maven-plugin 打包插件后,打包时插件会把依赖的 jar 文件一起打包进去,并在 META-INF 目录下生成一个 MANIFEST.MF 文件,这个文件里面包含了 Start-Class 和 Main-Class。
因为 Java 没有提供任何标准的方式来加载嵌套的 jar 文件,所以就无法加载一起打包进去的依赖,而 Main-Class 就是帮我们加载嵌套的 jar 文件和 Class 文件的。
当我们运行java -jar
命令时,就会去 MANIFEST.MF 文件中找 Main-Class,通过 Main-Class 中的 JarLauncher 去加载 BOOT-INF\classes 目录下的 Class 文件和 BOOT-INF\lib 目录下的 jar 文件,用反射去执行 Start-Class 类也就是我们项目的启动类,完成内嵌 Tomcat 的启动。
上海米哈游内推,福利好待遇高,五险二金,早晚餐零食水果下午茶烧烤,吃货天堂,还有奶茶咖啡券,旅游基金,内推奖励,看我这么卖力打广告就知道奖励力度有多大了,更有周年礼物年会抽奖等你来拿,欢迎大家自荐和推荐:https://app.mokahr.com/recommendation-apply/mihoyo/26460?recommendCode=NTAKBmA#/jobs?from=genPoster