Dra-M Dra-M
首页
技术
冥思
哲学
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

莫小龙

保持理智,相信未来。
首页
技术
冥思
哲学
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • JUC

    • Spring

      • 彻底透析SpringBoot jar可执行原理
        • 使用Hibernate-validator的Notnull等注解校验参数并使用全局异常处理
        • 简陋的注解扫描实现
        • SpringBoot+Swagger,快速构建REST API文档
        • Spring&kotlin 异常 NoClassDefFoundError
        • PageHelper不生效问题
        • SpringDataJpa一些注意事项
        • SpringDataJpa的动态条件查询和分页查询
      • Gradle

      • 面向对象

      • Stream流式编程思想及常用API介绍
      • JAVA快速导出Excel
    • Golang

    • 编程思想

    • 微服务

    • 中间件

    • Python

    • 运维

    • 技术
    • Java
    • Spring
    plz叫我红领巾
    2020-03-07
    目录

    彻底透析SpringBoot jar可执行原理

    文章篇幅较长,但是包含了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相关的类。

    fat jar 目录结构
    ├─BOOT-INF
    │  ├─classes
    │  └─lib
    ├─META-INF
    │  ├─maven
    │  ├─app.properties
    │  ├─MANIFEST.MF      
    └─org
        └─springframework
            └─boot
                └─loader
                    ├─archive
                    ├─data
                    ├─jar
                    └─util
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    也就是说想要知道fat jar是如何生成的,就必须知道spring-boot-maven-plugin工作机制,而spring-boot-maven-plugin属于自定义插件,因此我们又必须知道,Maven的自定义插件是如何工作的

    # Maven的自定义插件

    Maven 拥有三套相互独立的生命周期: clean、default 和 site, 而每个生命周期包含一些phase阶段, 阶段是有顺序的, 并且后面的阶段依赖于前面的阶段。生命周期的阶段phase与插件的目标goal相互绑定,用以完成实际的构建任务。

    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <executions>
            <execution>
                <goals>
                    <goal>repackage</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    repackage目标对应的将执行到org.springframework.boot.maven.RepackageMojo#execute,该方法的主要逻辑是调用了org.springframework.boot.maven.RepackageMojo#repackage

    private void repackage() throws MojoExecutionException {
         //获取使用maven-jar-plugin生成的jar,最终的命名将加上.orignal后缀
       Artifact source = getSourceArtifact();
        //最终文件,即Fat jar
       File target = getTargetFile();
        //获取重新打包器,将重新打包成可执行jar文件
       Repackager repackager = getRepackager(source.getFile());
        //查找并过滤项目运行时依赖的jar
       Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
             getFilters(getAdditionalFilters()));
        //将artifacts转换成libraries
       Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
             getLog());
       try {
           //提供Spring Boot启动脚本
          LaunchScript launchScript = getLaunchScript();
           //执行重新打包逻辑,生成最后fat jar
          repackager.repackage(target, libraries, launchScript);
       }
       catch (IOException ex) {
          throw new MojoExecutionException(ex.getMessage(), ex);
       }
        //将source更新成 xxx.jar.orignal文件
       updateArtifact(source, target, repackager.getBackupFile());
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    我们关心一下org.springframework.boot.maven.RepackageMojo#getRepackager这个方法,知道Repackager是如何生成的,也就大致能够推测出内在的打包逻辑。

    private Repackager getRepackager(File source) {
       Repackager repackager = new Repackager(source, this.layoutFactory);
       repackager.addMainClassTimeoutWarningListener(
             new LoggingMainClassTimeoutWarningListener());
        //设置main class的名称,如果不指定的话则会查找第一个包含main方法的类,repacke最后将会设置org.springframework.boot.loader.JarLauncher
       repackager.setMainClass(this.mainClass);
       if (this.layout != null) {
          getLog().info("Layout: " + this.layout);
           //重点关心下layout 最终返回了 org.springframework.boot.loader.tools.Layouts.Jar
          repackager.setLayout(this.layout.layout());
       }
       return repackager;
    }
    /**
     * Executable JAR layout.
     */
    public static class Jar implements RepackagingLayout {
       @Override
       public String getLauncherClassName() {
          return "org.springframework.boot.loader.JarLauncher";
       }
       @Override
       public String getLibraryDestination(String libraryName, LibraryScope scope) {
          return "BOOT-INF/lib/";
       }
       @Override
       public String getClassesLocation() {
          return "";
       }
       @Override
       public String getRepackagedClassesLocation() {
          return "BOOT-INF/classes/";
       }
       @Override
       public boolean isExecutable() {
          return true;
       }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38

    layout我们可以将之翻译为文件布局,或者目录布局,代码一看清晰明了,同时我们需要关注,也是下一个重点关注对象org.springframework.boot.loader.JarLauncher,从名字推断,这很可能是返回可执行jar文件的启动类。

    # MANIFEST.MF文件内容
    Manifest-Version: 1.0
    Implementation-Title: oneday-auth-server
    Implementation-Version: 1.0.0-SNAPSHOT
    Archiver-Version: Plexus Archiver
    Built-By: oneday
    Implementation-Vendor-Id: com.oneday
    Spring-Boot-Version: 2.1.3.RELEASE
    Main-Class: org.springframework.boot.loader.JarLauncher
    Start-Class: com.oneday.auth.Application
    Spring-Boot-Classes: BOOT-INF/classes/
    Spring-Boot-Lib: BOOT-INF/lib/
    Created-By: Apache Maven 3.3.9
    Build-Jdk: 1.8.0_171
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    repackager生成的MANIFEST.MF文件为以上信息,可以看到两个关键信息Main-Class和Start-Class。我们可以进一步,程序的启动入口并不是我们SpringBoot中定义的main,而是JarLauncher#main,而再在其中利用反射调用定义好的Start-Class的main方法

    # JarLauncher

    # 重点类介绍
    • java.util.jar.JarFile JDK工具类提供的读取jar文件

    • org.springframework.boot.loader.jar.JarFileSpringboot-loader 继承JDK提供JarFile类

    • java.util.jar.JarEntryDK工具类提供的``jar```文件条目

    • org.springframework.boot.loader.jar.JarEntry Springboot-loader 继承JDK提供JarEntry类

    • org.springframework.boot.loader.archive.Archive
      
      1

      Springboot抽象出来的统一访问资源的层

      • JarFileArchivejar包文件的抽象
      • ExplodedArchive文件目录

    这里重点描述一下JarFile的作用,每个JarFileArchive都会对应一个JarFile。在构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹类。我们可以看一下该类的注释。

    /* Extended variant of {@link java.util.jar.JarFile} that behaves in the same way but
    * offers the following additional functionality.
    * <ul>
    * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} based
    * on any directory entry.</li>
    * <li>A nested {@link JarFile} can be {@link #getNestedJarFile(ZipEntry) obtained} for
    * embedded JAR files (as long as their entry is not compressed).</li>
    **/ </ul>
    
    1
    2
    3
    4
    5
    6
    7
    8

    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.net.URLClassLoader,重写了java.lang.ClassLoader#loadClass(java.lang.String, boolean),然后我们再探讨他是如何修改双亲委派机制。

    在上面我们讲到Spring boot支持多个’!/‘以表示多个jar,而我们的问题在于,如何解决查找到这多个jar包。我们看一下LaunchedURLClassLoader的构造方法。

    public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
       super(urls, parent);
    }
    
    1
    2
    3

    urls注释解释道the URLs from which to load classes and resources,即fat jar包依赖的所有类和资源,将该urls参数传递给父类java.net.URLClassLoader,由父类的java.net.URLClassLoader#findClass执行查找类方法,该类的查找来源即构造方法传递进来的urls参数

    //LaunchedURLClassLoader的实现
    protected Class<?> loadClass(String name, boolean resolve)
          throws ClassNotFoundException {
       Handler.setUseFastConnectionExceptions(true);
       try {
          try {
              //尝试根据类名去定义类所在的包,即java.lang.Package,确保jar in jar里匹配的manifest能够和关联	           //的package关联起来
             definePackageIfNecessary(name);
          }
          catch (IllegalArgumentException ex) {
             // Tolerate race condition due to being parallel capable
             if (getPackage(name) == null) {
                // This should never happen as the IllegalArgumentException indicates
                // that the package has already been defined and, therefore,
                // getPackage(name) should not return null.
                 
                //这里异常表明,definePackageIfNecessary方法的作用实际上是预先过滤掉查找不到的包
                throw new AssertionError("Package " + name + " has already been "
                      + "defined but it could not be found");
             }
          }
          return super.loadClass(name, resolve);
       }
       finally {
          Handler.setUseFastConnectionExceptions(false);
       }
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    方法super.loadClass(name, resolve)实际上会回到了java.lang.ClassLoader#loadClass(java.lang.String, boolean),遵循双亲委派机制进行查找类,而Bootstrap ClassLoader和Extension ClassLoader将会查找不到fat jar依赖的类,最终会来到Application ClassLoader,调用java.net.URLClassLoader#findClass

    # 如何真正的启动

    Springboot2和Springboot1的最大区别在于,Springboo1会新起一个线程,来执行相应的反射调用逻辑,而SpringBoot2则去掉了构建新的线程这一步。方法是org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader)反射调用逻辑比较简单,这里就不再分析,比较关键的一点是,在调用main方法之前,将当前线程的上下文类加载器设置成LaunchedURLClassLoader

    protected void launch(String[] args, String mainClass, ClassLoader classLoader)
          throws Exception {
       Thread.currentThread().setContextClassLoader(classLoader);
       createMainMethodRunner(mainClass, args, classLoader).run();
    }
    
    
    1
    2
    3
    4
    5
    6

    # Demo

    public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
            JarFile.registerUrlProtocolHandler();
    // 构造LaunchedURLClassLoader类加载器,这里使用了2个URL,分别对应jar包中依赖包spring-boot-loader和spring-boot,使用 "!/" 分开,需要org.springframework.boot.loader.jar.Handler处理器处理
            LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader(
                    new URL[] {
                            new URL("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!/")
                            , new 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());
    // 加载类
    // 这2个类都会在第二步本地查找中被找出(URLClassLoader的findClass方法)
            classLoader.loadClass("org.springframework.boot.loader.JarLauncher");
            classLoader.loadClass("org.springframework.boot.SpringApplication");
    // 在第三步使用默认的加载顺序在ApplicationClassLoader中被找出
       classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration");
    
    //        SpringApplication.run(Application.class, args);
        }
           <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-loader -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-loader</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.1.3.RELEASE</version>
    
            </dependency>
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31

    # 总结

    对于源码分析,这次的较大收获则是不能一下子去追求弄懂源码中的每一步代码的逻辑,即便我知道该方法的作用。我们需要搞懂的是关键代码,以及涉及到的知识点。我从Maven的自定义插件开始进行追踪,巩固了对Maven的知识点,在这个过程中甚至了解到JDK对jar的读取是有提供对应的工具类。最后最重要的知识点则是自定义类加载器。整个代码下来并不是说代码究竟有多优秀,而是要学习他因何而优秀。

    作者:plz叫我红领巾

    出处:juejin.im/post/5d15c4… (opens new window)

    本博客欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。码字不易,您的点赞是我写作的最大动力。

    参考资料: SpringBoot源码分析之SpringBoot可执行文件解析 (opens new window)


    #Java#SpringBoot#转载
    上次更新: 10/23/2024
    为什么不用Executors去创建线程池
    使用Hibernate-validator的Notnull等注解校验参数并使用全局异常处理

    ← 为什么不用Executors去创建线程池 使用Hibernate-validator的Notnull等注解校验参数并使用全局异常处理→

    最近更新
    01
    mosquito配置ws协议
    10-23
    02
    Pip包的离线下载和安装
    10-23
    03
    stable diffusion 相关收藏
    02-24
    更多文章>
    Theme by Vdoing | Copyright © 2019-2024 Dra-M
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式