Comprensión profunda del principio de carga de SpringBoot FatJar

Prefacio

Este artículo presentará desde la perspectiva del código fuente cómo SpringBoot personaliza ClassLoader para cargar FatJar y cómo FatJar lee clases en archivos jar anidados en el jar. Algunos conocimientos de ClassLoader estarán involucrados en el proceso.

Conceptos básicos del cargador de clases

Mecanismo de delegación parental

La delegación parental significa que al cargar una clase, el cargador de clases principal la carga primero. Si el cargador de clases principal no puede cargar en el directorio de carga, entonces la carga el cargador secundario. El propósito de esto es garantizar que la misma La clase solo se carga una vez.

  • BootStrap Classloader: inicia el cargador de clases, principalmente cargando la biblioteca de clases principal
  • Extension ClassLoader: cargador de clases de extensión, en java.ext.dirs o jre/lib/ext
  • Application ClassLoader: AppClassLoader, cargador de clases de aplicaciones, el proyecto de inicio de Java ordinario se carga mediante AppClassLoader

<p>Algunos contenedores se basan en AppClassLoader para extender ClassLoader personalizado, con AppClassLoader como cargador de clases principal. Por ejemplo, Tomcat usa WebAppClassLoader y SpringBoot usa LaunchedURLClassLoader. </p>

Además, nuestro ClassLoader personalizado generalmente se extiende en función de AppClassLoader.

La delegación parental se refiere al objeto ClassLoader (instancia de ClassLoader), no a la herencia de la clase ClassLoader. Es un miembro del objeto que implementa la delegación parental. El método loadClass del código fuente de la clase de carga ClassLoader es el siguiente:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 加载过的类不再加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                //从父类加载器加载
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    //父类加载器找不到,调用findClass方法
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

Primero, determine si la clase se ha cargado. De lo contrario, la clase principal la cargará. Si la clase principal no se puede cargar, se llamará al método findClass.

 

Cargador de clases de URL

Como sugiere su nombre, la clase URLClassLoader se usa principalmente para cargar algunos recursos URL, como clases y paquetes jar. Puede reutilizar URLClassLoader para extender su propio ClassLoader. De hecho, Tomcat y SpringBoot también hacen esto.

¿Cuál es la URL?

Se refiere a un recurso, que incluye, entre otros, archivos de clase, paquetes jar e incluso archivos personalizados.

¿Cómo maneja la URL el flujo de bytes?

Dado que la URL puede ser cualquier archivo o recurso, la forma de leer el archivo también es diferente.

Cuando abrimos la función URL.openConnection, podemos ver que la lógica es procesada por el controlador URLStreamHandler.

    public URLConnection openConnection() throws java.io.IOException {
        return handler.openConnection(this);
    }

De esta manera, la URL extendida puede implementar la lógica de carga personalizada de la URL reescribiendo la clase URLStreamHandler. De hecho, FatJAR de SpringBoot también hace esto.

Hay una función getInputStream en URLConnection

public InputStream getInputStream() lanza IOException;

Lo que se devuelve es un flujo de bytes binario. Con InputStream se pueden construir recursos y clases.

 

Los entresijos de URLClassLoader

URLClassLoader es una clase derivada de ClassLoader y los paquetes Jar se pueden cargar a través de addURL.

    protected void addURL(URL url) {
        ucp.addURL(url);
    }

Y ucp es URLClassPath, que gestiona todas las URL agregadas.

URLClassLoader cumple con el mecanismo de delegación principal y anula el método findClass para buscar Class.

    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(
                new PrivilegedExceptionAction<Class<?>>() {
                    public Class<?> run() throws ClassNotFoundException {
                        String path = name.replace('.', '/').concat(".class");
                        Resource res = ucp.getResource(path, false);
                        if (res != null) {
                            try {
                                return defineClass(name, res);
                            } catch (IOException e) {
                                throw new ClassNotFoundException(name, e);
                            }
                        } else {
                            return null;
                        }
                    }
                }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

La clase de recurso se obtiene de Resource res = ucp.getResource(path, false); ucp es la URLClassPath anterior.

Vaya al método ucp.getResource y podrá ver los detalles de implementación de getResource, que es la carga de la URL que mencioné anteriormente.

El principio de carga de SpringBoot FatJar

Estructura FatJar

Descomprima el paquete jar de un proyecto SpringBoot y podrá ver la estructura de FatJar.

 

BOOT-INF es el archivo empaquetado por el proyecto, BOOT-INF/classes almacena el código del proyecto y los archivos de configuración, y BOOT-INF/lib almacena los paquetes jar anidados dependientes (es decir, jar en jar).
org.springframework.boot. * es la clase de inicio SpringBoot

Abra META-INF/MANIFEST.MF y podrá ver la clase de inicio del paquete jar.

Manifest-Version: 1.0
Implementation-Title: LittleBoy
Implementation-Version: 1.0-SNAPSHOT
Archiver-Version: Plexus Archiver
Built-By: liubs
Implementation-Vendor-Id: com.thanple.little.boy
Spring-Boot-Version: 1.5.1.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.thanple.little.boy.web.WebApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_191
Implementation-URL: http://projects.spring.io/spring-boot/LittleBoy/

Main-Class es la clase de inicio del paquete Jar, org.springframework.boot.loader.JarLauncher. Start-Class es la función principal para iniciar el proyecto que escribe.

FatJar no es una estructura de paquete jar estándar. Solo org.springframework.boot se ajusta al estándar del paquete jar. No se puede cargar todo el contenido del directorio BOOT-INF , por lo que SpringBoot necesita un cargador personalizado.

SpringBoot inicia el proceso de carga de clases

El org.springframework.boot.loader.JarLauncher mencionado hace un momento es la función principal de inicio real de SpringBoot ¿Cuál es la diferencia entre esta y la clase de función principal escrita localmente (se supone que es WebApplication)? La función principal local ordinaria, sin Jar anidado, se carga e inicia directamente mediante AppClassLoader, mientras que SpringBoot escribe su propio conjunto de cargadores de clases, LaunchedURLClassLoader.

 

La función principal de JarLauncher sirve como punto de entrada y la función Launcher.launch se llama al crear una instancia.

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

Cuando ingresa a Launcher.launch, puede ver que se crea un LaunchedURLClassLoader y LaunchedURLClassLoader carga las clases y archivos jar anidados en FatJar (es decir, jar en jar).

    protected void launch(String[] args) throws Exception {
        if (!this.isExploded()) {
            JarFile.registerUrlProtocolHandler();
        }
        //加载URL,由LaunchedURLClassLoader加载
        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();
        this.launch(args, launchClass, classLoader);
    }
    //创建LaunchedURLClassLoader
    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(this.isExploded(), this.getArchive(), urls, this.getClass().getClassLoader());
    }

Luego use LaunchedURLClassLoader para llamar a la función principal WebApplication escrita por usted mismo en this.launch(args, launchClass, classLoader)

public class MainMethodRunner {
    //...
    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);
    }
}

Aquí, la función principal de WebApplication, la función principal escrita por mí, se llama de forma reflexiva, completando así un conjunto de procesos de carga.

El enlace del proceso completo se muestra en la figura:

LanzadoURLClassLoader

LaunchedURLClassLoader, como cargador de clases principal para SpringBoot para cargar FatJar, hereda URLClassLoader y reescribe el método loadClass, pero esta clase no ve mucha lógica central.

	@Override
	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
			try {
				Class<?> result = loadClassInLaunchedClassLoader(name);
				if (resolve) {
					resolveClass(result);
				}
				return result;
			}
			catch (ClassNotFoundException ex) {
			}
		}
		if (this.exploded) {
			return super.loadClass(name, resolve);
		}
		Handler.setUseFastConnectionExceptions(true);
		try {
			try {
				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);
		}
	}

Distingue la diferencia entre org.springframework.boot.loader.jarmode y otros archivos jar, define el nombre del paquete y la lógica central todavía llama a super.loadClass, que es URLClassLoader.loadClass.

 

URLClassLoader.loadClass es el proceso de carga de URL anterior, entonces, ¿cómo distinguen la lógica FatJar y Jar ordinario?

Toda la diferencia radica en el proceso de carga de la URL en Launcher.launch:

 ClassLoader classLoader = this.createClassLoader(this.getClassPathArchivesIterator());

Puedes ver la lógica de getClassPathArchivesIterator en ExecutableArchiveLauncher

    protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
        Archive.EntryFilter searchFilter = this::isSearchCandidate;
        Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter, (entry) -> {
            return this.isNestedArchive(entry) && !this.isEntryIndexed(entry);
        });
        if (this.isPostProcessingClassPathArchives()) {
            archives = this.applyClassPathArchivePostProcessing(archives);
        }

        return archives;
    }

Entre ellos, getNestedArchives obtiene la información del archivo en el paquete jar (jar en jar) en FatJar, BOOT-INF/classes y BOOT-INF/lib/*.jar. Cada elemento se trata como un archivo y se devuelve una lista de iteradores.

 

Cargando FatJar

SpringBoot redefine (para ser precisos, extiende) la implementación jar en org.springframework.boot.loader.jar, incluidos JavaFile, JavaEntry, JarURLConnection, etc.

 

This.archive.getNestedArchives llega hasta el final para ver el código fuente. Puede ver que hay una función en JarFile.

  private JarFile createJarFileFromEntry(org.springframework.boot.loader.jar.JarEntry entry) throws IOException {
        return entry.isDirectory() ? this.createJarFileFromDirectoryEntry(entry) : this.createJarFileFromFileEntry(entry);
    }

BOOT-INF/classes como directorio construirá un JavaFile, y BOOT-INF/lib/*.jar como un jar anidado (jar dentro de jar) construirá otro JavaFile

Entonces, ¿cómo lee los datos este Jar anidado (jar en jar)?

Podemos encontrar la lógica en JavaFile.createJarFileFromFileEntry

    private JarFile createJarFileFromFileEntry(org.springframework.boot.loader.jar.JarEntry entry) throws IOException {
        if (entry.getMethod() != 0) {
            throw new IllegalStateException("Unable to open nested entry '" + entry.getName() + "'. It has been compressed and nested jar files must be stored without compression. Please check the mechanism used to create your executable jar file");
        } else {
            RandomAccessData entryData = this.entries.getEntryData(entry.getName());
            return new JarFile(this.rootFile, this.pathFromRoot + "!/" + entry.getName(), entryData, JarFile.JarFileType.NESTED_JAR);
        }
    }

Las entradas se refieren a cada dato en el FatJar externo, como BOOT-INF/lib/a.jar, BOOT-INF/lib/b.jar. Como jar en jar, primero obtenga el flujo de datos RandomAccessData en este jar en frasco. .

Luego, construya un JavaFile a través del flujo de datos RandomAccessData, de modo que se pueda leer y acceder al Jar construido en jar.

La ruta de jar in jar probablemente se vea así:

LittleBoy-1.0.jar!/BOOT-INF/lib/a.jar

LittleBoy-1.0.jar!/BOOT-INF/lib/b.jar

LittleBoy-1.0.jar!/BOOT-INF/lib/c.jar

La URL obtenida por JavaFile es la URL que URLClassLoader puede cargar.

    public URL getUrl() throws MalformedURLException {
        if (this.url == null) {
            String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
            file = file.replace("file:////", "file://");
            this.url = new URL("jar", "", -1, file, new Handler(this));
        }

        return this.url;
    }

Al devolver la URL aquí, se agrega un nuevo controlador para implementar la lógica de URLStreamHandler que procesa la URL. 

Abrimos el código fuente de este Handler, org.springframework.boot.loader.jar.Handler

Puede ver la lógica de procesamiento para obtener URLConnection

    protected URLConnection openConnection(URL url) throws IOException {
        if (this.jarFile != null && this.isUrlInJarFile(url, this.jarFile)) {
            return JarURLConnection.get(url, this.jarFile);
        } else {
            try {
                return JarURLConnection.get(url, this.getRootJarFileFromUrl(url));
            } catch (Exception var3) {
                return this.openFallbackConnection(url, var3);
            }
        }
    }

La JarURLConnection obtenida tiene un método getInputStream que devuelve el flujo de bytes buscado.

¿Cómo se carga el flujo de bytes devuelto en LaunchedURLClassLoader?

Como se mencionó anteriormente, LaunchedURLClassLoader hereda URLClassLoader. Según el flujo de bytes devuelto por la URL, el recurso se puede obtener en función de ClassName. El siguiente es el método URLClassLoader.getResource.

Y para este Recurso, puedes definirClase, es decir, definir una clase

El siguiente código está extraído de URLClassLoader.findClass

    protected Class<?> findClass(final String name)
        throws ClassNotFoundException
    {
        //省略部分代码
        ...
        
        String path = name.replace('.', '/').concat(".class");
        Resource res = ucp.getResource(path, false);
        if (res != null) {
            try {
                return defineClass(name, res);
            } catch (IOException e) {
                throw new ClassNotFoundException(name, e);
            }
        } else{
            return null;
        }
        
    }

 

Hasta ahora, se ha explicado completamente el proceso de carga de un FatJar.

Lei Jun anunció la arquitectura completa del sistema ThePaper OS de Xiaomi, diciendo que la capa inferior ha sido completamente reestructurada. Yuque anunció la causa de la falla y el proceso de reparación el 23 de octubre. Nadella, CEO de Microsoft: Abandonar Windows Phone y el negocio móvil fue una decisión equivocada Las tasas de uso de Java 11 y Java 17 excedieron el acceso de Hugging Face a Java 8. La interrupción de la red de Yuque duró aproximadamente 10 horas y ahora ha vuelto a la normalidad. Oracle lanzó extensiones de desarrollo de Java para Visual Studio Code . La Administración Nacional de Datos oficialmente Musk reveló : Done mil millones si Wikipedia pasa a llamarse "Enciclopedia Weiji" USDMySQL 8.2.0 GA
{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/3276866/blog/10108193
Recomendado
Clasificación