一、概述 SpringBootFatJar的设计,打破了标准jar的结构,在jar包内携带了其所依赖的jar包,通过jar中的main方法创建自己的类加载器,来识别加载运行其不规范的目录下的代码和依赖。二、标准的jar包结构 打开Java的Jar文件我们经常可以看到文件中包含着一个METAINF目录,这个目录下会有一些文件,其中必有一个MANIFEST。MF,这个文件描述了该Jar文件的很多信息其中MainClass定义Jar文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过javajarxxx。jar来运行该jar文件。 在生产环境是使用javajarxxx。jar的方式来运行SpringBoot程序。这种情况下,SpringBoot应用真实的启动类并不是我们所定义的带有main方法的类,而是JarLauncher类。查看SpringBoot所打成的FatJar,其MainClass是org。springframework。boot。loader。JarLauncher,这便是微妙之处。SpringBootVersion:2。1。3。RELEASEMainClass:org。springframework。boot。loader。JarLauncherStartClass:com。rock。springbootlearn。SpringbootLearnApplicationSpringBootClasses:BOOTINFclassesSpringBootLib:BOOTINFlibBuildJdk:1。8。0131复制代码 JAR包中的MANIFEST。MF文件详解以及编写规范三、探索JarLauncher org。springframework。boot。loader。JarLauncher这个类是哪里来的呢?答案在springbootloader。jar包中,可找到这个JarLauncher类的源码。在项目中加入maven依赖,以便查看源码和远程调试。dependencygroupIdorg。springframework。bootgroupIdspringbootloaderartifactIddependency复制代码 认真比较可以看出,这个springbootloader包中的内容与SpringBoot的FatJar包中的一部分内容几乎一样。JarLauncher在jar中的位置如下: 3。1只能拷贝出来一份儿 重点重点重点:因jar规范要求MainClass所指定的类必须位于jar包的顶层目录下,即org。springframework。boot。loader。JarLauncher这个org必须位于jar包中的第一级目录,不能放置在其他的目录下。所以所以所以只能将springbootloader这个jar包的内容拷贝出来,而不是整个jar直接放置于执行Jar中。3。2携带程序所依赖的jar而非仅class 上边JarLauncher的这个org。springframework。xx以及METAINF这两个目录是符合jar包规范的。但是BOOTINF这个目录里边有点像我们开发中的一些用法:依赖jar包在lib目录下但按照jar包规范jar中不能有jar包的情况下程序。class文件在classes目录下但xxx。class文件应该按照org。springframework。xx这样放置在jar中的根目录中 所以classes和lib你也能意识到,这个设计是独特的。早期jar包内携带依赖是采用如mavenshadeplugin的做法,把依赖的class文件拷贝到目标jar中,但也会造成重名(全限定名)的类会出现覆盖的情况。后来SpringBoot为了避免覆盖的情况,修改了打包机制,放弃了mavenshadeplugin那种拷贝class的方式,调整为依赖原始jar包;这同时意味着改变了Jar标准的运行机制,那么要想让classes和lib中代码能够正常运行,你试想一下如果没有自定义的classLoader来加载这些类文件,可以嘛?四、自定义类加载器的运行机制 自定义类加载器的常规处理:指定资源指定委托关系指定线程上下文类加载器调用逻辑入口方法4。1指定资源 构造方法中基于jar包的文件系统信息,构造Archive对象publicExecutableArchiveLauncher(){this。archivecreateArchive();}protectedfinalArchivecreateArchive()throwsException{ProtectionDomainprotectionDomaingetClass()。getProtectionDomain();CodeSourcecodeSourceprotectionDomain。getCodeSource();URIlocation(codeSource!null)?codeSource。getLocation()。toURI():Stringpath(location!null)?location。getSchemeSpecificPart():if(pathnull){thrownewIllegalStateException(Unabletodeterminecodesourcearchive);}FilerootnewFile(path);if(!root。exists()){thrownewIllegalStateException(Unabletodeterminecodesourcearchivefromroot);}return(root。isDirectory()?newExplodedArchive(root):newJarFileArchive(root));}复制代码 采集jar包中的classes和lib目录下的归档文件。后边创建ClassLoader的时候作为参数传入OverrideprotectedListgetClassPathArchives()throwsException{ListarchivesnewArrayList(this。archive。getNestedArchives(this::isNestedArchive));postProcessClassPathArchives(archives);}protectedbooleanisNestedArchive(Archive。Entryentry){if(entry。isDirectory()){returnentry。getName()。equals(BOOTINFCLASSES);}returnentry。getName()。startsWith(BOOTINFLIB);}复制代码publicstaticvoidmain(String〔〕args)throwsException{newJarLauncher()。launch(args);}复制代码4。2创建自定义ClassLoaderprotectedvoidlaunch(String〔〕args)throwsException{JarFile。registerUrlProtocolHandler();创建类加载器,并指定归档文件ClassLoaderclassLoadercreateClassLoader(getClassPathArchives());launch(args,getMainClass(),classLoader);}创建类加载器,将归档文件转换为URLprotectedClassLoadercreateClassLoader(Listarchives)throwsException{ListURLurlsnewArrayList(archives。size());for(Archivearchive:archives){urls。add(archive。getUrl());}returncreateClassLoader(urls。toArray(newURL〔0〕));}父加载器是AppClassLoaderprotectedClassLoadercreateClassLoader(URL〔〕urls)throwsException{getClass()。getClassLoader()是系统类加载器,因为默认情况下main方法所在类是由SystemClassLoader加载的,默认情况下是AppClassLoader。returnnewLaunchedURLClassLoader(urls,getClass()。getClassLoader());}复制代码4。3设置线程上下文类加载器,调用程序中的mainclasspublicstaticvoidmain(String〔〕args)throwsException{newJarLauncher()。launch(args);}protectedvoidlaunch(String〔〕args,StringmainClass,ClassLoaderclassLoader)throwsException{设置线程上下文类加载器Thread。currentThread()。setContextClassLoader(classLoader);调用MANIFEST。MF中配置的StartClass:xxx的main方法,还带入了参数createMainMethodRunner(mainClass,args,classLoader)。run();}复制代码