Finally understood the principle of SpringBoot jar package startup

The jar package generated by SpringBoot

The executable jar package of Spring Boot is also called "fat jar", which is a jar that contains all three-party dependencies. The biggest difference between it and the traditional jar package is that it contains a lib directory and embedded web container.

Directory structure of executable jar package

After packaging through the maven command, there will be two jar packages, one is application-name.version-SNAPSHOT.jar and the other is application-name.version-SNAPSHOT.jar.original. The latter only contains local resources after application compilation, while the former introduces related third-party dependencies.

The directory structure after decompressing the former is as follows:

Insert picture description here

This directory is more complicated than the traditional jar command packaging structure, and the meaning of the directory is as follows:

  • BOOT-INF/classes: The directory stores the compiled class files of the application.
  • BOOT-INF/lib: The directory stores third-party JAR package files that the application depends on.
  • META-INF: The directory stores application packaging information (Maven coordinates, pom files) and MANIFEST.MF files.
  • org: The directory stores SpringBoot related class files.

Configuration file: MANIFEST.MF

The MANIFEST.MF file is located in the META-INF folder of the jar package, and its contents are as follows:

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: cindy
Start-Class: com.shinemo.wangge.web.MainApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.3.3.RELEASE
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_212
Main-Class: org.springframework.boot.loader.JarLauncher

The main method of the class corresponding to the Main-Class attribute is used as the program entry to start the application.

The Start-Class attribute defines the startup class of our project.

Executable jar package launcher: JarLauncher

When using the java -jar command to execute the executable jar file of the Spring Boot application, the command guides the standard executable jar file and reads the Main-Class attribute value of the META-INF/MANIFEST.MF file in the jar. This value represents The application execution entry class is the class containing the main method.

From the content of the MANIFEST.MF file, you can see that the Main-Class attribute defines org.springframework.boot.loader.JarLauncher, and JarLauncher is the launcher corresponding to the Jar file. And the startup class MainApplication of our project is defined in the Start-Class attribute,

JarLauncher will add the class files under BOOT-INF/classes and the dependent jars under BOOT-INF/lib to the classpath, and then call the Start-Class attribute of the META-INF/MANIFEST.MF file to complete the startup of the application.

The inheritance relationship of Launcher is as follows:

image-20200901174219024

Starter realization principle

Startup class: JarLauncher

//JarLauncher.java

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 {
    
    
      //程序的入口
      new JarLauncher().launch(args);
   }

}

The default constructor of JarLauncher is empty. Its parent class ExecutiveArchiveLauncher will call the createArchive method of the parent class Launcher to load the jar package. After the jar package is loaded, we can get all the resources inside.

	//JarLauncher.java
	
	//JarLauncher默认构造函数
	public JarLauncher() {
    
    
	}
	
//ExecutableArchiveLauncher.java	

public ExecutableArchiveLauncher() {
    
    
		try {
    
    
      //开始加载jar包
			this.archive = createArchive();
		}
		catch (Exception ex) {
    
    
			throw new IllegalStateException(ex);
		}
	}
//Launcher.java	

protected final Archive createArchive() throws Exception {
    
    
    //通过获取当前Class类的信息,查找到当前归档文件的路径
		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);
		}
    //如果是目录,则创建ExplodedArchive,否则创建JarFileArchive
		return (root.isDirectory() ? new ExplodedArchive(root)
				: new JarFileArchive(root));
	}

Core method: launch(String[] args)

The launch method actually calls the launch method of the parent class Launcher

// Launcher.java	

protected void launch(String[] args) throws Exception {
    
    
    //注册 Spring Boot 自定义的 URLStreamHandler 实现类,用于 jar 包的加载读取, 可读取到内嵌的jar包
		JarFile.registerUrlProtocolHandler();
    //创建自定义的 ClassLoader 实现类,用于从 jar 包中加载类。
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
    //执行我们声明的 Spring Boot 启动类,进行 Spring Boot 应用的启动。
		launch(args, getMainClass(), classLoader);
	}

Simply put, it can create a reading jarloader package of classes to ensure BOOT-INF/libclasses and directory BOOT-classesembedded jarin the class can be loaded into a normal, after the execution start Spring Boot application.

Method one: registerUrlProtocolHandler

JarFile.registerUrlProtocolHandler();
// JarFile.java

public static void registerUrlProtocolHandler() {
    
    
    // 获得 URLStreamHandler 的路径
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    // 将 Spring Boot 自定义的 HANDLERS_PACKAGE(org.springframework.boot.loader) 补充上去
    System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
                                          : handlers + "|" + HANDLERS_PACKAGE));
    // 重置已缓存的 URLStreamHandler 处理器们
    resetCachedUrlHandlers();
}

The purpose of this method is through the org.springframework.boot.loaderpackage to set the "java.protocol.handler.pkgs"environment variable, thus to use a custom implementation class URLStreamHandler Handler, treatment jar:protocol URL.

利用java url协议实现扩展原理,自定义jar协议
将org.springframework.boot.loader包 追加到java系统 属性java.protocol.handler.pkgs中,实现自定义jar协议

java会在java.protocol.handler.pkgs系统属性指定的包中查找与协议同名的子包和名为Handler的类,
即负责处理当前协议的URLStreamHandler实现类必须在 <包名>.<协议名定义的包> 中,并且类名称必须为Handler
例如:
org.springframework.boot.loader.jar.Handler这个类 将用于处理jar协议

这个jar协议实现作用:
默认情况下,JDK提供的ClassLoader只能识别jar中的class文件以及加载classpath下的其他jar包中的class文件。
对于jar包中的jar包是无法加载的
所以spring boot 自己定义了一套URLStreamHandler实现类和JarURLConnection实现类,用来加载jar包中的jar包的class类文件

for example:

jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/

jar:file:C:/Users/Administrator/Desktop/demo/demo/target/jarlauncher-0.0.1-SNAPSHOT.jar!/lib/spring-boot-1.5.10.RELEASE.jar!/org/springframework/boot/loader/JarLauncher.class

We see that if there is a jar package that contains a jar, or a jar package contains a class file in the jar package, it will be separated by !/. This method can only be processed by org.springframework.boot.loader.jar.Handler. It is a URL protocol extended internally by SpringBoot .

Usually, the resource separator in the jar is !/. The JarFile URL provided in the JDK only supports one layer of "!/", but Spring Boot extends this protocol to support multiple layers of "!/".

Method two: createClassLoader

ClassLoader classLoader = createClassLoader(getClassPathArchives());
getClassPathArchives()
// ExecutableArchiveLauncher.java

@Override
protected List<Archive> getClassPathArchives() throws Exception {
    
    
 // <1> 获得所有 Archive
 List<Archive> archives = new ArrayList<>(
   this.archive.getNestedArchives(this::isNestedArchive));
 // <2> 后续处理:是个空方法
 postProcessClassPathArchives(archives);
 return archives;
}

<1>, The this::isNestedArchivecode segment to create an anonymous EntryFilter implementation class for filtering jarpackets unwanted directory. The purpose is to get filtered, BOOT-INF/classes/directory classes, as well as BOOT-INF/lib/embedded jarpackage.

// JarLauncher.java

static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

static final String BOOT_INF_LIB = "BOOT-INF/lib/";

@Override
protected boolean isNestedArchive(Archive.Entry entry) {
    
    
    // 如果是目录的情况,只要 BOOT-INF/classes/ 目录
 if (entry.isDirectory()) {
    
    
  return entry.getName().equals(BOOT_INF_CLASSES);
 }
 // 如果是文件的情况,只要 BOOT-INF/lib/ 目录下的 `jar` 包
 return entry.getName().startsWith(BOOT_INF_LIB);
}

<1> getNestedArchives() method implementation

	//JarFileArchive.java
	
	@Override
	public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
    
    
		List<Archive> nestedArchives = new ArrayList<>();
		for (Entry entry : this) {
    
    
			if (filter.matches(entry)) {
    
    
				nestedArchives.add(getNestedArchive(entry));
			}
		}
		return Collections.unmodifiableList(nestedArchives);
	}
createClassLoader(List archives)
// ExecutableArchiveLauncher.java

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    
    
 // 获得所有 Archive 的 URL 地址
    List<URL> urls = new ArrayList<>(archives.size());
 for (Archive archive : archives) {
    
    
  urls.add(archive.getUrl());
 }
 // 创建加载这些 URL 的 ClassLoader
 return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    
    
 return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

Archive obtained based on an array, create a custom ClassLoader implementation class LaunchedURLClassLoader, through which to load the BOOT-INF/classesclass directory, and the BOOT-INF/libdirectory jarclass package.

方法三:launch(String[] args, String mainClass, ClassLoader classLoader)

launch(args, getMainClass(), classLoader);
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
  throws Exception {
    
    
    // <1> 设置 LaunchedURLClassLoader 作为类加载器
 Thread.currentThread().setContextClassLoader(classLoader);
 // <2> 创建 MainMethodRunner 对象,并执行 run 方法,启动 Spring Boot 应用
 createMainMethodRunner(mainClass, args, classLoader).run();
}

<1>At: Set LaunchedURLClassLoader class loader as to guarantee that the jarloaded package to the appropriate class.

getMainClass()
// ExecutableArchiveLauncher.java

@Override
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);
 }
 return mainClass;
}

From the jarpackage MANIFEST.MFfile Start-Classconfiguration item ,, we get Spring Boot set the main startup class.

createMainMethodRunner
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
			ClassLoader classLoader) {
    
    
		return new MainMethodRunner(mainClass, args);
	}
run()
public void run() throws Exception {
    
    
    // <1> 加载 Spring Boot
   Class<?> mainClass = Thread.currentThread().getContextClassLoader()
         .loadClass(this.mainClassName);
  // <2> 反射调用 main 方法
   Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
   mainMethod.invoke(null, new Object[] {
    
     this.args });
}

This method is responsible for the real startup of the final Spring Boot application .

SpringBoot custom class loader: LaunchedURLClassLoader

LaunchedURLClassLoader is spring-boot-loadercustom items class loader to realize the jarpackage META-INF/classesdirectory in the class and META-INF/libinline jarpackage type of load .

The ClassLoader inherits from UrlClassLoader. UrlClassLoader loads the class by relying on the Url array passed in by the initial parameter, and tries to load the Class file from the resource pointed to by Url

//LaunchedURLClassLoader.java

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

The method super.loadClass(name, resolve) will actually return to java.lang.ClassLoader#loadClass(java.lang.String, boolean), follow the parent delegation mechanism to find the class, but Bootstrap ClassLoader and Extension ClassLoader will not be found The classes that fat jar depends on will eventually come to Application ClassLoader and call java.net.URLClassLoader#findClass

Why introduce a custom class loader

Because SpringBootthe nesting of Jar packages is realized, one Jar package can complete the operation of the entire program.

The introduction of custom class loader is to solve the problem of jar package nesting jar package, the AppClassLoarder that comes with the system does not support reading nested jar package

Why does SpringBoot copy all the files under the Loader class?

Because the program must have a startup entry after all, this entry has to be loaded by the application class loader, first load the SpringBoot Class Loader into memory, and then create a thread context loader through some subsequent operations to load third-party jars.

If SpringBoot Class Loaderyou put it under the lib file, it cannot be loaded at all, because it does not conform to a standard specification of the jar file.

Guess you like

Origin blog.csdn.net/kaihuishang666/article/details/108405691