当前位置: 首页 > 科技观察

吃透SpringBootjar的可执行原理

时间:2023-03-14 11:07:56 科技观察

涉及的知识点主要有Maven的生命周期和自定义插件,JDK提供jar包的工具类以及如何扩展Springboot,最后是自定义类加载器。spring-boot-maven-pluginSpringBoot的可执行jar包,又称fatjar,是包含所有第三方依赖的jar包。jar包内嵌了除java虚拟机以外的所有依赖。是一个一体化的jar包。普通插件maven-jar-plugin生成的包和spring-boot-maven-plugin生成的包的直接区别就是在fatjar中增加了两部分。第一部分是lib目录,存放Maven依赖的jar。包文件,第二部分是springbootloader相关的类。fatjar//目录结构├─BOOT-INF│├─classes│└─lib├─META-INF│├─maven│├─app.properties│├─MANIFEST.MF└─org└─springframework└??─boot└─loader├─archive├─data├─jar└─util也就是说,要想知道fatjar是如何生成的,就必须知道spring-boot-maven-plugin的工作机制,而spring-boot-maven-plugin是属于自己定义的插件,所以一定要知道Maven的自定义插件是如何工作的Maven的自定义插件Maven有三套独立的生命周期:clean、default和site,每个生命周期都包含一些阶段阶段,阶段是连续的,后面的阶段依赖于前面的阶段。生命周期的phase阶段与插件完成实际构建任务的目标绑定。org.springframework.bootspring-boot-maven-plugin重新打包repackagetarget会执行到org.springframework.boot.maven.RepackageMojo#execute,这个方法的主要逻辑是调用org.springframework.boot.maven.RepackageMojo#重新包装。privatevoidrepackage()throwsMojoExecutionException{//获取maven-jar-plugin生成的jar,最终名字会加上.orignalArtifactsource=getSourceArtifact();//最终的文件,即FatjarFiletarget=getTargetFile();//获取repackager并重新打包成可执行的jar文件Repackagerrepackager=getRepackager(source.getFile());//在运行时查找并过滤项目依赖的jarSetartifacts=filterDependencies(this.project.getArtifacts(),getFilters(getAdditionalFilters()));//将工件转换为库Librarieslibraries=newArtifactsLibraries(artifacts,this.requiresUnpack,getLog());try{//提供SpringBoot启动脚本LaunchScriptlaunchScript=getLaunchScript();//执行重新打包逻辑生成最终的fatjarrepackager.repackage(target,libraries,launchScript);}catch(IOExceptionex){thrownewMojoExecutionException(ex.getMessage(),ex);}//将source更新到xxx.jar.orignal文件updateArtifact(source,target,repackager.getB备份文件());}我们关心org.springframework.boot.maven.RepackageMojo#getRepackager这个方法,知道了Repackager是如何生成的,我们就可以大致猜出内部的打包逻辑privateRepackagergetRepackager(Filesource){Repackagerrepackager=newRepackager(source,this.layoutFactory);repackager.addMainClassTimeoutWarningListener(newLoggingMainClassTimeoutWarningListener());//设置主类的名称,如果不指定,会查找第一个包含main方法的类,repacke最终会设置org.springframework.boot.loader.JarLauncherrepackager.setMainClass(this.mainClass);if(this.layout!=null){getLog().info("布局:"+this.layout);//关注布局,最后返回org.springframework.boot.loader.tools.Layouts.Jarrepackager.setLayout(this.layout.layout());}返回重新包装器;}/***可执行JAR布局。*/publicstaticclassJarimplementsRepackagingLayout{@OverridepublicStringgetLauncherClassName(){return"org.springframework.boot.loader.JarLauncher";}@OverridepublicStringgetLibraryDestination(StringlibraryName,LibraryScopescope){return"BOOT-INF/lib/";}@OverridepublicStringgetClassesLocation(){返回“”;}@OverridepublicStringgetRepackagedClassesLocation(){return"BOOT-INF/classes/";}@OverridepublicbooleanisExecutable(){返回真;翻译过来就是文件布局,或者目录布局。代码一目了然。同时,我们需要注意它。它也是下一个焦点对象org.springframework.boot.loader.JarLauncher。从名字上看,很可能是返回一个启动类的可执行jar文件MANIFEST.MF文件内容Manifest-Version:1.0Implementation-Title:oneday-auth-serverImplementation-Version:1.0.0-SNAPSHOTArchiver-Version:PlexusArchiverBuilt-By:onedayImplementation-Vendor-Id:com.onedaySpring-Boot-Version:2.1.3.RELEASEMain-Class:org.springframework.boot.loader.JarLauncherStart-Class:com.oneday.auth.ApplicationSpring-Boot-Classes:BOOT-INF/classes/Spring-Boot-Lib:BOOT-INF/lib/Created-By:ApacheMaven3.3.9Build-Jdk:1.8.0_171repackager生成的MANIFEST.MF文件就是以上信息,可以看到两个关键信息Main-Class和入门级。我们可以更进一步,程序的启动入口不是我们SpringBoot中定义的main,而是JarLauncher#main,然后利用反射调用定义的Start-Class的main方法。JarLauncher的关键类引入了java.util.jar.JarFileJDK工具类来读取jar文件。文件入口org.springframework.boot.loader.jar.JarEntrySpringboot-loader继承JDK提供JarEntry类org.springframework.boot.loader.archive.Archive统一访问SpringbootJarFileArchive抽象的资源层jar包文件抽象ExplodedArchive文件目录这里重点描述JarFile的作用,每个JarFileArchive都会对应一个JarFile。构建时会解析内部结构,获取jar包中的各个文件或文件夹类。我们可以看看这个类的注解。/*{@linkjava.util.jar.JarFile}的扩展变体,其行为方式相同,但*提供以下附加功能。*

    *
  • 嵌套的{@linkJarFile}可以基于*任何目录条目{@link#getNestedJarFile(ZipEntry)获得}。
  • *
  • 嵌套的{@linkJarFile}可以对于*嵌入式JAR文件(只要它们的条目未压缩)是{@link#getNestedJarFile(ZipEntry)obtained}。
**/jar中的资源分隔符是!/,即JarFileURLJDK提供的只支持一个'!/',而Springboot扩展了这个协议,让它支持多个'!/',可以表示jar中的jar,目录中的jar,fatjar资源。最基本的自定义类加载机制:BootstrapClassLoader(加载JDK/lib目录下的类)次要基础:ExtensionClassLoader(加载JDK/lib/ext目录下的类)常见:ApplicationClassLoader(程序自身classpath下的类))首先要注意双亲委托机制。如果一个类可以被委托的最基本的ClassLoader加载,那么它就不能被高级的ClassLoader加载。这是为了引入一个不在JDK下但类名相同的类。.第二,如果在这种机制下,fatjar依赖的各种第三方jar文件不在程序自己的classpath中,也就是说,如果我们使用双亲委派机制,我们将无法获取到jar我们完全依赖的包,所以我们需要修改双亲委托机制的类查找方法,自定义类加载机制。先简单介绍一下Springboot2中的LaunchedURLClassLoader,它继承了java.net.URLClassLoader,重写了java.lang.ClassLoader#loadClass(java.lang.String,boolean),然后我们讨论他是如何修改双亲委托机制的。上面我们提到了Springboot支持多个'!/'来表示多个jar,而我们的问题就是如何解决查找这多个jar包的问题。我们来看看LaunchedURLClassLoader的构造方法。publicLaunchedURLClassLoader(URL[]urls,ClassLoaderparent){super(urls,parent);}urls注释解释了从中加载类和资源的url,即fatjar包依赖的所有类和资源,urls参数传递给父类java.net.URLClassLoader,java.net.URLClassLoader。父类的net.URLClassLoader#findClass执行搜索类方法。该类的搜索源是构造方法传入的urls参数。//LaunchedURLClassLoader的实现protectedClassloadClass(Stringname,booleanresolve)throwsClassNotFoundException{Handler.setUseFastConnectionExceptions(true);try{try{//尝试根据类名定义类所在的包,即java.lang。打包,确保jar中jar中匹配的manifest可以关联到关联的//packagedefinePackageIfNecessary(name);}catch(IllegalArgumentExceptionex){//由于具有并行能力而容忍竞争条件if(getPackage(name)==null){//这不应该发生,因为IllegalArgumentException指示//包已经被定义,因此,//getPackage(name)不应返回null。//这里的异常说明definePackageIfNecessary方法的作用其实是预先过滤掉找不到的包thrownewAssertionError("包"+name+"已经被"+"定义但找不到");}}返回super.loadClass(名称,解析);}最后{Handler.setUseFastConnectionExceptions(false);}}方法super.loadClass(name,resolve)实际上会返回java.lang.ClassLoader#loadClass(java.lang.String,boolean),遵循双亲委派机制搜索类,BootstrapClassLoader和ExtensionClassLoader不会能够找到fatjar依赖的类,最终会来到ApplicationClassLoader,调用java.net.URLClassLoader#findClass如何真正启动Springboot2和Springboot1最大的区别是Springboo1会新建一个线程去执行对应的反射调用逻辑,而SpringBoot2去掉了新建线程的步骤该方法是org.springframework.boot.loader.Launcher#launch(java.lang.String[],java.lang.String,java.lang.ClassLoader)。反射调用逻辑比较简单,这里就不分析了,但是重点是,在调用main方法之前,将当前线程的上下文类加载器设置为LaunchedURLClassLoaderprotectedvoidlaunch(String[]args,StringmainClass,ClassLoaderclassLoader)throwsException{Thread.currentThread().setContextClassLoader(classLoader);createMainMethodRunner(mainClass,args,classLoader).run();}Demopublicstaticvoidmain(String[]args)throwsClassNotFoundException,MalformedURLException{JarFile.registerUrlProtocolHandler();//构造LaunchedURLClassLoader类加载器,这里使用2个URL,对应jar包依赖spring-boot-loader和spring-boot包,以“!/”分隔,需要org.springframework.boot.loader。jar.Handler处理器处理LaunchedURLClassLoaderclassLoader=newLaunchedURLClassLoader(newURL[]{newURL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-loader-1.2.3.RELEASE.jar!/"),新URL("jar:file:/E:/IdeaProjects/oneday-auth/oneday-auth-server/target/oneday-auth-server-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-2.1.3.RELEASE.jar!/")},Application.class.getClassLoader());//加载Class//这两个类会在第二步本地查找(URLClassLoader的findClass方法)classLoader.loadClass("org.springframework.boot.loader.JarLauncher");classLoader.loadClass("org.springframework.boot.SpringApplication");//第三步,默认加载顺序在ApplicationClassLoaderclassLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");//SpringApplication.run(Application.class,args);org.springframework.bootspring-boot-加载呃2.1.3.RELEASEorg.springframework.bootspring-boot-maven-plugin2.1.3.RELEASE总结对于源码分析,这次最大的收获是不能一下子去追寻和理解源码中每一步的逻辑,即使我知道方法我们需要了解的是关键代码和涉及的知识点。我从Maven的自定义插件开始跟踪,巩固了对Maven的认识。在这个过程中,我什至了解到JDK提供了相应的jar读取工具类。最后一个也是最重要的知识点是自定义类加载器。整篇代码不是说代码有多好,而是学习为什么好。