SpringBoot的 jar为什么 可以直接运行?

SpringBoot的 jar为什么 可以直接运行?

 

先打包个JAR看下:

SpringBoot提供了一个插件spring-boot-maven-plugin用于把程序打包成一个可执行的jar包。在pom文件里加入这个插件即可:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

 

打包完生成的 example.jar 内部的结构如下:
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.test
│           └── mytest
│               ├── pom.properties
│               └── pom.xml
├── org
│   └── springframework
│       └── boot
│           └── loader
│               ├── archive
│               ├── data
│               ├── jar 
│               ├── ...
│    
├── BOOT-INF
│   ├── class
│        ├── com
│           └── test
│               ├── XX.java
│               ├── XX.calss
│        ├── application.properties 
│        ├── log4j.properties
│            
│   ├── lib
│        ├── spring-boot-1.3.5.RELEASE.jar
│        ├── spring-boot-autoconfigure-1.3.5.RELEASE.jar
│        ├── ...

然后可以直接执行jar包就能启动程序了:

java -jar example.jar

 打包出来fat jar内部有4种文件类型:

  • META-INF:程序入口,其中MANIFEST.MF用于描述jar包的信息

  • BOOT-INF/lib:放置第三方依赖的jar包,比如springboot的一些jar包

  • BOOT-INF/classes:存放应用编译后的 class 文件。

  • org: spring-boot-loader本身需要的class放置处 

 

MANIFEST.MF文件的内容:

Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 11
Implementation-Title: davinqi-test-pdfcli
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Version: 2.4.0
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
Start-Class: com.shebao.pdfcli.PdfCliApp

它的Main-Class是org.springframework.boot.loader.JarLauncher,当我们使用java -jar执行jar包的时候会调用JarLauncher的main方法,

而不是我们编写的SelfApplication(  XXXAppApplication  )

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

官方地址文档:
                 https://docs.spring.io/spring-boot/docs/current/reference/html/build-tool-plugins.html#build-tool-plugins-maven-plugin

The Spring Boot Maven Plugin provides Spring Boot support in Maven, letting you package executable jar or war archives and run an application “in-place”. To use it, you must use Maven 3.2 (or later).

翻译: Spring Boot Maven Plugin提供了Spring Boot在Maven中的支持,让你打包可执行的jar或war档案,并“就地”运行应用程序。要使用它,必须使用Maven 3.2(或更高版本)。

由上图我们可以看到,Spring Boot Maven plugin提供几种常用操作,maven标准说法是5种goal。

  1. spring-boot:repackage,默认goal。在mvn package之后,再次打包可执行的jar/war,同时保留mvn package生成的jar/war为.origin
  2. spring-boot:run,运行Spring Boot应用
  3. spring-boot:start,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理
  4. spring-boot:stop,在mvn integration-test阶段,进行Spring Boot应用生命周期的管理
  5. spring-boot:build-info,生成Actuator使用的构建信息文件build-info.properties
 
        当执行mvn clean package命令的时候,其会默认执行该插件的repackage任务。其会在target目录下生成以下两个文件: test.jar,test.jar.original 。
original后缀的文件原本是个jar后缀的文件,被重命名为original后缀文件了(简称源jar)。而现在看到的jar后缀是由源jar文件经过spring-boot-maven-plugin生成的一个fatjar文件(后文简称springboot fatjar)。
 
什么是fatjar?
      fatjar就是将一个jar及其依赖的三方jar全部打到一个包中。因为里面额外打包了依赖的三方jar包,所以比源jar大很多 。 

 

那么我们就从这个 repackage开始看: 

可以看到:   在项目中可以找到对应 上边说的(maven标准说法是5种goal)的执行实体类:

 

 

由于上面的XML 标签<mojo> 以及类关系UML图, 他们都实现了 org.apache.maven.plugin.Mojo#execute.

 

红框处为maven类的接口类。mojo这个东西就是入口。

Springboot 的  RepackageMojo 类实现了mojo接口就相当于实现 maven 的执行入口。我们打开mojo这个接口。那么我们主要看  RepackageMojo 这个类的执行过程:

org.springframework.boot.maven.RepackageMojo#execute

 调用

org.springframework.boot.maven.RepackageMojo#repackage   

 

 

private void repackage() throws MojoExecutionException {
    //  Artifact  mvn的接口 通过他获取mvn的依赖信息 
   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 {
       // 启动脚本
      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.获取源文件的Artifact 

 

2.创建并获取打包文件,这里仅仅是生成,并没有写入任何东西。

3.通过源文件构建一个打包对象Repackager。

 

 

4.获取项目的所有依赖Artifact,并且过滤一些不需要依赖对象

 

5.构建依赖对象


6.获取启动脚本

 

 

7.由步骤3构建的打包对象进行重新打包

 

 

8.更新Artifact 

   

 

private Repackager getRepackager(File source) {
   Repackager repackager = new Repackager(source, this.layoutFactory);
   repackager.addMainClassTimeoutWarningListener(


 new LoggingMainClassTimeoutWarningListener());
 // 不指定的,会查找第一个包含main方法的类, 最后将会设置org.springframework.boot.loader.JarLauncher
   repackager.setMainClass(this.mainClass);
   if (this.layout != null) {
      getLog().info("Layout: " + this.layout);
       // 视图布局  layout 判断Jar 还是war 
      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;
   }
}

这样就生成了我们想要的文章最初的文件结构 ( layout布局)

 

启动类  JarLauncher


    public class JarLauncher extends ExecutableArchiveLauncher {
        static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
        static final String BOOT_INF_LIB = "BOOT-INF/lib/";

        public JarLauncher() {
        }

        protected JarLauncher(Archive archive) {
            super(archive);
        }

        @Override
        protected boolean isNestedArchive(Archive.Entry entry) {
            if (entry.isDirectory()) {
                return entry.getName().equals(BOOT_INF_CLASSES);
            }
            return entry.getName().startsWith(BOOT_INF_LIB);
        }

        public static void main(String[] args) throws Exception {   //项目入口,重点在launch这个方法中 
            new JarLauncher().launch(args); }
    }

 

   

   protected void launch(String[] args) throws Exception {
        if (!this.isExploded()) {
            JarFile.registerUrlProtocolHandler();
        }
        // 创建LaunchedURLClassLoader。如果根类加载器和扩展类加载器没有加载到某个类的话,
        // 就会通过LaunchedURLClassLoader这个加载器来加载类。
        // 这个加载器会从Boot-INF下面的class目录和lib目录下加载类。
        ClassLoader classLoader = this.createClassLoader(this.getClassPathArchivesIterator());
        String jarMode = System.getProperty("jarmode");
        String launchClass = jarMode != null && !jarMode.isEmpty() ? "org.springframework.boot.loader.jarmode.JarModeLauncher" : this.getMainClass(); 
 // 这个方法会读取jar描述文件中的Start-Class属性,然后通过反射调用到这个类的main方法。
        this.launch(args, launchClass, classLoader);
    }
  protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(this.isExploded(), this.getArchive(), urls, this.getClass().getClassLoader());
    }
protected String getMainClass() throws Exception {
        Manifest manifest = this.archive.getManifest();
        String mainClass = null;
        if (manifest != null) {
            mainClass = manifest.getMainAttributes().getValue("Start-Class");
        }

        if (mainClass == null) {
            throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
        } else {
            return mainClass;
        }
    }
   protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        this.createMainMethodRunner(launchClass, args, classLoader).run();
    }
  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((Object)null, this.args);
    }

寻找这个类:  Start-Class: com.shebao.pdfcli.PdfCliApp

  • Spring Boot 执行入口是 JarLauncher 的 main 方法;
  •  逻辑是先创建一个 LaunchedURLClassLoader:先判断根类加载器和扩展类加载器能否加载到某个类,如果都加载不到就从 Boot-INF 下面的 class 和 lib 目录下去加载;
  • 读取Start-Class属性,通过反射,调用启动类的 main(),然后就是执行spring容器的类的加载初始化等操作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值