SpringBoot 的jar包为什么可以直接启动?

??首先,先准备一个jar包,我这里准备了一个demo-0.0.1-SNAPSHOT.jar;先来看看jar包里面的目录结构:

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
39
40
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── com
│   │       └── sf
│   │           └── demo
│   │               └── DemoApplication.class
│   └── lib
│       ├── spring-boot-2.1.3.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.1.3.RELEASE.jar
│       ├── spring-boot-starter-2.1.3.RELEASE.jar
│       ├── 这里省略掉很多jar包
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.sf
│           └── demo
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── LaunchedURLClassLoader.class
                ├── Launcher.class
                ├── 省略class
                ├── archive
                │   ├── Archive$Entry.class
                │   ├── 省略class
                ├── data
                │   ├── RandomAccessData.class
                │   ├── 省略class
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── 省略class
                └── util
                    └── SystemPropertyUtils.class

这个文件目录分为BOOT-INF/classesBOOT-INF/libMETA-INForg

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

  • BOOT-INF/lib:主要存放应用依赖的jar包文件

  • META-INF:主要存放mavenMANIFEST.MF文件

  • org:主要存放springboot相关的class文件

??当你使用命令java -jar demo-0.0.1-SNAPSHOT.jar时,它会找到META-INF下的MANIFEST.MF文件,可以从文件中发现,其内容中的Main-Class属性值为org.springframework.boot.loader.JarLauncher,并且项目的引导类定义在Start-Class属性中,值为com.sf.demo.DemoApplication,该属性是由springboot引导程序启动需要的,JarLauncher就是对应的jar文件的启动器.

1
2
3
4
5
6
7
8
9
10
11
12
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.sf.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.0.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher

??启动类org.springframework.boot.loader.JarLauncher并非是项目中引入的类,而是spring-boot-maven-plugin插件repackage追加进去的.

探索JarLauncher的实现原理

??当执行java -jar命令或执行解压后的org.springframework.boot.loader.JarLauncher类时,JarLauncher会将BOOT-INF/classes下的类文件和BOOT-INF/lib下依赖的jar加入到classpath下,最后调用META-INF下的MANIFEST.MF文件的Start-Class属性来完成应用程序的启动,也就是说它是springboot loader提供了一套标准用于执行springboot打包出来的JAR包.

JarLauncher重点类的介绍:
  • java.util.jar.JarFileJDK工具类,用于读取JAR文件的内容

  • org.springframework.boot.loader.jar.JarFile:继承于JDK工具类JarFile类并扩展了一些嵌套功能

  • java.util.jar.JarEntryJDK工具类,此类用于表示JAR文件条目

  • org.springframework.boot.loader.jar.JarEntry:也是继承于JDK工具类JarEntry

  • org.springframework.boot.loader.archive.Archivespring boot loader抽象出来的统一访问资源的接口

  • org.springframework.boot.loader.archive.JarFileArchiveJAR文件的实现

  • org.springframework.boot.loader.archive.ExplodedArchive:文件目录的实现

在项目里面添加一个依赖配置,就可以看JarLauncher的源码:

1
2
3
4
5
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-loader</artifactId>
  <scope>provided</scope>
</dependency>

org.springframework.boot.loader.ExecutableArchiveLauncher
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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);
   }

  //start
   public static void main(String[] args) throws Exception {
      new JarLauncher().launch(args);
   }

}
org.springframework.boot.loader.Launcher
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
/**
 *
 * 启动程序的基类,该启动程序可以使用一个或多个支持的完全配置的类路径来启动应用程序
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @since 1.0.0
 */
public abstract class Launcher {

   private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";

   /**
    * 启动应用程序,此方法是子类方法{@code public static void main(String[] args)}调用的初始入口点
    * @param args the incoming arguments
    * @throws Exception if the application fails to launch
    */
   protected void launch(String[] args) throws Exception {
      if (!isExploded()) {
         //①注册一个自定义URL的jar协议
         JarFile.registerUrlProtocolHandler();
      }
      //②创建指定archive的类加载器
      ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
      String jarMode = System.getProperty("jarmode");
      //③获取Start-Class属性对应的com.sf.demo.DemoApplication
      String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
      //④利用反射调用Start-Class,执行main方法
      launch(args, launchClass, classLoader);
   }
}

①注册一个自定义URLJAR协议

org.springframework.boot.loader.jar.JarFile#registerUrlProtocolHandler

spring boot loader扩展了URL协议,将包名org.springframework.boot.loader追加到java系统属性java.protocol.handler.pkgs中,该包下存在协议对应的Handler类,即org.springframework.boot.loader.jar.Handler其实现协议为JAR.

1
2
3
4
5
6
7
8
9
/**
 * 注册一个'java.protocol.handler.pkgs'属性,让URLStreamHandler处理jar的URL
 */
public static void registerUrlProtocolHandler() {
   String handlers = System.getProperty(PROTOCOL_HANDLER, "");
   System.setProperty(PROTOCOL_HANDLER,
         ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
   resetCachedUrlHandlers();
}
org.springframework.boot.loader.jar.JarFile#resetCachedUrlHandlers
1
2
3
4
5
6
7
8
9
10
11
/**
 * 防止已经使用了jar协议,需要重置URLStreamHandlerFactory缓存的处理程序。
 */
private static void resetCachedUrlHandlers() {
   try {
      URL.setURLStreamHandlerFactory(null);
   }
   catch (Error ex) {
      // Ignore
   }
}

②创建指定archive的类加载器

org.springframework.boot.loader.ExecutableArchiveLauncher#getClassPathArchivesIterator
1
2
3
4
5
6
7
8
9
10
@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;
}
org.springframework.boot.loader.Launcher#createClassLoader(java.util.Iterator)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 创建一个指定的archives的类加载器
 * @param archives the archives
 * @return the classloader
 * @throws Exception if the classloader cannot be created
 * @since 2.3.0
 */
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
   List<URL> urls = new ArrayList<>(50);
   while (archives.hasNext()) {
      Archive archive = archives.next();
      urls.add(archive.getUrl());
      archive.close();
   }
   return createClassLoader(urls.toArray(new URL[0]));
}
org.springframework.boot.loader.Launcher#createClassLoader(java.util.Iterator)
1
2
3
4
5
6
7
8
9
/**
     * 创建一个指定的自定义URL的类加载器
     * @param urls the URLs
     * @return the classloader
     * @throws Exception if the classloader cannot be created
     */
    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(isExploded(), urls, getClass().getClassLoader());
    }

③获取Start-Class属性对应的com.sf.demo.DemoApplication

org.springframework.boot.loader.ExecutableArchiveLauncher#getMainClass

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected String getMainClass() throws Exception {
   Manifest manifest = this.archive.getManifest();
   String mainClass = null;
   if (manifest != null) {
      //从配置文件获取Start-Class对应的com.sf.demo.DemoApplication
      mainClass = manifest.getMainAttributes().getValue("Start-Class");
   }
   if (mainClass == null) {
      throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
   }
   return mainClass;
}

④利用反射调用Start-Class,执行main方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
     * 启动应用程序
     * @param args the incoming arguments
     * @param launchClass the launch class to run
     * @param classLoader the classloader
     * @throws Exception if the launch fails
     */
    protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
    //将当前线程的上下文类加载器设置成LaunchedURLClassLoader
        Thread.currentThread().setContextClassLoader(classLoader);
    //启动应用程序
        createMainMethodRunner(launchClass, args, classLoader).run();
    }
  /**
     * 构造一个MainMethodRunner类,来启动应用程序
     * @param mainClass the main class
     * @param args the incoming arguments
     * @param classLoader the classloader
     * @return the main method runner
     */
    protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
        return new MainMethodRunner(mainClass, args);
    }
org.springframework.boot.loader.MainMethodRunner
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
/**
 * 用来调用main方法的工具类
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @since 1.0.0
 */
public class MainMethodRunner {

   private final String mainClassName;

   private final String[] args;

   /**
    * Create a new {@link MainMethodRunner} instance.
    * @param mainClass the main class
    * @param args incoming arguments
    */
   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 });
   }

}

我们先了解一下类加载机制:

在这里插入图片描述

??我们知道双亲委派模型的原则,当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试加载任务。

??由于demo-0.0.1-SNAPSHOT.jar中依赖的各个JDK包,并不在程序自己的classpath下,它是存放在JDK包里的BOOT-INF/lib目录下,如果我们采用双亲委派机制的话,根本获取不到我们JAR包的依赖,因此我们需要破坏双亲委派模型,使用自定义类加载机制。

??在springboot2中,LaunchedURLClassLoader自定义类加载器继承URLClassLoader,重写了loadClass方法;在JDK里面,JAR的资源分隔符是!/,但是JDK中只支持一个!/,这无法满足spring boot loader的需求,so,springboot扩展了JarFile,从这里可以看到org.springframework.boot.loader.jar.JarFile#createJarFileFromEntry,它支持了多个!/,表示jar文件嵌套JAR文件、JAR文件嵌套Directory.

org.springframework.boot.loader.LaunchedURLClassLoader
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
public class LaunchedURLClassLoader extends URLClassLoader {
  @Override
  protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
     if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
        ........省略代码
     try {
        try {
           //尝试根据类名去定义类所在的包,即java.lang.Package,确保jar文件嵌套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.
              throw new AssertionError("Package " + name + " has already been defined but it could not be found");
           }
        }
        return super.loadClass(name, resolve);
     }
     finally {
        Handler.setUseFastConnectionExceptions(false);
     }
  }
}
org.springframework.boot.loader.LaunchedURLClassLoader#definePackageIfNecessary
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
/**
 * 在进行调用findClass方法之前定义一个包,确保嵌套jar与包关联
 * @param className the class name being found
 */
private void definePackageIfNecessary(String className) {
   int lastDot = className.lastIndexOf('.');
   if (lastDot >= 0) {
      String packageName = className.substring(0, lastDot);
      if (getPackage(packageName) == null) {
         try {
            definePackage(className, packageName);
         }
         catch (IllegalArgumentException ex) {
            // Tolerate race condition due to being parallel capable
            if (getPackage(packageName) == null) {
               // This should never happen as the IllegalArgumentException
               // indicates that the package has already been defined and,
               // therefore, getPackage(name) should not have returned null.
               throw new AssertionError(
                     "Package " + packageName + " has already been defined but it could not be found");
            }
         }
      }
   }
}

总结:

1、springboot 扩展了JDKURL协议;

2、springboot 自定义了类加载器LaunchedURLClassLoader

3、Launcher利用反射调用StartClass#main方法(org.springframework.boot.loader.MainMethodRunner#run);

4、springboot1springboot2主要区别是在启动应用程序时,springboot1会启动一个线程去反射调用,springboot2直接调用;

参考资料:

https://www.yht7.com/news/18153

https://segmentfault.com/a/1190000016192449

https://cloud.tencent.com/developer/article/1469863

https://www.cnblogs.com/xxzhuang/p/11194559.html

http://www.10qianwan.com/articledetail/577937.html

https://blog.csdn.net/shenchaohao12321/article/details/103543446

https://fangjian0423.github.io/2017/05/31/springboot-executable-jar/

如果文章存在问题、不妥的地方或者有疑惑麻烦给我留言,大家一起学习,感谢您~
欢迎关注我的微信公众号,里面有很多干货,各种面试题
在这里插入图片描述