当前位置: 首页 > 后端技术 > Java

面试官:说说为什么SpringBoot可以使用Jar包启动?

时间:2023-04-02 10:13:50 Java

很多初学者会很疑惑SpringBoot是如何将应用代码和所有依赖打包成一个单独的Jar包的,因为传统的Java项目打包成Jar包后,需要通过-classpath属性指定依赖,才能运行。今天就来分析讲解一下SpringBoot的启动原理。SpringBoot打包插件SpringBoot提供了一个maven项目打包插件spring-boot-maven-plugin,如下:/artifactId>可以方便的将SpringBoot项目打包成jar包。这样一来,我们就不需要再部署Tomcat、Jetty等Web服务器容器了。我们先来看看SpringBoot的打包结构是什么样子的。打开target目录,发现有两个jar包:其中springboot-0.0.1-SNAPSHOT.jar是通过SpringBoot提供的打包插件以新的格式打包的。它变成了一个FatJar,里面包含了所有的依赖;而springboot-0.0.1-SNAPSHOT.jar.original是Java原生打包方式生成的,只包含项目本身的内容。SpringBootFatJar的组织结构我们将SpringBoot的可执行Jar展开后的结构如下:BOOT-INF目录:包含我们的项目代码(classes目录)和需要的依赖(lib目录);META-INF目录:通过MANIFEST.MF文件提供Jar包的元数据,声明jar的启动类;org.springframework.boot.loader:SpringBoot的loader代码,实现了JarinJar加载的神奇源码。我们看到,如果去掉BOOT-INF目录,这将是一个很普通的标准Jar包,包括meta信息和可执行代码部分,其/META-INF/MAINFEST.MF指定了Jar包的启动meta信息,org.springframework.boot.loader执行相应的逻辑操作。MAINFEST.MF的元信息内容如下:Manifest-Version:1.0Spring-Boot-Classpath-Index:BOOT-INF/classpath.idxImplementation-Title:springbootImplementation-Version:0.0.1-SNAPSHOTSpring-Boot-Layers-Index:BOOT-INF/layers.idxStart-Class:com.listenvision.SpringbootApplicationSpring-Boot-Classes:BOOT-INF/classes/Spring-Boot-Lib:BOOT-INF/lib/Build-Jdk-Spec:1.8Spring-Boot-Version:2.5.6Created-By:MavenJarPlugin3.2.0Main-Class:org.springframework.boot.loader.JarLauncher相当于一个Properties配置文件,每一行是一个配置项。我们重点关注两个配置项:Main-Class配置项:Java指定的jar包的启动类,设置为spring-boot-loader项目的JarLauncher类,用于启动SpringBoot应用。Start-Class配置项:SpringBoot指定的主要启动类,这里设置为我们定义的Application类。Spring-Boot-Classes配置项:指定加载应用类的入口点。Spring-Boot-Lib配置项:指定加载应用所依赖的库。启动原理SpringBoot的启动原理如下图所示:源码分析JarLauncherJarLauncher类是SpringBootjar包的启动类。完整的类图如下:WarLauncher类是SpringBootwar包的启动类。启动类org.springframework.boot.loader.JarLauncher并不是引入到项目中,而是通过spring-boot-maven-plugin插件重新打包添加的。接下来我们先来看下JarLauncher的源代码,比较简单,如下图所示:publicclassJarLauncherextendsExecutableArchiveLauncher{privatestaticfinalStringDEFAULT_CLASSPATH_INDEX_LOCATION="BOOT-INF/classpath.idx";staticfinalEntryFilterNESTED_ARCHIVE_ENTRY_FILTER=(entry)->{if(entry.isDirectory()){returnentry.getName().equals("BOOT-INF/classes/");}}returnentry.getName().startsWith("BOOT-INF/lib/");};publicJarLauncher(){}protectedJarLauncher(Archivearchive){super(archive);}@OverrideprotectedClassPathIndexFilegetClassPathIndex(Archivearchive)throwsIOException{//只有展开的存档需要,常规的已经有定义的顺序if(archiveinstanceofExplodedArchive){Stringlocation=getClassPathIndexFileLocation(archive);返回ClassPathIndexFile.loadIfPossible(archive.getUrl(),地点);}返回super.getClassPathIndex(存档);}privateStringgetClassPathIndexFileLocation(Archivearchive)抛出IOException{Manifestmanifest=archive.getManifest();属性attributes=(manifest!=null)?清单.getMainAttributes():空;字符串位置=(属性!=空)?attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE):空;返回(位置!=null)?位置:DEFAULT_CLASSPATH_INDEX_LOCATION;}@OverrideprotectedbooleanisPostProcessingClassPathArchives(){returnfalse;}@OverrideprotectedbooleanisSearchCandidate(Archive.Entryentry){returnentry.getName().startsWith("BOOT-INF/");}@OverrideprotectedbooleanisNestedArchive(Archive.Entryentry){returnNESTED_ARCHIVE_ENTRY_FILTER.matches(entry);}publicstaticvoidmain(String[]args)抛出异常{//调用基类Launcher定义的launch方法newJarLauncher().launch(args);}}主要依赖于它的main方法,调用了基类Launcher定义的launch方法,而Launcher是ExecutableArchiveLauncher的父类下面我们来看看Launcher类源代码:LauncherpublicabstractclassLauncher{privatestaticfinalStringJAR_MODE_LAUNCHER="org.springframework.boot.loader.jarmode.JarModeLauncher";protectedvoidlaunch(String[]args)throwsException{if(!isExploded()){JarFile.registerUrlProtocolHandler();}ClassLoaderclassLoader=createClassLoader(getClassPathArchivesIterator());StringjarMode=System.getProperty("jarmode");字符串launchClass=(jarMode!=null&&!jarMode.isEmpty())?JAR_MODE_LAUNCHER:getMainClass();启动(参数,启动类,类加载器);}@DeprecatedprotectedClassLoadercreateClassLoader(Listarchives)throwsException{returncreateClassLoader(archives.iterator());}受保护的ClassLoadercreateClassLoader(Iteratorarchives)throwsException{Listurls=newArrayList<>(50);while(archives.hasNext()){urls.add(archives.next().getUrl());}returncreateClassLoader(urls.toArray(newURL[0]));}protectedClassLoadercreateClassLoader(URL[]urls)throwsException{returnnewLaunchedURLClassLoader(isExploded(),getArchive(),urls,getClass().getClassLoader());}protectedvoidlaunch(String[]args,StringlaunchClass,ClassLoaderclassLoader)throwsException{Thread.currentThread().setContextClassLoader(classLoader);createMainMethodRunner(launchClass,args,classLoader).run();}protectedMainMethodRunnercreateMainMethodRunner(StringmainClass,String[]args,ClassLoaderclassLoader){returnnewMainMethodRunner(mainClass,args);}受保护的抽象字符串getMainClass()抛出异常;protectedIteratorgetClassPathArchivesIterator()throwsException{返回getClassPathAr韭菜()。迭代器();}@DeprecatedprotectedListgetClassPathArchives()throwsException{thrownewIllegalStateException("意外调用getClassPathArchives()");}protectedfinalArchivecreateArchive()抛出异常{ProtectionDomainprotectionDomain=getClass().getProtectionDomain();代码源codeSource=protectionDomain.getCodeSource();URI位置=(codeSource!=null)?codeSource.getLocation().toURI():空;字符串路径=(位置!=空)?location.getSchemeSpecificPart():null;if(path==null){thrownewIllegalStateException("无法确定代码源存档");}文件根目录=新文件(路径);if(!root.exists()){thrownewIllegalStateException("无法从"+root中确定代码源存档);}返回(root.isDirectory()?新爆炸dArchive(根):新的JarFileArchive(根));}protectedbooleanisExploded(){returnfalse;}protectedArchivegetArchive(){返回空值;}}launch方法会先创建一个类加载器,然后判断jar是否在MANIFEST文件中,如果在.MF文件中设置了jarmode属性,如果没有设置,从getMainClass()返回launchClass的值,它由PropertiesLauncher子类实现,并返回MANIFEST.MF中配置的Start-Class属性的值。调用createMainMethodRunner方法构建一个MainMethodRunner对象并调用其run方法。PropertiesLauncher@OverrideprotectedStringgetMainClass()throwsException{//加载jar包目标目录下MANIFEST.MF文件中的Start-Class配置,找到springboot的启动类StringmainClass=getProperty(MAIN,"Start-班级”);if(mainClass==null){thrownewIllegalStateException("No'"+MAIN+"'or'Start-Class'specified");}returnmainClass;}MainMethodRunner是目标类的main方法的执行者。此时mainClassName被赋值为MANIFEST.MF中配置的Start-Class属性值为com.listenvision.SpringbootApplication,然后通过反射执行SpringbootApplication的main方法,达到启动SpringBoot的效果。公共类MainMethodRunner{privatefinalStringmainClassName;私有最终字符串[]参数;publicMainMethodRunner(StringmainClass,String[]args){this.mainClassName=mainClass;this.args=(args!=null)?args.clone():空;}publicvoidrun()throwsException{ClassmainClass=Class.forName(this.mainClassName,false,Thread.currentThread().getContextClassLoader());方法mainMethod=mainClass.getDeclaredMethod("main",String[].class);mainMethod.setAccessible(true);mainMethod.invoke(null,newObject[]{this.args});}}总结jar包类似于zip压缩文件,但是zip文件多了一个META-INF/MANIFEST.MF文件,在构建jar包的时候会自动创建。SpringBoot提供了一个插件spring-boot-maven-plugin,用于将程序打包成可执行的jar包。使用java-jar启动SpringBootjar包。第一个调用的入口类是JarLauncher。内部调用Launcher的launch后,构建了MainMethodRunner对象。最后通过反射调用SpringbootApplication的main方法,实现启动效果。