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
熟悉的两个常量,这不就是我们打包后的文件目录吗,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