java -jar命令引导启动Springboot项目的那点事

2022-07-25,,,,

前言:Java官方规定java -jar命令引导的具体启动类必须配置在MANIFEST.MF资源的Main-Class属性中。比如通过java -jar XXX.jar来运行应用时,如不做特殊设置就要求在jar文件中必须包含META-INF/MANIFEST.MF文件,且通过类似Main-Class: org.springframework.boot.loader.JarLauncher来指定启动类全路径名,有点类似jre中的java -cp  XXX.jar org.springframework.boot.loader.JarLauncher方式。在spring-boot-maven插件repackage(goal)的那些事这篇博客中简单介绍了采用spring-boot-maven插件打包Springboot应用后的jar包的组成结构,下面通过下图所示的META-INF/MANIFEST.MF内容来分析下Springboot应用启动的那些事,以下MANIFEST.MF文件的属性顺序进行了少许调整,需要说明的是红框以外的内容阅读下即可,重点关注红框部分内容;

大胆猜测下:执行java -jar first-app-by-gui-0.0.1.jar命令时会执行org.springframework.boot.loader.JarLauncher类的main方法,main方法中的逻辑是将Spring-Boot-Classes和Spring-Boot-Lib下的类文件、配置和依赖加载到jvm中,最后通过某种方式(反射)执行com.dongnao.FirstAppByGuiApplication的main方法来启动Springboot应用。以下内容围绕这个思想结合源码来进行分析,首先看一下Main-Class属性配置的JarLauncher源码,main方法中内容可以理解为有一个Jar启动器要启动

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

}

一、首先查看new JarLauncher()代码,由于JarLauncher类的不带参数的构造方法中无任何实现,默认调用父类(ExecutableArchiveLauncher)不带参数的构造方法,如下图所示。

public abstract class ExecutableArchiveLauncher extends Launcher {

	private final Archive archive;

	public ExecutableArchiveLauncher() {
		try {
			this.archive = createArchive();
		}
		catch (Exception ex) {
			throw new IllegalStateException(ex);
		}
	}
}

我们可以发现在构造方法中通过调用createArchive()方法创建了一个Archive对象,那么这个Archive对象指的是什么呢?archive英语翻译过来为“归档文件”,软件研发领域指的是比如我们开发了一个网站、系统、公众号、接口、应用等,这些都是由类文件、配置文件、页面、样式、JS等组成一个整体协作共同完成应用功能,最终以一个文件夹或者jar包的形式提供服务,我们习惯性把这个文件夹称为归档文件(Archive)。每个归档文件下又有若干个小文件,我们称为归档文件的资源(Archive.Entry)。Springboot-loader中提供了关于Archive的实现,同时提供了两个子类JarFileArchive和ExplodedArchive。简单理解下两个子类:JarFileArchive指的是我们打包后形成的jar或者war包,而ExplodedArchive指的是比如把war包部署到服务器,服务器启动后解压缩形成的文件夹这种形式。

此处的Archive对象指的是通过java -jar命令要启动的jar包本身;

题外话:

  1. 现实生活中的“归档文件”指的是文件归档是指立档单位在其职能活动中形成的、办理完毕、应作为文书档案保存的各种纸质文件材料。 遵循文件的形成规律,保持文件之间的有机联系,区分不同价值,便于保管和利用;
  2. 其实关于exploded这个单词在通过IDEA部署web项目时会有下图所示的两个选项:erms-oss:war这种指的是发布模式,即先打包成war包,然后通过IDE的帮助部署war到服务器的webapps下;erms-oss:war exploded指的是以文件夹发布项目,指的是将当前项目编译后的out路径告诉TOMCAT实例,反过来让TOMCAT来找这个项目,并未真正将应用代码部署到服务器中,一般用于开发过程中且支持热启动;

二、接下来看launch方法的如下实现,接下来依次解释每行代码功能:

protected void launch(String[] args) throws Exception {
	JarFile.registerUrlProtocolHandler();
	ClassLoader classLoader = createClassLoader(getClassPathArchives());
	launch(args, getMainClass(), classLoader);
}

1、JarFile.registerUrlProtocolHandler()具体实现代码如下:

/**
 * Register a {@literal 'java.protocol.handler.pkgs'} property so that a
 * {@link URLStreamHandler} will be located to deal with jar URLs.
 */
public static void registerUrlProtocolHandler() {
	String handlers = System.getProperty(PROTOCOL_HANDLER, "");
	System.setProperty(PROTOCOL_HANDLER,
			("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
	resetCachedUrlHandlers();
}

JDK内置了针对URL的关联协议(诸如file、ftp、http、https等)提供了下图所示的UrlStreamHandler实现类,这些实现类都放置在sun.set.www.protocol包中。通过下图我们可以发现所有的实现类均存放在sun.net.www.protocol包下,且命名符合sun.net.www.protocol.协议名称.Handler规律,比如要处理jar协议的类,那么全路径为的类全路径名为sun.net.www.protocol.jar.Handler。通常通过System.setProperty("java.protocol.handler.pkgs","sun.net.www.protocol")代码设置java关联协议处理类(即UrlStreamHandler实现类)所在的包名,如果有多个包,通过"|"分隔。

虽然JDK内置了sun.net.www.protocol.jar.Handler来处理jar协议的连接处理等,但却无法处理spring-boot-maven插件打包的jar文件中/BOOT-INF/lib目录下存在第三方的依赖jar,所以需要Springboot提供UrlStreamHandler的实现类(org.springframework.boot.loader.jar.Handler)来扩展jar协议处理功能,该类存在spring-boot-maven插件打包的jar的org文件夹。

简而言之,Springboot在通过提供jar协议扩展实现类的同时,将实现类所在的包名配置到了Java系统参数java.protocol.handler.pkgs中,大体是这样:System.setProperty("java.protocol.handler.pkgs","org.springframework.boot.loader"),需要注意的是在sun.net.www.protocol和org.springframework.boot.loader包下面都含有jar.Handler类,根据最终结果肯定是采用spring-boot-loader下的,那么是如何实现的呢,我们在URL的getUrlStreamHandler找到了答案:在执行while之前,packagePrefixList=org.springframework.boot.loader|sun.net.www.protocol,while中的逻辑是先用org.springframework.boot.loader.jar.Handler创建Handler实例对象,创建成功后跳出while循环执行后续逻辑,可以看出JDK内置包sun.net.www.protocol作为一个兜底实现。

/**
 * Returns the Stream Handler.
 * @param protocol the protocol to use
 */
static URLStreamHandler getURLStreamHandler(String protocol) {

	URLStreamHandler handler = handlers.get(protocol);
	if (handler == null) {

		boolean checkedWithFactory = false;

		// Use the factory (if any)
		if (factory != null) {
			handler = factory.createURLStreamHandler(protocol);
			checkedWithFactory = true;
		}

		// Try java protocol handler
		if (handler == null) {
			String packagePrefixList = null;

			packagePrefixList
				= java.security.AccessController.doPrivileged(
				new sun.security.action.GetPropertyAction(
					protocolPathProp,""));
			if (packagePrefixList != "") {
				packagePrefixList += "|";
			}

			// REMIND: decide whether to allow the "null" class prefix
			// or not.
			packagePrefixList += "sun.net.www.protocol";

			StringTokenizer packagePrefixIter =
				new StringTokenizer(packagePrefixList, "|");

			while (handler == null &&
				   packagePrefixIter.hasMoreTokens()) {

				String packagePrefix =
				  packagePrefixIter.nextToken().trim();
				try {
					String clsName = packagePrefix + "." + protocol +
					  ".Handler";
					Class<?> cls = null;
					try {
						cls = Class.forName(clsName);
					} catch (ClassNotFoundException e) {
						ClassLoader cl = ClassLoader.getSystemClassLoader();
						if (cl != null) {
							cls = cl.loadClass(clsName);
						}
					}
					if (cls != null) {
						handler  =
						  (URLStreamHandler)cls.newInstance();
					}
				} catch (Exception e) {
					// any number of exceptions can get thrown here
				}
			}
		}

		// 代码已省略...
	}

	return handler;

}

2、ClassLoader classLoader = createClassLoader(getClassPathArchives())做了两件事:

      2.1 getClassPathArchives()简单来说是从归档文件的/BOOT-INF/classes和/BOOT-INF/lib下的依赖jar设置到类路径中,便于启动时加载调用

      2.2 createClassLoader()方法是创建类加载器,需要注意的是这个类加载器会用到步骤1部分提及到的扩展jdk内置关联协议jar的UrlStreamHandler实现类。

3、先看一下如下两个图:launch方法的实现和调用

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

先说结论:设置当前线程上下文的ClassLoader,然后利用这个ClassLoader根据从/META-INF/MANIFEST.MF配置文件中读取配置的Start-Class作为类的全路径名加载Start-Class对应的Class对象,并调用其main方法;

咱们看一下getMainClass()的实现会发现

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

首先设置当前线程上下文的ClassLoader,然后创建一个Main方法Runner,并执行;MainMethodRunner这个类并没有什么特别的,重点看一下run方法

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

总结:MANIFEST.MF中的Main-Class作为引导类,经过一系列准备后最终执行Start-Class类的main方法,具体细节如下

  1. 扩展JDK内置的关联协议的默认实现来满足利用spring-boot-maven插件打包的jar包中含有依赖jar;
  2. 从归档jar包中读取/BOOT-INF下面的/classes和/lib下的归档文件(包括class文件和依赖jar)来构建类路径,并创建能够加载这些归档文件的ClassLoader,创建过程中会用到步骤1中扩展的jar协议自定义实现类(UrlStreamHandler实现类);
  3. 从/META-INF/MANIFEST.MF中读取Start-Class配置,利用反射调用Start-Class配置类的main方法;

以上,完了!

本文地址:https://blog.csdn.net/yu102655/article/details/112687592

《java -jar命令引导启动Springboot项目的那点事.doc》

下载本文的Word格式文档,以方便收藏与打印。