欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-06-23 10:46:06
前言:java -jar命令并不是专门用于启动Springboot项目的,而是Java官方用于运行jar包中某个字节码文件的通用命令。比如通过java -jar XXX.jar来运行应用时,如不做特殊设置就要求在jar文件中必须包含META-INF/MANIFEST.MF文件,且通过类似Main-Class: org.springframework.boot.loader.JarLauncher来指定需要运行的字节码类文件全路径名,有点类似jre中的java-cp XXX.jarorg.springf......

前言: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命令引导启动Springboot项目的那点事

大胆猜测下:执行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来找这个项目,并未真正将应用代码部署到服务器中,一般用于开发过程中且支持热启动;

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

二、接下来看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实现类)所在的包名,如果有多个包,通过"|"分隔。

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

虽然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方法的实现和调用

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

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