Los puntos de conocimiento involucrados incluyen principalmente el ciclo de vida de Maven y los complementos personalizados, las clases de herramientas proporcionadas por JDK para paquetes jar y cómo se extiende Springboot, y finalmente el cargador de clases personalizado.
Complemento Spring-Boot-Maven
El paquete jar ejecutable de SpringBoot, también conocido como jar gordo, es un paquete jar que contiene todas las dependencias de terceros. El paquete jar incorpora todas las dependencias excepto la máquina virtual Java, y es un paquete jar todo en uno.
La diferencia directa entre el paquete generado por el complemento común maven-jar-plugin y el paquete generado por spring-boot-maven-plugin es que hay dos partes principales añadidas al fat jar. La primera parte es el directorio lib, que almacena los archivos jar de los que depende Maven.Archivo de paquete, la segunda parte son las clases relacionadas con el cargador de arranque Spring.
fat jar //estructura de directorio ├─BOOT-INF │ ├─clases │ └─lib ├─META-INF │ ├─experto │ ├─aplicación.propiedades │ ├─MANIFEST.MF └─org └─estructura de muelles └─arranque └─cargador ├─archivo ├─datos ├─frasco └─util
Es decir, si desea saber cómo se genera el fat jar, debe conocer el mecanismo de funcionamiento de spring-boot-maven-plugin, y spring-boot-maven-plugin es un complemento personalizado, por lo que debemos sepa que el complemento personalizado de Maven cómo funciona
Complemento personalizado de Maven
Maven tiene tres ciclos de vida independientes: limpio, predeterminado y sitio, y cada ciclo de vida contiene algunas fases, las fases son secuenciales y las fases posteriores dependen de las anteriores. La fase de fase del ciclo de vida está ligada al objetivo objetivo del complemento para completar la tarea de construcción real.
<complemento> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-complemento</artifactId> <ejecuciones> <ejecución> <objetivos> <objetivo>reempaquetar</objetivo> </objetivos> </ejecución> </ejecuciones> </complemento>
El objetivo de reempaquetado correspondiente se ejecutará en org.springframework.boot.maven.RepackageMojo#execute. La lógica principal de este método es llamar a org.springframework.boot.maven.RepackageMojo#repackage
reempaquetado vacío privado () lanza MojoExecutionException { // Obtenga el jar generado por maven-jar-plugin, el nombre final tendrá el sufijo .original Fuente del artefacto = getSourceArtifact(); //El archivo final, el frasco Gordo Destino del archivo = getTargetFile(); // Obtenga el reempaquetador y vuelva a empaquetarlo en un archivo jar ejecutable Repackager repackager = getRepackager (fuente.getFile()); //Buscar y filtrar los jars de los que depende el proyecto en tiempo de ejecución Set<Artefacto> artefactos = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); //Convertir artefactos en bibliotecas Bibliotecas bibliotecas = new ArtifactsLibraries(artefactos, this.requiresUnpack, obtenerRegistro()); tratar { // Proporcionar secuencia de comandos de inicio de Spring Boot LaunchScript launchScript = getLaunchScript(); //Ejecutar la lógica de reempaquetado para generar el fat jar final repackager.repackage(objetivo, bibliotecas, launchScript); } captura (Excepción IOex) { lanza una nueva MojoExecutionException(ex.getMessage(), ex); } //Actualizar fuente al archivo xxx.jar.original updateArtifact(origen, destino, reempaquetador.getBackupFile()); }
Nos preocupamos por el método org.springframework.boot.maven.RepackageMojo#getRepackager, y sabiendo cómo se genera Repackager, podemos inferir aproximadamente la lógica de empaquetado interna.
Repackager privado getRepackager (fuente del archivo) { Repackager repackager = new Repackager(fuente, this.layoutFactory); repackager.addMainClassTimeoutWarningListener( nuevo LoggingMainClassTimeoutWarningListener()); //Establecer el nombre de la clase principal, si no se especifica, encontrará la primera clase que contiene el método principal, y reempaquetar finalmente establecerá org.springframework.boot.loader.JarLauncher repackager.setMainClass(this.mainClass); if (este.diseño!= nulo) { getLog().info("Diseño: " + este.diseño); //Enfóquese en el diseño y finalmente devuelva org.springframework.boot.loader.tools.Layouts.Jar repackager.setLayout(this.layout.layout()); } reenvasador de devolución; }
/** * Diseño JAR ejecutable. */ Jar de clase estática pública implementa RepackagingLayout { @Anular cadena pública getLauncherClassName() { devolver "org.springframework.boot.loader.JarLauncher"; } @Anular public String getLibraryDestination(String nombreBiblioteca, alcance de LibraryScope) { devuelve "BOOT-INF/lib/"; } @Anular public String getClassesLocation() { devolver ""; } @Anular public String getRepackagedClassesLocation() { devuelve "BOOT-INF/clases/"; } @Anular booleano público isExecutable() { devolver verdadero; } }
diseño Podemos traducirlo en diseño de archivo o diseño de directorio. El código es claro y claro. Al mismo tiempo, debemos prestar atención al objeto org.springframework.boot.loader.JarLauncher. Inferido del nombre, esto es probable para devolver la clase de inicio para archivos jar ejecutables.
Contenido del archivo MANIFEST.MF
Versión del manifiesto: 1.0 Título de implementación: oneday-auth-server Implementación-Versión: 1.0.0-SNAPSHOT Archiver-Versión: Plexus Archiver Construido por: un día Id. de proveedor de implementación: com.oneday Spring-Boot-Versión: 2.1.3.RELEASE Clase principal: org.springframework.boot.loader.JarLauncher Clase de inicio: com.oneday.auth.Application Spring-Boot-Classes: BOOT-INF/clases/ Spring-Boot-Lib: BOOT-INF/lib/ Creado por: Apache Maven 3.3.9 Build-Jdk: 1.8.0_171
El archivo MANIFEST.MF generado por repackager es la información anterior, y puede ver dos datos clave: Main-Class y Start-Class. Podemos ir más allá, la entrada de inicio del programa no es la principal definida en nuestro SpringBoot, sino JarLauncher#main, y luego usar la reflexión para llamar al método principal de la clase de inicio definida.
Lanzador de tarros
Introducción a las clases clave
- java.util.jar.JarFile lee el archivo jar proporcionado por la clase de herramienta JDK
- org.springframework.boot.loader.jar.JarFileSpringboot-loader hereda la clase JarFile proporcionada por JDK
- entradas del archivo jar proporcionadas por la clase de herramienta java.util.jar.JarEntryDK
- org.springframework.boot.loader.jar.JarEntry Springboot-loader hereda la clase JarEntry proporcionada por JDK
- org.springframework.boot.loader.archive.Archive La capa de recursos de acceso unificado abstraída por Springboot
- Abstracción de archivo de paquete JarFileArchive jar
- Directorio de archivos ExplodedArchive
Aquí nos enfocamos en describir el rol de JarFile, cada JarFileArchive corresponderá a un JarFile. Durante la construcción, se analizará la estructura interna para obtener cada clase de archivo o carpeta en el paquete jar. Podemos echar un vistazo a las anotaciones para esta clase.
/* Variante extendida de {@link java.util.jar.JarFile} que se comporta de la misma manera pero * ofrece la siguiente funcionalidad adicional. * * <li>Un {@link JarFile} anidado se puede {@link #getNestedJarFile(ZipEntry) obtenido} basado en * en cualquier entrada del directorio.</li> * <li>Se puede {@link #getNestedJarFile(ZipEntry) obtenido} un {@link JarFile} anidado para * Archivos JAR incrustados (siempre que su entrada no esté comprimida).</li> </ul> **/
El separador de recursos en jar es !/. La URL de JarFile proporcionada por JDK solo admite un '!/', y Spring Boot amplía este protocolo para admitir varios '!/', que pueden representar jar en jar, jar en directorio, fat jar recursos.
Mecanismo de carga de clase personalizado
- El más básico: Bootstrap ClassLoader (carga clases en el directorio /lib del JDK)
- Sub-básico: Extensión ClassLoader (carga clases en el directorio /lib/ext del JDK)
- Común: Application ClassLoader (clases en el propio classpath del programa)
En primer lugar, es importante prestar atención al mecanismo de delegación principal. Si el ClassLoader más básico puede cargar una clase, el ClassLoader de alto nivel no puede cargarla. Esto es para introducir una clase que no es JDK con el mismo nombre de clase para el alcance incorrecto.
En segundo lugar, si bajo este mecanismo, los diversos archivos jar de terceros de los que depende el jar gordo no están en la ruta de clase del programa, es decir, si usamos el mecanismo de delegación principal, no podemos obtener el paquete jar del que dependemos. en absoluto. , por lo que debemos modificar el método de búsqueda de clases en el mecanismo de delegación principal y personalizar el mecanismo de carga de clases.
Primero presente brevemente LaunchedURLClassLoader en Springboot2, que hereda java.net.URLClassLoader y reescribe java.lang.ClassLoader#loadClass(java.lang.String, boolean), y luego analizamos cómo modifica el mecanismo de delegación principal.
Mencionamos anteriormente que Spring boot admite múltiples '!/' para representar múltiples jars, y nuestro problema es cómo encontrar estos paquetes de jars múltiples. Echemos un vistazo al constructor de LaunchedURLClassLoader.
public LaunchedURLClassLoader(URL[] urls, padre de ClassLoader) { super(urls, padre); }
La anotación urls explica las URL desde las que cargar clases y recursos, es decir, todas las clases y recursos de los que depende el paquete fat jar, pasan el parámetro urls a la clase principal java.net.URLClassLoader y la clase principal java. net.URLClassLoader#findClass Ejecute el método de clase de búsqueda, y la fuente de búsqueda de la clase es el parámetro urls pasado por el constructor.
//Implementación de LaunchedURLClassLoader clase protegida <?> loadClass (nombre de cadena, resolución booleana) lanza ClassNotFoundException { Handler.setUseFastConnectionExceptions(verdadero); tratar { tratar { //Intente definir el paquete donde se encuentra la clase de acuerdo con el nombre de la clase, es decir, java.lang.Package, para asegurarse de que el manifiesto coincidente en el jar in jar se pueda asociar con el paquete asociado // definePackageIfNecessary(nombre); } captura (IllegalArgumentException ex) { // Tolerar la condición de carrera debido a la capacidad paralela if (getPackage(nombre) == nulo) { // Esto nunca debería suceder como indica IllegalArgumentException // que el paquete ya ha sido definido y, por lo tanto, // getPackage(nombre) no debería devolver un valor nulo. //La excepción aquí muestra que la función del método definePackageIfNecessary es en realidad filtrar previamente los paquetes que no se pueden encontrar throw new AssertionError("Paquete" + nombre + "ya ha sido" + "definido pero no se pudo encontrar"); } } volver super.loadClass(nombre, resolver); } por fin { Handler.setUseFastConnectionExceptions(falso); } }
El método super.loadClass(name, resolve) en realidad volverá a java.lang.ClassLoader#loadClass(java.lang.String, boolean), siguiendo el mecanismo de delegación principal para encontrar clases, pero Bootstrap ClassLoader y Extension ClassLoader no podrán para encontrarlas Las clases de las que depende el fat jar eventualmente llegarán a Application ClassLoader y llamarán a java.net.URLClassLoader#findClass
Cómo empezar realmente
La mayor diferencia entre Springboot2 y Springboot1 es que Springboo1 iniciará un nuevo subproceso para ejecutar la lógica de llamada de reflexión correspondiente, mientras que SpringBoot2 elimina el paso de crear un nuevo subproceso.
El método es org.springframework.boot.loader.Launcher#launch(java.lang.String[], java.lang.String, java.lang.ClassLoader) La lógica de llamada de reflexión es relativamente simple, así que no la analizaré aquí, el punto más crítico Sí, antes de llamar al método principal, establezca el cargador de clases de contexto del subproceso actual en LaunchedURLClassLoader
lanzamiento vacío protegido (String[] args, String mainClass, ClassLoader classLoader) lanza una excepción { Thread.currentThread().setContextClassLoader(classLoader); createMainMethodRunner(mainClass, args, classLoader).run(); }
Manifestación
public static void main(String[] args) lanza ClassNotFoundException, MalformedURLException { JarFile.registerUrlProtocolHandler(); // Construya el cargador de clases LaunchedURLClassLoader. Aquí se utilizan dos URL, que corresponden a los paquetes dependientes spring-boot-loader y spring-boot en el paquete jar. Están separados por "!/" y requieren org.springframework.boot. loader.jar Procesamiento del controlador LaunchedURLClassLoader classLoader = new LaunchedURLClassLoader( nueva URL[] { nueva URL("jar:archivo:/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!/") , nueva URL("jar:archivo:/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!/") }, Aplicación.clase.getClassLoader()); // clase de carga // Estas dos clases se encontrarán en el segundo paso de la búsqueda local (método findClass de URLClassLoader) classLoader.loadClass("org.springframework.boot.loader.JarLauncher"); classLoader.loadClass("org.springframework.boot.SpringApplication"); // Usar el orden de carga predeterminado que se encuentra en ApplicationClassLoader en el tercer paso classLoader.loadClass("org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration"); // SpringApplication.run(Application.class, args); }
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-loader --> <dependencia> <groupId>org.springframework.boot</groupId> <artifactId>cargador de arranque de resorte</artifactId> <versión>2.1.3.LIBERAR</versión> </dependencia> <dependencia> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-complemento</artifactId> <versión>2.1.3.LIBERAR</versión> </dependencia>
Resumir
Para el análisis del código fuente, la mayor ganancia esta vez es que no puedo intentar comprender la lógica de cada paso del código en el código fuente a la vez, incluso si conozco la función del método. Lo que necesitamos entender es el código clave y los puntos de conocimiento involucrados.
Comencé a rastrear desde el complemento personalizado de Maven y consolidé mi conocimiento de Maven. En el proceso, incluso aprendí que JDK proporciona clases de herramientas correspondientes para leer archivos jar. El último y más importante punto de conocimiento es el cargador de clases personalizado. El código completo no es para decir qué tan bueno es el código, sino para saber por qué es bueno.