⾯试官:SpringBootjar可执⾏原理,知道吗?
⽂章篇幅较长,但是包含了SpringBoot 可执⾏jar包从头到尾的原理,请读者耐⼼观看。同时⽂章是基于 SpringBoot-2.1.3进⾏分析。涉及的知识点主要包括Maven的⽣命周期以及⾃定义插件,JDK提供关于jar包的⼯具类以及Springboot如何扩展,最后是⾃定义类加载器。spring-boot-maven-plugin
SpringBoot 的可执⾏jar包⼜称 fat jar ,是包含所有第三⽅依赖的 jar 包,jar 包中嵌⼊了除 java 虚拟机以外的所有依赖,是⼀个 all-in-one jar 包。普通插件 maven-jar-plugin⽣成的包和 spring-boot-maven-plugin⽣成的包之间的直接区别,是 fat jar中主要增加了两部分,第⼀部分是lib⽬录,存放的是Maven依赖的jar包⽂件,第⼆部分是 spring boot loader相关的类。
1. fat jar ⽬录结构
2. ├─BOOT-INF
3. │├─classes
4. │└─lib
5. ├─META-INF
6. │├─maven
7. │├─app.properties
8. │├─MANIFEST.MF
9. └─org
0. └─springframework
1. └─boot
2. └─loader
3. ├─archive
4. ├─data
5. ├─jar
6. └─util
也就是说想要知道 fat jar是如何⽣成的,就必须知道 spring-boot-maven-plugin⼯作机制,⽽ spring-boot-maven-plugin属于⾃定义插件,因此我们⼜必须知道,Maven的⾃定义插件是如何⼯作的
Maven的⾃定义插件
Maven 拥有三套相互独⽴的⽣命周期: clean、default 和 site, ⽽每个⽣命周期包含⼀些phase阶段, 阶段是有顺序的, 并且后⾯的阶段依赖于前⾯的阶段。⽣命周期的阶段phase与插件的⽬标goal相互绑定,⽤以完成实际的构建任务。
1. <plugin>
2. <groupId>org.springframework.boot</groupId>
3. <artifactId>spring-boot-maven-plugin</artifactId>
4. <executions>
5. <execution>
6. <goals>
7. <goal>repackage</goal>
8. </goals>
9. </execution>
0. </executions>
1. </plugin>
repackage⽬标对应的将执⾏到 org.springframework.boot.maven.RepackageMojo#execute,该⽅法的主要逻辑是调⽤了
org.springframework.boot.maven.RepackageMojo#repackage
1. private void repackage() throws MojoExecutionException {
2. //获取使⽤maven-jar-plugin⽣成的jar,最终的命名将加上.orignal后缀
3. Artifact source = getSourceArtifact();
4. //最终⽂件,即Fat jar
5. File target = getTargetFile();
6. //获取重新打包器,将重新打包成可执⾏jar⽂件
7. Repackager repackager = File());
8. //查并过滤项⽬运⾏时依赖的jar
9. Set < Artifact > artifacts = filterDependencies(Artifacts(), getFilters(getAdditionalFilters()));
0. //将artifacts转换成libraries
1. Libraries libraries = new ArtifactsLibraries(artifacts, quiresUnpack, getLog());
2. try {
3. //提供Spring Boot启动脚本
4. LaunchScript launchScript = getLaunchScript();
5. //执⾏重新打包逻辑,⽣成最后fat jar
6. package(target, libraries, launchScript);
7. } catch (IOException ex) {
8. throw new Message(), ex);
9. }
0. //将source更新成 ignal⽂件
1. updateArtifact(source, target, BackupFile());
2. }
我们关⼼⼀下 org.springframework.boot.maven.RepackageMojo#getRepackager这个⽅法,知道 Repackager是如何⽣成的,也就⼤致能够推测出内在的打包逻辑。
1. private Repackager getRepackager(File source) {
2. Repackager repackager = new Repackager(source, this.layoutFactory);
3. repackager.addMainClassTimeoutWarningListener(
4. new LoggingMainClassTimeoutWarningListener());
5. //设置main class的名称,如果不指定的话则会查第⼀个包含main⽅法的类,repacke最后将会设置
org.springframework.boot.loader.JarLauncher
6. repackager.setMainClass(this.mainClass);
7. if (this.layout != null) {
8. getLog().info("Layout: " + this.layout);
9. //重点关⼼下layout 最终返回了 org.springframework.ls.Layouts.Jar
0. repackager.setLayout(this.layout.layout());
1. }
2. return repackager;
3. }
1. /**
2. * Executable JAR layout.
3. */
4. public static class Jar implements RepackagingLayout {
5. @Override
6. public String getLauncherClassName() {
7. return "org.springframework.boot.loader.JarLauncher";
8. }
9. @Override
0. public String getLibraryDestination(String libraryName, LibraryScope scope) {
1. return "BOOT-INF/lib/";
2. }
3. @Override
4. public String getClassesLocation() {
5. return "";
6. }
7. @Override
8. public String getRepackagedClassesLocation() {
9. return "BOOT-INF/classes/";
0. }
1. @Override
2. public boolean isExecutable() {
3. return true;
4. }
5. }
layout我们可以将之翻译为⽂件布局,或者⽬录布局,代码⼀看清晰明了,同时我们需要关注,也是下⼀个重点关注对象
org.springframework.boot.loader.JarLauncher,从名字推断,这很可能是返回可执⾏ jar⽂件的启动类。
MANIFEST.MF⽂件内容
1. Manifest-Version: 1.0
2. Implementation-Title: oneday-auth-server
3. Implementation-Version: 1.0.0-SNAPSHOT
4. Archiver-Version: Plexus Archiver
5. Built-By: oneday
6. Implementation-Vendor-Id: day
7. Spring-Boot-Version: 2.1.3.RELEASE
8. Main-Class: org.springframework.boot.loader.JarLauncher
9. Start-Class: day.auth.Application
0. Spring-Boot-Classes: BOOT-INF/classes/
1. Spring-Boot-Lib: BOOT-INF/lib/
2. Created-By: Apache Maven
3.3.9
3. Build-Jdk: 1.8.0_171
repackager⽣成的MANIFEST.MF⽂件为以上信息,可以看到两个关键信息 Main-Class和 Start-Class。
我们可以进⼀步,程序的启动⼊⼝并不是我们SpringBoot中定义的 main,⽽是 JarLauncher#main,⽽再在其中利⽤反射调⽤定义好的 Start-Class的 main⽅法
JarLauncher
重点类介绍
1、 java.util.jar.JarFile JDK⼯具类提供的读取 jar⽂件
2、 org.springframework.boot.loader.jar.JarFile Springboot-loader 继承JDK提供 JarFile类
3、 java.util.jar.JarEntry DK⼯具类提供的``jar```⽂件条⽬
4、 org.springframework.boot.loader.jar.JarEntry Springboot-loader 继承JDK提供 JarEntry类
5、 org.springframework.boot.loader.archive.Archive Springboot抽象出来的统⼀访问资源的层
6、 JarFileArchive jar包⽂件的抽象
7、 ExplodedArchive⽂件⽬录
这⾥重点描述⼀下 JarFile的作⽤,每个 JarFileArchive都会对应⼀个 JarFile。在构造的时候会解析内部结构,去获取 jar包⾥的各个⽂件或⽂件夹类。我们可以看⼀下该类的注释。
1. /* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
2. * offers the following additional functionality.
3. * <ul>
4. * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
5. * on any directory entry.</li>
6. * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
7. * embedded JAR files (as long as their entry is not compressed).</li>
8. **/ </ul>
jar⾥的资源分隔符是 !/,在JDK提供的 JarFile URL只⽀持⼀个’!/‘,⽽Spring boot扩展了这个协议,让它⽀持多个’!/‘,就可以表⽰jar in jar、jar in directory、fat jar的资源了。
⾃定义类加载机制
最基础:Bootstrap ClassLoader(加载JDK的/lib⽬录下的类)
次基础:Extension ClassLoader(加载JDK的/lib/ext⽬录下的类)
普通:Application ClassLoader(程序⾃⼰classpath下的类)
⾸先需要关注双亲委派机制很重要的⼀点是,如果⼀个类可以被委派最基础的ClassLoader加载,就不能让⾼层的ClassLoader加载,这样是为了范围错误的引⼊了⾮JDK下但是类名⼀样的类。其⼆,如果在这个机制下,由于 fat jar中依赖的各个第三⽅ jar⽂件,并不在程序⾃⼰classpath下,也就是说,如果我们采⽤双亲委派机制的话,根本获取不到我们所依赖的jar包,因此我们需要修改双亲委派机制的查class 的⽅法,⾃定义类加载机制。
先简单的介绍Springboot2中 LaunchedURLClassLoader,该类继承了 java.URLClassLoader,重写了
springboot结构java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我们再探讨他是如何修改双亲委派机制。
在上⾯我们讲到Spring boot⽀持多个’!/‘以表⽰多个jar,⽽我们的问题在于,如何解决查到这多个jar包。我们看⼀下LaunchedURLClassLoader的构造⽅法。
1. public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
2. super(urls, parent);
3. }
urls注释解释道 theURLsfromwhich to load classesandresources,即fat jar包依赖的所有类和资源,将该urls参数传递给⽗类
java.URLClassLoader,由⽗类的 java.URLClassLoader#findClass执⾏查类⽅法,该类的查来源即构造⽅法传递进来的urls参数
1. //LaunchedURLClassLoader的实现
2. protected Class <? > loadClass(String name, boolean resolve)
3. throws ClassNotFoundException {
4. Handler.setUseFastConnectionExceptions(true);
5. try {
6. try {
7. //尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar⾥匹配的manifest能够和关联的package关联起来
8. definePackageIfNecessary(name);
9. } catch (IllegalArgumentException ex) {
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论