Spring Boot的可执行jar包又称作“fat jar”,是包含所有三方依赖的jar。它与传统jar包最大的不同是包含了一个lib目录和内嵌了web容器。
通过maven命令打包后,会有2个jar包,一个为application-name.version-SNAPSHOT.jar,一个为application-name.version-SNAPSHOT.jar.original。后者仅包含应用编译后的本地资源,而前者引入了相关的第三方依赖。
将前者解压后的目录结构如下:
该目录比使用传统jar命令打包结构更复杂一些,目录含义如下:
BOOT-INF/classes:目录存放应用编译后的class文件。BOOT-INF/lib:目录存放应用依赖的第三方JAR包文件。META-INF:目录存放应用打包信息(Maven坐标、pom文件)和MANIFEST.MF文件。org:目录存放SpringBoot相关class文件。MANIFEST.MF文件位于jar包的META-INF文件夹内,内容如下:
Manifest-Version: 1.0 Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Archiver-Version: Plexus Archiver Built-By: cindy Start-Class: com.shinemo.wangge.web.MainApplication Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Version: 2.3.3.RELEASE Created-By: Apache Maven 3.6.3 Build-Jdk: 1.8.0_212 Main-Class: org.springframework.boot.loader.JarLauncherMain-Class这个属性对应的class的main方法是作为程序入口启动应用的。
Start-Class这个属性定义了我们项目的启动类。
当使用java -jar命令执行Spring Boot应用的可执行jar文件时,该命令引导标准可执行的jar文件,读取在jar中META-INF/MANIFEST.MF文件的Main-Class属性值,该值代表应用程序执行入口类也就是包含main方法的类。
从MANIFEST.MF文件内容可以看到,Main-Class这个属性定义了org.springframework.boot.loader.JarLauncher,JarLauncher就是对应Jar文件的启动器。而我们项目的启动类MainApplication定义在Start-Class属性中,
JarLauncher会将BOOT-INF/classes下的类文件和BOOT-INF/lib下依赖的jar加入到classpath下,然后调用META-INF/MANIFEST.MF文件Start-Class属性完成应用程序的启动。
Launcher的继承关系如下:
JarLauncher默认构造函数实现为空,它父类ExecutableArchiveLauncher会调用再上一级父类Launcher的createArchive方法加载jar包, 加载了jar包之后,我们就能获取到里面所有的资源。
//JarLauncher.java //JarLauncher默认构造函数 public JarLauncher() { } //ExecutableArchiveLauncher.java public ExecutableArchiveLauncher() { try { //开始加载jar包 this.archive = createArchive(); } catch (Exception ex) { throw new IllegalStateException(ex); } } //Launcher.java protected final Archive createArchive() throws Exception { //通过获取当前Class类的信息,查找到当前归档文件的路径 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); } //如果是目录,则创建ExplodedArchive,否则创建JarFileArchive return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root)); }launch方法实际上是调用父类Launcher的launch方法
// Launcher.java protected void launch(String[] args) throws Exception { //注册 Spring Boot 自定义的 URLStreamHandler 实现类,用于 jar 包的加载读取, 可读取到内嵌的jar包 JarFile.registerUrlProtocolHandler(); //创建自定义的 ClassLoader 实现类,用于从 jar 包中加载类。 ClassLoader classLoader = createClassLoader(getClassPathArchives()); //执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。 launch(args, getMainClass(), classLoader); }简单来说,就是创建一个可以读取 jar 包中类的加载器,保证 BOOT-INF/lib 目录下的类和 BOOT-classes 内嵌的 jar 中的类能够被正常加载到,之后执行 Spring Boot 应用的启动。
该方法的目的就是通过将 org.springframework.boot.loader 包设置到 "java.protocol.handler.pkgs" 环境变量,从而使用到自定义的 URLStreamHandler 实现类 Handler,处理 jar: 协议的 URL。
利用java url协议实现扩展原理,自定义jar协议 将org.springframework.boot.loader包 追加到java系统 属性java.protocol.handler.pkgs中,实现自定义jar协议 java会在java.protocol.handler.pkgs系统属性指定的包中查找与协议同名的子包和名为Handler的类, 即负责处理当前协议的URLStreamHandler实现类必须在 <包名>.<协议名定义的包> 中,并且类名称必须为Handler 例如: org.springframework.boot.loader.jar.Handler这个类 将用于处理jar协议 这个jar协议实现作用: 默认情况下,JDK提供的ClassLoader只能识别jar中的class文件以及加载classpath下的其他jar包中的class文件。 对于jar包中的jar包是无法加载的 所以spring boot 自己定义了一套URLStreamHandler实现类和JarURLConnection实现类,用来加载jar包中的jar包的class类文件举个例子:
jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/ jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class我们看到如果有 jar 包中包含 jar,或者 jar 包中包含 jar 包里面的 class 文件,那么会使用 !/分隔开,这种方式只有 org.springframework.boot.loader.jar.Handler 能处理,它是 SpringBoot 内部扩展出来一种URL协议.
通常,jar里的资源分隔符是!/,在JDK提供的JarFile URL只支持一层“!/”,而Spring Boot扩展了该协议,可支持多层“!/”。
<1> 处,this::isNestedArchive 代码段,创建了 EntryFilter 匿名实现类,用于过滤 jar 包不需要的目录。目的就是过滤获得,BOOT-INF/classes/ 目录下的类,以及 BOOT-INF/lib/ 的内嵌 jar 包。
// JarLauncher.java static final String BOOT_INF_CLASSES = "BOOT-INF/classes/"; static final String BOOT_INF_LIB = "BOOT-INF/lib/"; @Override protected boolean isNestedArchive(Archive.Entry entry) { // 如果是目录的情况,只要 BOOT-INF/classes/ 目录 if (entry.isDirectory()) { return entry.getName().equals(BOOT_INF_CLASSES); } // 如果是文件的情况,只要 BOOT-INF/lib/ 目录下的 `jar` 包 return entry.getName().startsWith(BOOT_INF_LIB); }<1>处getNestedArchives()方法实现
//JarFileArchive.java @Override public List<Archive> getNestedArchives(EntryFilter filter) throws IOException { List<Archive> nestedArchives = new ArrayList<>(); for (Entry entry : this) { if (filter.matches(entry)) { nestedArchives.add(getNestedArchive(entry)); } } return Collections.unmodifiableList(nestedArchives); }基于获得的 Archive 数组,创建自定义 ClassLoader 实现类 LaunchedURLClassLoader,通过它来加载 BOOT-INF/classes 目录下的类,以及 BOOT-INF/lib 目录下的 jar 包中的类。
<1> 处:设置 LaunchedURLClassLoader 作为类加载器,从而保证能够从 jar 包中加载到相应的类。
从 jar 包的 MANIFEST.MF 文件的 Start-Class 配置项,,获得我们设置的 Spring Boot 的主启动类。
该方法负责最终的 Spring Boot 应用真正的启动。
LaunchedURLClassLoader 是 spring-boot-loader 项目自定义的类加载器,实现对 jar 包中 META-INF/classes 目录下的类和 META-INF/lib 内嵌的 jar 包中的类的加载。
该ClassLoader继承自UrlClassLoader。UrlClassLoader加载class就是依靠初始参数传入的Url数组,并且尝试从Url指向的资源中加载Class文件
//LaunchedURLClassLoader.java protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Handler.setUseFastConnectionExceptions(true); try { try { //尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar里匹配的manifest能够和关联的package关联起来 definePackageIfNecessary(name); } catch (IllegalArgumentException ex) { // Tolerate race condition due to being parallel capable if (getPackage(name) == null) { // This should never happen as the IllegalArgumentException indicates // that the package has already been defined and, therefore, // getPackage(name) should not return null. //这里异常表明,definePackageIfNecessary方法的作用实际上是预先过滤掉查找不到的包 throw new AssertionError("Package " + name + " has already been " + "defined but it could not be found"); } } return super.loadClass(name, resolve); } finally { Handler.setUseFastConnectionExceptions(false); } }方法super.loadClass(name, resolve)实际上会回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循双亲委派机制进行查找类,而Bootstrap ClassLoader和Extension ClassLoader将会查找不到fat jar依赖的类,最终会来到Application ClassLoader,调用java.net.URLClassLoader#findClass
因为SpringBoot实现了Jar包的嵌套,一个Jar包就可以完成整个程序的运行。
引入自定义类加载器就是为了能解决jar包嵌套jar包的问题,系统自带的AppClassLoarder不支持读取嵌套jar包
因为程序毕竟要有一个启动入口,这个入口要由应用类加载器加载,先将SpringBoot Class Loader加载到内存中,然后通过后续的一些操作创建线程上下文加载器,去加载第三方jar。
如果将SpringBoot Class Loader 也放到lib文件下,是根本无法被加载到的,因为它根本不符合jar文件的一个标准规范
