【Spring】Spring Boot 为什么可以使用 Jar 包启动

1.概述

转载:Spring Boot 为什么可以使用 Jar 包启动

可能很多初学者会比较困惑,Spring Boot 是如何做到将应用代码和所有的依赖打包成一个独立的 Jar 包,因为传统的 Java 项目打包成 Jar 包之后,需要通过 -classpath 属性来指定依赖,才能够运行。我们今天就来分析讲解一下 Spring Boot 的启动原理。

2. Spring Boot 打包插件

Spring Boot 提供了一个名叫 spring-boot-maven-plugin 的 maven 项目打包插件,可以方便的将 Spring Boot 项目打成 jar 包。 这样我们就不再需要部署 Tomcat 、Jetty等之类的 Web 服务器容器啦。

我们先看一下 Spring Boot 打包后的结构是什么样的,打开 target 目录我们发现有两个jar包:

hello-0.0.1-SNAPSHOT.jar:17.3MB
hello-0.0.1-SNAPSHOT.jar.original:3KB
java 运行

其中,hello-0.0.1-SNAPSHOT.jar 是通过 Spring Boot 提供的打包插件采用新的格式打成 Fat Jar,包含了所有的依赖;而 hello-0.0.1-SNAPSHOT.jar.original 则是Java原生的打包方式生成的,仅仅只包含了项目本身的内容。

3.SpringBoot FatJar 的组织结构

我们将 Spring Boot 打的可执行 Jar 展开后的结构如下所示:

.
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── com
│   │       └── javanorth
│   │           └── hello
│   │               └── HelloApplication.class
│   └── lib
│       ├── spring-boot-2.5.0.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.5.0.RELEASE.jar
│       ├── spring-boot-configuration-processor-2.5.0.RELEASE.jar
│       ├── spring-boot-starter-2.5.0.RELEASE.jar
│       ├── ...
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.javanorth
│           └── hello
│               ├── pom.properties
│               └── pom.xml
│   
├── org
│   └── springframework
│       └── boot
│           └── loader
│               ├── ExecutableArchiveLauncher.class
│               ├── JarLauncher.class
│               ├── Launcher.class
│               ├── MainMethodRunner.class
│               ├── ...
java 运行

BOOT-INF目录:包含了我们的项目代码(classes目录),以及所需要的依赖(lib 目录)

META-INF目录:通过 MANIFEST.MF 文件提供 Jar包的元数据,声明了 jar 的启动类

org.springframework.boot.loader :Spring Boot 的加载器代码,实现的 Jar in Jar 加载的魔法源

我们看到,如果去掉BOOT-INF目录,这将是一个非常普通且标准的Jar包,包括元信息以及可执行的代码部分,其/META-INF/MAINFEST.MF指定了Jar包的启动元信息,org.springframework.boot.loader 执行对应的逻辑操作。

4. MAINFEST.MF 元信息分析

元信息内容如下所示:

Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 11
Implementation-Title: hello
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.javanorth.hello.HelloApplication
Spring-Boot-Version: 2.5.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
java 运行

它相当于一个 Properties 配置文件,每一行都是一个配置项目。重点来看看两个配置项:

  1. Main-Class 配置项:Java 规定的 jar 包的启动类,这里设置为 spring-boot-loader 项目的 JarLauncher 类,进行 Spring Boot 应用的启动。

  2. Start-Class 配置项:Spring Boot 规定的主启动类,这里设置为我们定义的 Application 类。

  3. Spring-Boot-Classes 配置项:指定加载应用类的入口

  4. Spring-Boot-Lib 配置项: 指定加载应用依赖的库

5.启动原理

Spring Boot 的启动原理如下图所示:
在这里插入图片描述

6.源码分析

org.springframework.boot.loader.JarLauncher
JarLauncher 类是针对 Spring Boot jar 包的启动类, 完整的类图如下所示:

在这里插入图片描述
其中的 WarLauncher 类,是针对 Spring Boot war 包的启动类。 启动类 org.springframework.boot.loader.JarLauncher 并非为项目中引入类,而是 spring-boot-maven-plugin 插件 repackage 追加进去的。 接下来我们先来看一下 JarLauncher 的源码,比较简单,如下图所示:

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 运行

当执行 java -jar 命令或执行解压后的 org.springframework.boot.loader.JarLauncher 类时,JarLauncher 会将 BOOT-INF/classes 下的类文件和 BOOT-INF/lib 下依赖的jar加入到classpath下,后调用 META-INF/MANIFEST.MF 文件 Start-Class 属性 [指向项目中的 com.javanorth.hello.HelloApplicatioin 启动类] 完成应用程序的启动。

JarLauncher 假定依赖项jar包含在 /BOOT-INF/lib 目录中,并且应用程序类包含在 /BOOT-INF/classes 目录中。它的 main 方法调用的则是基类 Launcher 定义的 launch 方法,而 Launcher 是ExecutableArchiveLauncher 的父类。

6.1 ExecutableArchiveLauncher

ExecutableArchiveLauncher 是 JarLauncher 的直接父类,继承了 Launcher 基类,并实现部分抽象方法

public abstract class ExecutableArchiveLauncher extends Launcher {
    private static final String START_CLASS_ATTRIBUTE = "Start-Class";
    protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";
    private final Archive archive;
    private final ClassPathIndexFile classPathIndex;
    public ExecutableArchiveLauncher() {
        try {
            this.archive = createArchive();
            this.classPathIndex = getClassPathIndex(this.archive);
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
protected ExecutableArchiveLauncher(Archive archive) {
        try {
            this.archive = archive;
            this.classPathIndex = getClassPathIndex(this.archive);
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
    protected ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {
        return null;
    }
    @Override
    protected String getMainClass() throws Exception {
        Manifest manifest = this.archive.getManifest();
        String mainClass = null;
        if (manifest != null) {
            mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
        }
        if (mainClass == null) {
            throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
        }
        return mainClass;
    }
    @Override
    protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
        List<URL> urls = new ArrayList<>(guessClassPathSize());
        while (archives.hasNext()) {
            urls.add(archives.next().getUrl());
        }
        if (this.classPathIndex != null) {
            urls.addAll(this.classPathIndex.getUrls());
        }
        return createClassLoader(urls.toArray(new URL[0]));
    }
    private int guessClassPathSize() {
        if (this.classPathIndex != null) {
            return this.classPathIndex.size() + 10;
        }
        return 50;
    }
    @Override
    protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
        Archive.EntryFilter searchFilter = this::isSearchCandidate;
        Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
                (entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
        if (isPostProcessingClassPathArchives()) {
            archives = applyClassPathArchivePostProcessing(archives);
        }
        return archives;
    }
    private boolean isEntryIndexed(Archive.Entry entry) {
        if (this.classPathIndex != null) {
            return this.classPathIndex.containsEntry(entry.getName());
        }
        return false;
    }
    private Iterator<Archive> applyClassPathArchivePostProcessing(Iterator<Archive> archives) throws Exception {
        List<Archive> list = new ArrayList<>();
        while (archives.hasNext()) {
            list.add(archives.next());
        }
        postProcessClassPathArchives(list);
        return list.iterator();
    }
    protected boolean isSearchCandidate(Archive.Entry entry) {
        return true;
    }
    protected abstract boolean isNestedArchive(Archive.Entry entry);
    protected boolean isPostProcessingClassPathArchives() {
        return true;
    }
    protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
    }
    @Override
    protected boolean isExploded() {
        return this.archive.isExploded();
    }
    @Override
    protected final Archive getArchive() {
        return this.archive;
    }
}
java 运行

6.2 Launcher

org.springframework.boot.loader.Launcher

如下则是 Launcher 的源码

  1. launch 方法会首先创建类加载器,而后判断是否 jar 是否在 MANIFEST.MF 文件中设置了 jarmode 属性。

  2. 如果没有设置,launchClass 的值就来自 getMainClass() 返回,该方法由子类实现,返回 MANIFEST.MF 中配置的 START_CLASS_ATTRIBUTE 属性值

  3. 调用 createMainMethodRunner 方法,构建一个 MainMethodRunner 对象并调用其 run 方法

jarmode 是创建 docker 镜像时用到的参数,使用该参数是为了生成带有多个 layer 信息的镜像,这里暂不注意

public abstract class Launcher {
    private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
    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);
    }
    @Deprecated
    protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
        return createClassLoader(archives.iterator());
    }
    protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
        List<URL> urls = new ArrayList<>(50);
        while (archives.hasNext()) {
            urls.add(archives.next().getUrl());
        }
        return createClassLoader(urls.toArray(new URL[0]));
    }
    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(isExploded(), getArchive(), urls, getClass().getClassLoader());
    }
    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);
    }
    protected abstract String getMainClass() throws Exception;
    protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
        return getClassPathArchives().iterator();
    }
    @Deprecated
    protected List<Archive> getClassPathArchives() throws Exception {
        throw new IllegalStateException("Unexpected call to getClassPathArchives()");
    }
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
        String path = (location != null) ? location.getSchemeSpecificPart() : null;
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException("Unable to determine code source archive from " + root);
        }
        return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
    }
    protected boolean isExploded() {
        return false;
    }
    protected Archive getArchive() {
        return null;
    }
}
java 运行

6.3 MainMethodRunner

org.springframework.boot.loader.MainMethodRunner

从名字可以判断这是一个目标类main方法的执行器,此时的 mainClassName 被赋值为 MANIFEST.MF 中配置的 START_CLASS_ATTRIBUTE 属性值,也就是 com.javanorth.hello.HelloApplication,之后便是通过反射执行 HelloApplication 的 main 方法,从而达到启动 Spring Boot 的效果。

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 });
    }
}
java 运行

7.总结

jar 包类似于 zip 压缩文件,只不过相比 zip 文件多了一个 META-INF/MANIFEST.MF 文件,该文件在构建 jar 包时自动创建
想要制作可执行 JAR 包,在 MANIFEST.MF 中指定 Main-Class 是关键。使用 java 执行 jar 包的时候,实际上等同于使用 java 命令执行指定的 Main-Class 程序。

Spring Boot 提供了一个插件 spring-boot-maven-plugin ,用于把程序打包成一个可执行的jar包

使用 java -jar 启动 Spring Boot 的 jar 包,首先调用的入口类是 JarLauncher,内部调用 Launcher 的 launch 后构建 MainMethodRunner 对象,最终通过反射调用 HelloApplication 的 main 方法实现启动效果。

M.扩展

https://mp.weixin.qq.com/s/ar5Sbga_oEdXikD5B-7iqQ

【Spring】SpringBoot内置Tomcat启动原理

【Spring】Spring 外置 tomcat 启动原理

 

【Spring】Spring Boot 为什么可以使用 Jar 包启动-CSDN博客

 

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    

也就是说想要知道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>    

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());    
}    

我们关心一下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;    
   }    
}    

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

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud
  • 视频教程:https://doc.iocoder.cn/video/

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  

repackager生成的MANIFEST.MF文件为以上信息,可以看到两个关键信息Main-ClassStart-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 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>    
**/    

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);    
}    

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);    
   }    
}    

方法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();    
}    

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>    

总结

对于源码分析,这次的较大收获则是不能一下子去追求弄懂源码中的每一步代码的逻辑,即便我知道该方法的作用。我们需要搞懂的是关键代码,以及涉及到的知识点。

我从Maven的自定义插件开始进行追踪,巩固了对Maven的知识点,在这个过程中甚至了解到JDK对jar的读取是有提供对应的工具类。最后最重要的知识点则是自定义类加载器。整个代码下来并不是说代码究竟有多优秀,而是要学习他因何而优秀。

posted @ 2025-09-30 15:31  CharyGao  阅读(8)  评论(0)    收藏  举报