spring-boot-loader执行Jar文件原理

刘弘济
2023-12-01

1.spring-boot-loader简介

​ spring-boot-loader模块让你的springboot应用具备打包为可执行jar或war文件的能力。只需要引入Maven插件或者Gradle插件就可以自动生成。

2.spring-boot-loader的优势

​ Java中并没有标准的方法加载嵌入式的jar文件,通常都是在一个jar文件中。这种情况下,如果你要通过命令行的形式发布一个没有打包的独立程序的话,可能会出现问题。

​ 为了解决这种问题,很多开发人员将所有的class文件都打包为一个jar文件,然后依赖其他的jar文件。但是这种方式下,开发人员很难去判断哪个依赖的文件库是被程序真正使用到的。更普遍的问题是,在不同的jar文件中,如果有相同名称的文件则会冲突。(这里说明一下,在传统的可执行jar文件中会有/META-INF/MANIFEST.MF文件,这里主要介绍两个属性:Main-Class和classpath,Main-Class是可执行jar的启动类,classpath则可以指定依赖的类库。)

3.springboot可执行jar文件结构

读者可以自行编写一个简单的springboot应用,然后用Maven插件打包。

example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-BOOT-INF
    +-classes
    |  +-mycompany
    |     +-project
    |        +-YourClasses.class
    +-lib
       +-dependency1.jar
       +-dependency2.jar
  • META-INF : 存放应用相关的元信息,如MANIFEST.MF文件。
  • BOOT-INF/lib: 存放应用依赖的jar包。
  • BOOT-INF/classes:存放应用编译后的class文件。
  • org:存放springboot相关的class文件。

熟悉Java EE的读者可能会发现,其中目录BOOT-INF下的classes和lib和WEB-INF下的classes和lib相似。但是为什么可以通过命令行的方式去执行Jar文件呢?

4.spring-boot-loder原理

springboot应用可执行jar文件被java -jar 命令执行时,还是要按照Java官方文档的规定,命令的启动类必须配置在MANIFEST.MF文件的Main-class属性。

查看springboot应用jar包中的MANIFEST.MF发现:

Main-Class: org.springframework.boot.loader.JarLauncher

可执行JAR文件启动器-Jarlauncher

首先,在pom.xml中添加spring-boot-loader依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-loader</artifactId>
    <scope>provided</scope>
</dependency>

在IDEA中,双击shift搜索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);
	}

	@Override
	protected boolean isNestedArchive(Archive.Entry entry) {
		if (entry.isDirectory()) {
			return entry.getName().equals(BOOT_INF_CLASSES);
		}
		return entry.getName().startsWith(BOOT_INF_LIB);
	}

	public static void main(String[] args) throws Exception {
		new JarLauncher().launch(args);
	}

}

从上面代码可以看出,BOOT- INF/classes/和BOOT-INF/lib/路径分别用常量BOOT INF_ CLASSES 和BOOT INF_ LIB表示,并且用于isNestedArchive(Archive. Entry)方法判断,从该方法的实现分析,方法参数Archive . Entry对象看似为JAR文件中的资源,比如application. properties,不过该对象与Java标准的java . util. jar . JarEntry对象类似,其name属性( getName()方法)为JAR资源的相对路径。当application. properties资源位于FAT JAR时,实际的Archive . Entry#getName()为/BOOT- INF/classes/application. properties,故符合entry. getName(). startsWith(BOOT_ INF_ LIB) 的判断,即isNestedArchive(Archive. Entry)方法返回true.反之,该方法返回false时,说明FAT JAR被解压至文件目录,因此从侧面说明了Spring Boot应用能直接通过java org. springframework. boot . loader .JarLauncher命令启动的原因。换言之,Archive. Entry与java.util.jar .JarEntry也存在差异,它也可表示文件或目录。实际上,Archive . Entry存在两种实现,其中一种为 JarFileArchive.JarFileEntry,基于java.util. jar . JarEntry实现,表示FATJAR嵌入资源:

public class JarFileArchive implements Archive {
 ....
	/**
	 * {@link Archive.Entry} implementation backed by a {@link JarEntry}.
	 */
	private static class JarFileEntry implements Entry {

		private final JarEntry jarEntry;

		JarFileEntry(JarEntry jarEntry) {
			this.jarEntry = jarEntry;
		}

		public JarEntry getJarEntry() {
			return this.jarEntry;
		}

		@Override
		public boolean isDirectory() {
			return this.jarEntry.isDirectory();
		}

		@Override
		public String getName() {
			return this.jarEntry.getName();
		}

	}

}

另一种实现是ExplodedArchive.FileEntry,基于文件系统实现:

public class ExplodedArchive implements Archive {

	......

   /**
    * {@link Entry} backed by a File.
    */
   private static class FileEntry implements Entry {

      private final String name;

      private final File file;

      FileEntry(String name, File file) {
         this.name = name;
         this.file = file;
      }

      public File getFile() {
         return this.file;
      }

      @Override
      public boolean isDirectory() {
         return this.file.isDirectory();
      }

      @Override
      public String getName() {
         return this.name;
      }

   }

}

从上面可以知道JarLauncher支持Jar和文件系统的两种启动方式。

同时,JarLauncher 同样作为引导类,当执行java -jar 命令时,/META-INF/ 资源的Main-Class属性将调用其main(String[])方法,实际上调用的是JarLauncher#launch(args)方法,而该方法继承于基类org. springframework . boot . loader .

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.
    * @param args the incoming arguments
    * @throws Exception if the application fails to launch
    */
   protected void launch(String[] args) throws Exception {
      JarFile.registerUrlProtocolHandler();
      ClassLoader classLoader = createClassLoader(getClassPathArchives());
      launch(args, getMainClass(), classLoader);
   }

这里主要看launch方法

	protected void launch(String[] args, String mainClass, ClassLoader classLoader)
			throws Exception {
		Thread.currentThread().setContextClassLoader(classLoader);
		createMainMethodRunner(mainClass, args, classLoader).run();
	}

protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
			ClassLoader classLoader) {
		return new MainMethodRunner(mainClass, args);
	}

该方法的执行者为MainMethodRunner#run()方法:

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 });
	}

MainMethodRunner对象需要关联mainClass及main方法参数args,而mainClass来自于ExecutableArchiveLauncher#getMainClass方法:

@Override
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);
   }
   return mainClass;
}

到这里就可以知道,主要的mainMethodRunner关联的是MANIFEST.MF文件中Start-Class属性指定的类。

最后总结一下,首先是按照Java规范指定main-class(Spring boot中的JarLauncher),然后指定类加载器,加载并执行MANIFEST.MF文件中Start-Class属性指定类的main方法。

 类似资料: