spring-boot-loader模块使得springboot应用具备打包为可执行jar或war文件的能力。只需要引入Maven插件或者Gradle插件就可以自动生成。
Java中并没有标准的方法加载嵌入式的jar文件,通常都是在一个jar文件中。这种情况下,如果你要通过命令行的形式发布一个没有打包的独立程序的话,可能会出现问题。
为了解决这种问题,很多人员使用”shaded jars”方式,即将所有的class文件都打包在一个jar包里面,也就是通常所有的”uberjar”。这种方式下,开发人员很难去判断哪个依赖的文件库是被程序真正使用到的。更普遍的问题是,在不同的jar文件中,如果有相同名称的文件则会冲突。spring boot采用了一种不同的方式,让我们可以直接从命令行启动jar。这也就是spring-boot-loader模块提供的功能。
这里补充一点,如果你对jar文件或者Manifest不是很清楚的话,可以看这篇文章.java 打包技术之jar文件
这里说明一下,在传统的可执行jar文件中会有/META-INF/MANIFEST.MF文件,这里主要介绍两个属性:Main-Class和classpath,Main-Class是可执行jar的启动类,classpath则可以指定依赖的类库。
SpringBoot loader插件提供的可执行文件结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 example.jar | +-META-INF (1 ) | +-MANIFEST.MF +-org (2 ) | +-springframework | +-boot | +-loader | +-<spring boot loader classes> +-BOOT-INF +-classes (3 ) | +-mycompany | +-project | +-YourClasses.class +-lib (4 ) | +-dependency1.jar | +-dependency2.jar | ...........
META-INF :Jar文件MANIFEST.MF文件存放处
org.springframework.boot.loader : springboot-loader启动应用class存放处
BOOT-INF/classes : 应用本身文件存放处
BOOT-INF/lib :应用需要的依赖存放处
MANIFEST.MF 文件内容 1 2 3 4 5 6 7 8 9 10 Manifest-Version: 1.0 Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Built-By: zhangke Start-Class: club.fengxiu.App Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Version: 2.3 .0 .RELEASE Created-By: Apache Maven 3.6 .3 Build-Jdk: 1.8 .0_251 Main-Class: org.springframework.boot.loader.JarLauncher
从中可以看得到,它的Main-Class是org.springframework.boot.loader.JarLauncher,即当使用java -jar
执行jar包的时候会调用JarLunch的main方法,而不是调用应用本身定义的SpringApplication注解的类。
从这里应该可以猜测出,Springboot-Loader模块打包出的jar具备可执行能力跟这个类有很大的关系。它是SpringBoot定义的一个工具类,用于执行应用定义的SpringApplication类。相当于SpringBoot Loader提供了一套标准用于执行SpringBoot打包出来的jar。
JarLuncher的执行流程 SpringBoot loader模块类简介 由于下面会多次涉及到一些类,
JarLauncher#main 1 2 3 public static void main (String[] args) throws Exception { new JarLauncher ().launch(args); }
这个方法比较简单,构造JarLuncher,然后调用launch方法,并将控制台的参数传进去。这个是默认的构造函数,因此这个类在创建的时候,同时会调用父类的构造函数,也就是ExecutableArchiveLauncher的默认构造函数,ExecutableArchiveLauncher#ExecutableArchiveLauncher()代码如下
1 2 3 4 5 6 7 8 public ExecutableArchiveLauncher () { try { this .archive = createArchive(); } catch (Exception ex) { throw new IllegalStateException (ex); } }
可以看出,这里会调用createArchive()方法,这个方法主要是用来创建Archive,这个类是SpringBoot-loader定义的归档文件基础抽象类。具体的实现有俩个,JarFileArchive和ExplodedArchive。JarFileArchive是用来对Jar包文件的抽象,主要用来获取Jar包中的各种文件或者信息,主要实现是通过JarFile类,其实也就是JarFile的一个装饰器。ExplodedArchive是文件目录的抽象。
JarFile:对jar包的封装,每个JarFileArchive都会对应一个JarFile。JarFile被构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹,这些文件或文件夹会被封装到Entry中,也存储在JarFileArchive中。如果Entry是个jar,会解析成JarFileArchive。注意这里的JarFile是对java默认类java.util.jar.JarFile的重新定义。
有了以上知识,下面就就可以来看createArchive方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected final Archive createArchive () throws Exception { ProtectionDomain protectionDomain = getClass().getProtectionDomain(); CodeSource codeSource = protectionDomain.getCodeSource(); URI location = (codeSource != null ) ? codeSource.getLocation().toURI() : null ; String path = (location != null ) ? location.getSchemeSpecificPart() : null ; if (path == null ) { throw new IllegalStateException ("Unable to determine code source archive" ); } File root = new File (path); if (!root.exists()) { throw new IllegalStateException ("Unable to determine code source archive from " + root); } return (root.isDirectory() ? new ExplodedArchive (root) : new JarFileArchive (root)); }
Launcher#launch(java.lang.String[]) 1 2 3 4 5 6 7 8 protected void launch (String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); ClassLoader classLoader = createClassLoader(getClassPathArchives()); launch(args, getMainClass(), classLoader); }
这个方法主要分为三步,下面分别介绍每一步
JarFile.registerUrlProtocolHandler() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static void registerUrlProtocolHandler () { String handlers = System.getProperty(PROTOCOL_HANDLER, "" ); System.setProperty(PROTOCOL_HANDLER, ("" .equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE)); resetCachedUrlHandlers(); } private static void resetCachedUrlHandlers () { try { URL.setURLStreamHandlerFactory(null ); } catch (Error ex) { } }
查看系统是否注册了指定的URL处理器,如果没有则使用org.springframework.boot.loader.jar.Handler自定义的。这里具体的操作可以看
createClassLoader(getClassPathArchives()) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 protected boolean isNestedArchive (Archive.Entry entry) { if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } return entry.getName().startsWith(BOOT_INF_LIB); } protected List<Archive> getClassPathArchives () throws Exception { List<Archive> archives = new ArrayList <>(this .archive.getNestedArchives(this ::isNestedArchive)); postProcessClassPathArchives(archives); return archives; } protected ClassLoader createClassLoader (List<Archive> archives) throws Exception { List<URL> urls = new ArrayList <>(archives.size()); for (Archive archive : archives) { urls.add(archive.getUrl()); } return createClassLoader(urls.toArray(new URL [0 ])); } protected ClassLoader createClassLoader (URL[] urls) throws Exception { return new LaunchedURLClassLoader (urls, getClass().getClassLoader()); }
Launcher#launch(args, getMainClass(), classLoader) 这一步主要是获取MainClass,然后启动应用
JarArchive的getMainClass方法,主要是通过MANIFEST.MF文件获取对应Start-Class对应的值
1 2 3 4 5 6 7 8 9 10 11 12 @Override 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; }
1 2 3 4 5 6 7 8 9 protected void launch (String[] args, String mainClass, ClassLoader classLoader) throws Exception { Thread.currentThread().setContextClassLoader(classLoader); createMainMethodRunner(mainClass, args, classLoader).run(); } protected MainMethodRunner createMainMethodRunner (String mainClass, String[] args, ClassLoader classLoader) { return new MainMethodRunner (mainClass, args); }
MainMethodRunner的run方法
1 2 3 4 5 6 7 8 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 }); }
到这一步,真正执行的应用对应的类。
LaunchedURLClassLoader 这个是在Springboot-loader中使用的ClassLoader,这个类重写了LoadClass这个方法,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Handler.setUseFastConnectionExceptions(true ); try { try { definePackageIfNecessary(name); } catch (IllegalArgumentException ex) { if (getPackage(name) == null ) { throw new AssertionError ("Package " + name + " has already been defined but it could not be found" ); } } return super .loadClass(name, resolve); } finally { Handler.setUseFastConnectionExceptions(false ); } }
从上面可以看出,LaunchedURLClassLoader加载class,用的是UrlClassLoader中的loadClass,但是这里的definePackageIfNecessary目前我还没有搞懂。
总结 Spring-boot Laoder定义了一套可执行Jar的标准规则,然后使用JarLunch或者WarLunch来启动,这俩个是最常用的,流程基本上类似。Jar包的URL路径使用自定义的规则并且这个规则需要使用org.springframework.boot.loader.jar.Handler处理器处理。