前言: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.jarorg.springframework.boot.loader.JarLauncher方式。在spring-boot-maven插件repackage(goal)的那些事这篇博客中简单介绍了采用spring-boot-maven插件打包Springboot应用后的jar包的组成结构,下面通过下图所示的META-INF/MANIFEST.MF内容来分析下Springboot应用启动的那些事,以下MANIFEST.MF文件的属性顺序进行了少许调整,需要说明的是红框以外的内容阅读下即可,重点关注红框部分内容;
大胆猜测下:执行java -jarfirst-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);}@Overrideprotected 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包中。通过下图我们可以发现所有的实现类均存放在.www.protocol包下,且命名符合.www.protocol.协议名称.Handler规律,比如要处理jar协议的类,那么全路径为的类全路径名为.www.protocol.jar.Handler。通常通过System.setProperty("java.protocol.handler.pkgs",".www.protocol")代码设置java关联协议处理类(即UrlStreamHandler实现类)所在的包名,如果有多个包,通过"|"分隔。
虽然JDK内置了.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"),需要注意的是在.www.protocol和org.springframework.boot.loader包下面都含有jar.Handler类,根据最终结果肯定是采用spring-boot-loader下的,那么是如何实现的呢,我们在URL的getUrlStreamHandler找到了答案:在执行while之前,packagePrefixList=org.springframework.boot.loader|.www.protocol,while中的逻辑是先用org.springframework.boot.loader.jar.Handler创建Handler实例对象,创建成功后跳出while循环执行后续逻辑,可以看出JDK内置包.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 handlerif (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 += ".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.2createClassLoader()方法是创建类加载器,需要注意的是这个类加载器会用到步骤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项目的那点事》对你有帮助,请点赞、收藏,并留下你的观点哦!