Finalmente entendí el principio de inicio del paquete jar SpringBoot

El paquete jar generado por SpringBoot

El paquete jar ejecutable de Spring Boot también se llama "fat jar", que es un jar que contiene todas las dependencias de tres partes. La mayor diferencia entre este y el paquete jar tradicional es que contiene un directorio lib y un contenedor web integrado.

Estructura de directorio del paquete jar ejecutable

Después de empaquetar a través del comando maven, habrá dos paquetes jar, uno es application-name.version-SNAPSHOT.jar y el otro es application-name.version-SNAPSHOT.jar.original. El último solo contiene recursos locales después de la compilación de la aplicación, mientras que el primero introduce dependencias de terceros relacionadas.

La estructura de directorios después de descomprimir el primero es la siguiente:

Inserte la descripción de la imagen aquí

Este directorio es más complicado que la estructura de empaquetado del comando jar tradicional, y el significado del directorio es el siguiente:

  • BOOT-INF / classes: el directorio almacena los archivos de clases compilados de la aplicación.
  • BOOT-INF / lib: el directorio almacena archivos de paquetes JAR de terceros de los que depende la aplicación.
  • META-INF: el directorio almacena información de empaquetado de la aplicación (coordenadas Maven, archivos pom) y archivos MANIFEST.MF.
  • org: el directorio almacena archivos de clases relacionados con SpringBoot.

Archivo de configuración: MANIFEST.MF

El archivo MANIFEST.MF se encuentra en la carpeta META-INF del paquete jar y su contenido es el siguiente:

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

El método principal de la clase correspondiente al atributo Main-Class se utiliza como entrada del programa para iniciar la aplicación.

El atributo Start-Class define la clase de inicio de nuestro proyecto.

Lanzador de paquetes jar ejecutable: JarLauncher

Cuando se usa el comando java -jar para ejecutar el archivo jar ejecutable de la aplicación Spring Boot, el comando guía el archivo jar ejecutable estándar y lee el valor del atributo Main-Class del archivo META-INF / MANIFEST.MF en el jar. Este valor representa La clase de entrada de ejecución de la aplicación es la clase que contiene el método principal.

Desde el contenido del archivo MANIFEST.MF, puede ver que el atributo Main-Class define org.springframework.boot.loader.JarLauncher, y JarLauncher es el lanzador correspondiente al archivo Jar. Y la clase de inicio MainApplication de nuestro proyecto se define en el atributo Start-Class,

JarLauncher agregará los archivos de clase en BOOT-INF / classes y los archivos jar dependientes en BOOT-INF / lib a la ruta de clase, y luego llamará al atributo Start-Class del archivo META-INF / MANIFEST.MF para completar el inicio de la aplicación.

La relación de herencia de Launcher es la siguiente:

imagen-20200901174219024

Principio de realización del motor de arranque

Clase de inicio: 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);
   }

}

El constructor predeterminado de JarLauncher está vacío. Su clase principal ExecutiveArchiveLauncher llamará al método createArchive de la clase principal Launcher para cargar el paquete jar. Después de que se cargue el paquete jar, podemos obtener todos los recursos dentro.

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

Método principal: lanzamiento (String [] args)

El método de lanzamiento en realidad llama al método de lanzamiento de la clase principal 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);
	}

En pocas palabras, puede crear un jarpaquete de cargador de lectura de clases para garantizar que las BOOT-INF/libclases y el directorio BOOT-classesincrustados jaren la clase se puedan cargar en una aplicación Spring Boot normal, después del inicio de la ejecución.

Método uno: 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();
}

El propósito de este método es, a través del org.springframework.boot.loaderpaquete, establecer la "java.protocol.handler.pkgs"variable de entorno, y así utilizar una clase de implementación personalizada URLStreamHandler Handler, jar:URL de protocolo de tratamiento .

利用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类文件

por ejemplo:

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

Vemos que si hay un paquete jar que contiene un jar, o un paquete jar contiene un archivo de clase en el paquete jar, estará separado por! /. Este método solo puede ser procesado por org.springframework.boot.loader.jar.Handler. Es un protocolo de URL extendido internamente por SpringBoot .

Por lo general, el separador de recursos en el jar es! /. La URL JarFile proporcionada en el JDK solo admite una capa de "! /", Pero Spring Boot extiende este protocolo para admitir múltiples capas de "! /".

Método dos: 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>, El this::isNestedArchivesegmento de código para crear una clase de implementación EntryFilter anónima para filtrar jarpaquetes de directorio no deseado. El propósito es obtener BOOT-INF/classes/clases de directorio filtradas, así como paquetes BOOT-INF/lib/incrustados jar.

// 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> implementación del método getNestedArchives ()

	//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 (Lista de archivos)
// 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());
}

Archivo obtenido en base a una matriz, cree una clase de implementación ClassLoader personalizada LaunchedURLClassLoader, a través de la cual cargar el BOOT-INF/classesdirectorio de clases y el paquete de clases de BOOT-INF/libdirectorios jar.

方法 三 : lanzamiento (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>En: Configure el cargador de clases LaunchedURLClassLoader para garantizar que el jarpaquete cargado sea la clase adecuada.

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

Desde el elemento de configuración del archivo de jarpaquete , obtenemos que Spring Boot establezca la clase de inicio principal .MANIFEST.MFStart-Class

createMainMethodRunner
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
			ClassLoader classLoader) {
    
    
		return new MainMethodRunner(mainClass, args);
	}
correr()
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 });
}

Este método es responsable del inicio real de la aplicación Spring Boot final .

Cargador de clases personalizado SpringBoot: LaunchedURLClassLoader

LaunchedURLClassLoader es spring-boot-loaderun cargador de clases de elementos personalizados para realizar el directorio del jarpaquete META-INF/classesen la clase y META-INF/libel tipo de carga del jarpaquete en línea .

ClassLoader hereda de UrlClassLoader. UrlClassLoader carga la clase basándose en la matriz Url pasada por el parámetro inicial e intenta cargar el archivo de clase desde el recurso apuntado por 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);
   }
}

El método super.loadClass (nombre, resolver) realmente regresará a java.lang.ClassLoader # loadClass (java.lang.String, boolean), siga el mecanismo de delegación principal para encontrar la clase, pero Bootstrap ClassLoader y Extension ClassLoader no se encontrarán Las clases de las que depende fat jar eventualmente llegarán a Application ClassLoader y llamarán a java.net.URLClassLoader # findClass

Por qué introducir un cargador de clases personalizado

Debido a que se realiza SpringBootel anidamiento de paquetes Jar, un paquete Jar puede completar la operación de todo el programa.

La introducción del cargador de clases personalizado es para resolver el problema del paquete jar anidado del paquete jar, el AppClassLoarder que viene con el sistema no admite la lectura del paquete jar anidado

¿Por qué SpringBoot copia todos los archivos de la clase Loader?

Debido a que el programa debe tener una entrada de inicio después de todo, esta entrada debe ser cargada por el cargador de clases de la aplicación, primero cargue el cargador de clases SpringBoot en la memoria y luego cree un cargador de contexto de subprocesos a través de algunas operaciones posteriores para cargar archivos jar de terceros.

Si SpringBoot Class Loaderlo coloca debajo del archivo lib, no se puede cargar en absoluto, porque no se ajusta a una especificación estándar del archivo jar.

Supongo que te gusta

Origin blog.csdn.net/kaihuishang666/article/details/108405691
Recomendado
Clasificación