欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

打包后的SpringBoot项目为什么可以直接运行

时间:2023-06-12
前言

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.0Implementation-Title: app-operation-serviceImplementation-Version: 1.0.0-RELEASEArchiver-Version: Plexus ArchiverBuilt-By: ambitionImplementation-Vendor-Id: com.ambitionClass-Path: lib/spring-boot-starter-2.2.5.RELEASE.jarSpring-Boot-Version: 2.2.5.RELEASEMain-Class: org.springframework.boot.loader.JarLauncherStart-Class: com.ambition.operation.AppOperationApplicationSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Created-By: Apache Maven 3.5.2Build-Jdk: 1.8.0_211Implementation-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 getClassPathArchives() throws Exception { List archives = new ArrayList(this.archive.getNestedArchives(this::isNestedArchive)); this.postProcessClassPathArchives(archives); return archives; } protected abstract boolean isNestedArchive(Entry entry); protected void postProcessClassPathArchives(List 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 archives) throws Exception { List 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 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-INFclasses 目录下的 Class 文件和 BOOT-INFlib 目录下的 jar 文件,用反射去执行 Start-Class 类也就是我们项目的启动类,完成内嵌 Tomcat 的启动。


上海米哈游内推,福利好待遇高,五险二金,早晚餐零食水果下午茶烧烤,吃货天堂,还有奶茶咖啡券,旅游基金,内推奖励,看我这么卖力打广告就知道奖励力度有多大了,更有周年礼物年会抽奖等你来拿,欢迎大家自荐和推荐:https://app.mokahr.com/recommendation-apply/mihoyo/26460?recommendCode=NTAKBmA#/jobs?from=genPoster

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。