美文网首页晓我课堂
SpringBoot源码解读(一 .可执行JAR源码分析)

SpringBoot源码解读(一 .可执行JAR源码分析)

作者: liushiping | 来源:发表于2022-01-05 23:24 被阅读0次

    一.可执行JAR结构分析

    在Spring Boot应用中,使用spring-boot-maven-plugin插件执行mvn package命令生成的jar文件,可以通过java -jar命令直接运行,这种jar文件称为可执行jar文件(Executable JAR)。

    1.可执行jar文件的获取

    可以从任意SpringBoot工程中运行mvn package命令生成的jar文件,如没有现成的SpringBoot工程,可以参考下列步骤生成一个。
    https://start.spring.io/中创建创建SpringBoot项目,填写项目的Group、Aritact及Package信息如:

    项目元数据信息
    填写完信息后,点击GENERAT按钮生成SpringBoot项目压缩文件并下载到本地,通过unzip
    命令解压后,进入项目目录并执行mvn package命令,在项目的target目录下便生成了可执行jar文件(executable-jar-0.0.1-SNAPSHOT.jar)和原始Maven打包的jar文件(executable-jar-0.0.1-SNAPSHOT.jar.original)等文件。接下来我们打开jar文件一窥究竟吧。
    2.可执行jar文件内部结构

    执行unzip executable-jar-0.0.1-SNAPSHOT.jar -d temp将jar包解压到temp目录下,在通过tree命令查看目录结构:

    william@liushipingdeMacBook-Pro target % tree temp 
    temp
    ├── BOOT-INF
    │   ├── classes
    │   │   ├── application.properties
    │   │   └── cn
    │   │       └── lsp
    │   │           └── springboot
    │   │               └── executablejar
    │   │                   └── ExecutableJarApplication.class
    │   ├── classpath.idx
    │   ├── layers.idx
    │   └── lib
    │       ├── ... ...
    │       ├── spring-boot-2.6.2.jar
    │       ├── ... ...
    ├── META-INF
    │   ├── MANIFEST.MF
    │   └── maven
    │       └── cn.lsp.springboot
    │           └── executable-jar
    │               ├── pom.properties
    │               └── pom.xml
    └── org
        └── springframework
            └── boot
                └── loader
                    ├── ClassPathIndexFile.class
                    ├── ExecutableArchiveLauncher.class
                    ├── JarLauncher.class
                    ├── LaunchedURLClassLoader.class
                    ├── Launcher.class
                    ├── ... ...
    
    1.BOOT-INF/classes目录存放应用编译后的class文件;
    2.BOOT-INF/classpath.id 可执行jar文件依赖的类路径索引文件;
    3.BOOT-INF/lib目录存放应用依赖的jar包;
    4.META-INF目录存放应用相关的元信息,如MANIFEST.MF文件;
    5.org目录存放启动SpringBoot相关的class文件;
    

    通过解压目录看出,和传统的jar文件相比,多了BOOT-INF目录和启动SpringBoot相关的class文件,并且将传统的class文件放置到了BOOT-INF是classes目录下,所依赖的jar均放到了BOOT-INF/lib目录。
    我们知道。通过java -jar运行的是标准的可执行jar文件,按照Java官方文档的规定,该命令引导的具体启动类必须配置在META-INF/MANIFEST.MF文件的Main-Class属性中。那我们来查看一下该文件的内容:

    william@liushipingdeMacBook-Pro temp % cat META-INF/MANIFEST.MF 
    Manifest-Version: 1.0
    Created-By: Maven Jar Plugin 3.2.0
    Build-Jdk-Spec: 16
    Implementation-Title: executable-jar
    Implementation-Version: 0.0.1-SNAPSHOT
    Main-Class: org.springframework.boot.loader.JarLauncher
    Start-Class: cn.lsp.springboot.executablejar.ExecutableJarApplication
    Spring-Boot-Version: 2.6.2
    Spring-Boot-Classes: BOOT-INF/classes/
    Spring-Boot-Lib: BOOT-INF/lib/
    Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
    Spring-Boot-Layers-Index: BOOT-INF/layers.idx
    

    可以发现Main-Class属性的值为org.springframework.boot.loader.JarLauncher,而我们自己的项目中的Main Class全路径名(cn.lsp.springboot.executablejar.ExecutableJarApplication)则存放到了Start-Class属性中。从文件内容可以看出SpringBoot的运行都是通过org.springframework.boot.loader.JarLauncher来引导的,该类就是可执行jar的启动器。

    二.可执行JAR源码分析

    由于可执行jar文件的启动类为org.springframework.boot.loader.JarLauncher,为了方便分析源码了解其实现原理,我们将该类所在jar包引入项目的依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-loader</artifactId>
        <scope>provided</scope>
    </dependency>
    
    启动流程源码解读
    public class JarLauncher extends ExecutableArchiveLauncher {
    
        private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";
    
        static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
            if (entry.isDirectory()) {
                return entry.getName().equals("BOOT-INF/classes/");
            }
            return entry.getName().startsWith("BOOT-INF/lib/");
        };
    
        public JarLauncher() {
        }
    
        protected JarLauncher(Archive archive) {
            super(archive);
        }
    
        @Override
        protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
            // Only needed for exploded archives, regular ones already have a defined order
            if (archive instanceof ExplodedArchive) {
                String location = getClassPathIndexFileLocation(archive);
                return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);
            }
            return super.getClassPathIndex(archive);
        }
    
        private String getClassPathIndexFileLocation(Archive archive) throws IOException {
            Manifest manifest = archive.getManifest();
            Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;
            String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;
            return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;
        }
    
        @Override
        protected boolean isPostProcessingClassPathArchives() {
            return false;
        }
    
        @Override
        protected boolean isSearchCandidate(Archive.Entry entry) {
            return entry.getName().startsWith("BOOT-INF/");
        }
    
        @Override
        protected boolean isNestedArchive(Archive.Entry entry) {
            return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
        }
    
        public static void main(String[] args) throws Exception {
            new JarLauncher().launch(args);
        }
    
    }
    

    该类是一个标准的Java应用程序入口类,继承自ExecutableArchiveLauncher,常量DEFAULT_CLASSPATH_INDEX_LOCATION所指向的文件内容为应用依赖的jar文件类路径。isNestedArchive方法用于判断Archive.Entry是否是Jar文件中的资源,Archive.Entry有两种实现,JarFileArchive.JarFileEntryExplodedArchive.FileEntry,前者基于jar文件,后者基于文件系统,所以JarLauncher支持Jar文件和文件系统两种启动方式。
    当执行java -jar命令时,META-INF/MANIFEST.MF文件的Main-Class属性将调用main(String[])方法,实际上是调用JarLauncher#launch(args)方法,该方法继承于基类org.springframework.boot.loader.Launcher,他们之间的继承层次图如下:

    org.springframework.boot.loader.Launcher
          org.springframework.boot.loader.ExecutableArchiveLauncher
                org.springframework.boot.loader.JarLauncher                  //用于引导jar文件
                org.springframework.boot.loader.WarLauncher                 // 用于引导war文件
    

    下面分析Launcher#launch(args)方法实现:

    public abstract class Launcher {
           ... ...
        protected void launch(String[] args) throws Exception {
            if (!isExploded()) {
                JarFile.registerUrlProtocolHandler();
            }
            ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
            String jarMode = System.getProperty("jarmode");
            String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
            launch(args, launchClass, classLoader);
        }
          ...  ...
    }
    

    JarFile.registerUrlProtocolHandler()方法将package org.springframework.boot.loader追加到Java系统属性java.protocol.handler.pkgs中,即org.springframework.boot.loader.jar.Handler,其实现协议为JAR,用于覆盖JDK内建的sun.net.www.protocol.jar.Handler。由于SpringBoot的可执行Jar文件除了包含传统的Java Jar中的资源外,还包含依赖的Jar文件,当SpringBoot的可执行jar被java -jar命令引导时,其内部的jar文件无法被JDK内建的sun.net.www.protocol.jar.Handler当做Class Path,所以需要替换才能确保正常运行。

    createClassLoader(Iterator)方法用于创建LaunchedURLClassLoader,实现类的加载。

    最后调用实际的引导类launch(args, launchClass, classLoader)

        protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
            Thread.currentThread().setContextClassLoader(classLoader);
            createMainMethodRunner(launchClass, args, classLoader).run();
        }
    
        protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
            return new MainMethodRunner(mainClass, args);
        }
    

    该方法的实际执行者为MainMethodRunner#run()方法。

    public class MainMethodRunner {
    
        private final String mainClassName;
    
        private final String[] args;
    
        public MainMethodRunner(String mainClass, String[] args) {
            this.mainClassName = mainClass;
            this.args = (args != null) ? args.clone() : null;
        }
    
        public void run() throws Exception {
            Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
            Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
            mainMethod.setAccessible(true);
            mainMethod.invoke(null, new Object[] { this.args });
        }
    }
    

    MainMethodRunner对象需要关联mainClass及main方法参数args,通过反射来调用项目中真正的入口类的main方法,即META-INF/MANIFEST.MF文件中指定的Start-Class: cn.lsp.springboot.executablejar.ExecutableJarApplication。至此,应用程序的class path等环境在启动前已准备完毕,真正进入应用的启动阶段。

    三.总结

    1.SpringBoot的Launcher有JarLauncher和WarLauncher,前者引导jar文件启动,后者引导war文件启动;
    2.SpringBoot的Launcher有两种引导模式,基于Jar和文件系统;
    3.由于SpringBoot生成的可执行jar文件与传统jar文件不同,因此需要实现自己的org.springframework.boot.loader.jar.Handler来覆盖JDK内建的sun.net.www.protocol.jar.Handler,从而按照SpringBoot自己的方式来初始化classpath等环境并引导jar运行;

    相关文章

      网友评论

        本文标题:SpringBoot源码解读(一 .可执行JAR源码分析)

        本文链接:https://www.haomeiwen.com/subject/ptdoqrtx.html