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包本身;
题外话:
- 现实生活中的“归档文件”指的是文件归档是指立档单位在其职能活动中形成的、办理完毕、应作为文书档案保存的各种纸质文件材料。 遵循文件的形成规律,保持文件之间的有机联系,区分不同价值,便于保管和利用;
- 其实关于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方法,具体细节如下
- 扩展JDK内置的关联协议的默认实现来满足利用spring-boot-maven插件打包的jar包中含有依赖jar;
- 从归档jar包中读取/BOOT-INF下面的/classes和/lib下的归档文件(包括class文件和依赖jar)来构建类路径,并创建能够加载这些归档文件的ClassLoader,创建过程中会用到步骤1中扩展的jar协议自定义实现类(UrlStreamHandler实现类);
- 从/META-INF/MANIFEST.MF中读取Start-Class配置,利用反射调用Start-Class配置类的main方法;
以上,完了!
以上是关于java -jar命令引导启动Springboot项目的那点事的主要内容,如果未能解决你的问题,请参考以下文章