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

Posted 于大圣

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java -jar命令引导启动Springboot项目的那点事相关的知识,希望对你有一定的参考价值。

前言: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方法;

以上,完了!

以上是关于java -jar命令引导启动Springboot项目的那点事的主要内容,如果未能解决你的问题,请参考以下文章

Java 布尔运算

java [Java] Java常用代码#java

Java - 35 Java 实例

Java While 循环

Java 字符串

Java If ... Else