Spring boot extends URLClassLoader to implement nested jar loading

Posted by greywire on Fri, 13 Dec 2019 10:57:49 +0100

To: https://segmentfault.com/a/11900013532009

In the last article Spring boot application startup principle (1) embedding startup script into jar The principle of how springboot integrates the startup script and Runnable Jar into Executable Jar is introduced in so that the generated jar/war file can be started directly
This article will show how springboot extends URLClassLoader to implement class (resource) loading of nested jar s to start our application.

This sample uses the java8 + grdle4.2 + springboot2.0.0.release environment

First, start with a simple example

build.gradle

 

group 'com.manerfan.spring'
version '1.0.0'

apply plugin: 'java'
apply plugin: 'java-library'

sourceCompatibility = 1.8

buildscript {
    ext {
        springBootVersion = '2.0.0.RELEASE'
    }

    repositories {
        mavenLocal()
        maven {
            name 'aliyun maven central'
            url 'http://maven.aliyun.com/nexus/content/groups/public'
        }
    }

    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

bootJar {
    launchScript()
}

repositories {
    mavenLocal()
    maven {
        name 'aliyun maven central'
        url 'http://maven.aliyun.com/nexus/content/groups/public'
    }
}

dependencies {
    api 'org.springframework.boot:spring-boot-starter-web'
}

WebApp.java

 

@SpringBootApplication
@RestController
public class WebApp {
    public static void main(String[] args) {
        SpringApplication.run(WebApp.class, args);
    }

    @RequestMapping("/")
    @GetMapping
    public String hello() {
        return "Hello You!";
    }
}

Execute gradle build to build jar package, which contains application program, third-party dependency and spring boot program. The directory structure is as follows

 

spring-boot-theory-1.0.0.jar
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── application program
│   └── lib
│       └── Third party reliance jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot Startup program

View the content of MANIFEST.MF (please use Google for the function of MANIFEST.MF file)

 

Manifest-Version: 1.0
Start-Class: com.manerfan.springboot.theory.WebApp
Main-Class: org.springframework.boot.loader.JarLauncher

As you can see, the startup class of jar is org.springframework.boot.loader.JarLauncher, not our com.manerfan.springboot.theory.WebApp, and the application entry class is marked as start class

jar startup is not through the application entry class, but through the JarLauncher agent. In fact, SpringBoot has three different launchers: JarLauncher ,WarLauncher ,PropertiesLauncher

 

Spring boot uses the Launcher agent to start. The most important point is that you can customize the ClassLoader to load the jar, class or resource files within the jar file (jar in jar) or in other paths
For more information about ClassLoader, please refer to Deep understanding of ClassLoader of JVM

Archive

  • Archive file
  • Usually in the form of tar/zip
  • jar is a zip file

Spring boot abstracts the concept of Archive. An Archive can be jar (jar file Archive), can be an expanded Archive, and can be abstracted as a logical layer for unified access to resources.

In the above example, spring-boot-theory-1.0.0.jar is not only a JarFileArchive, but also a JarFileArchive under spring-boot-theory-1.0.0.jar!/BOOT-INF/lib
Unzip spring-boot-theory-1.0.0.jar to the directory spring-boot-theory-1.0.0, and the directory spring-boot-theory-1.0.0 is an exploded archive

 

public interface Archive extends Iterable<Archive.Entry> {
    // Get the url of the archive
    URL getUrl() throws MalformedURLException;
    // Get jar!/META-INF/MANIFEST.MF or [ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // Get jar!/BOOT-INF/lib/*.jar or [ArchiveDir]/BOOT-INF/lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}

JarLancher

Launcher for JAR based archives. This launcher assumes that dependency jars are included inside a  /BOOT-INF/lib directory and that application classes are included inside a  /BOOT-INF/classes directory.

By definition, JarLauncher can load jar under internal / BOOT-INF/lib and application class under / BOOT-INF/classes

In fact, the implementation of JarLauncher is very simple

 

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

Its main entry creates a new JarLauncher and calls the launch method initiator in the parent class Launcher
When creating JarLauncher again, the parent class ExecutableArchiveLauncher finds its own jar and creates archive

 

public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;
    public ExecutableArchiveLauncher() {
        try {
            // Find your own jar and create Archive
            this.archive = createArchive();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}

public abstract class Launcher {
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
        String path = (location == null ? null : location.getSchemeSpecificPart());
        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);
        }
        return (root.isDirectory() ? new ExplodedArchive(root)
                : new JarFileArchive(root));
    }
}

In the launch method of Launcher, use the getNestedArchives method of the above archive to find the archive corresponding to all jar s and / BOOT-INF/classes directory under / BOOT-INF/lib, generate launchedr url classloader through the URLs of these archives, and set it as thread context class loader to start the application

 

public abstract class Launcher {
    protected void launch(String[] args) throws Exception {
        JarFile.registerUrlProtocolHandler();
        // Generate custom ClassLoader
        ClassLoader classLoader = createClassLoader(getClassPathArchives());
        // Startup application
        launch(args, getMainClass(), classLoader);
    }

    protected void launch(String[] args, String mainClass, ClassLoader classLoader)
            throws Exception {
        // Set the custom ClassLoader as the current thread context class loader
        Thread.currentThread().setContextClassLoader(classLoader);
        // Startup application
        createMainMethodRunner(mainClass, args, classLoader).run();
    }
}

public abstract class ExecutableArchiveLauncher extends Launcher {
    protected List<Archive> getClassPathArchives() throws Exception {
        // Get the archive corresponding to all jar s and / BOOT-INF/classes under / BOOT-INF/lib
        List<Archive> archives = new ArrayList<>(
                this.archive.getNestedArchives(this::isNestedArchive));
        postProcessClassPathArchives(archives);
        return archives;
    }
}

public class MainMethodRunner {
    // Start-Class in MANIFEST.MF
    private final String mainClassName;

    private final String[] args;

    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args == null ? null : args.clone());
    }

    public void run() throws Exception {
        // Load application main entry class
        Class<?> mainClass = Thread.currentThread().getContextClassLoader()
                .loadClass(this.mainClassName);
        // Find the main method
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        // Call the main method and start
        mainMethod.invoke(null, new Object[] { this.args });
    }
}

So far, the main method of our application main entry class is executed. All application class files can be loaded through / BOOT-INF/classes, and all dependent third-party jar s can be loaded through / BOOT-INF/lib

LaunchedURLClassLoader

Before analyzing the launched urlclassloader, first take a look at the URLStreamHandler

URLStreamHandler

java defines the concept of URL and implements various URL protocols (see URL )http file ftp jar and the corresponding URLConnection can flexibly obtain resources under various protocols

 

public URL(String protocol,
           String host,
           int port,
           String file,
           URLStreamHandler handler)
    throws MalformedURLException

For jars, each jar corresponds to a url, such as
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/

The resources in the jar also correspond to a url and are separated by '! /', such as
jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

For the original JarFile URL, only one '! /' is supported. SpringBoot extends this protocol to support multiple '! /' to implement jar in jar resources, such as
jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

The class format of the custom URL is [pkgs].[protocol].Handler. JarFile.registerUrlProtocolHandler() was called to register the custom URL when the launch method of the Launcher was run Handler

 

private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
public static void registerUrlProtocolHandler() {
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
            : handlers + "|" + HANDLERS_PACKAGE));
    resetCachedUrlHandlers();
}

When processing the following URL, the '! /' separator will be processed circularly. Starting from the top layer, first construct the JarFile of spring-boot-theory.jar, then construct the JarFile of spring-aop-5.0.4.RELEASE.jar, and finally construct the JarFile pointing to spring proxy.class
JarURLConnection Obtain the spring proxy.class content through the getInputStream method of JarURLConnection

jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

From a URL to reading the content, the whole process is

  • Register a Handler to handle the protocol 'jar:'
  • Extend JarFile and JarURLConnection to handle jar in jar
  • Loop processing, find inner resources
  • Get resource content through getInputStream

URLClassLoader can load the class file from the jar through the original jar protocol
LaunchedURLClassLoader class file loading in the case of jar in jar through the extended jar protocol

WarLauncher

It's easy to build a war package

  1. Add in to build.gradle: apply plugin: 'war'
  2. build.gradle set the dependency of embedded container to providedprovidedruntime 'org. Springframework. Boot: spring boot starter Tomcat'
  3. Modify WebApp content and override the configure method of SpringBootServletInitializer

 

@SpringBootApplication
@RestController
public class WebApp extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(WebApp.class, args);
    }

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(WebApp.class);
    }

    @RequestMapping("/")
    @GetMapping
    public String hello() {
        return "Hello You!";
    }
}

Build the war package with the directory organization of

 

spring-boot-theory-1.0.0.war
├── META-INF
│   └── MANIFEST.MF
├── WEB-INF
│   ├── classes
│   │   └── application program
│   └── lib
│       └── Third party reliance jar
│   └── lib-provided
│       └── Third party dependencies related to embedded containers jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot Startup program

The content of MANIFEST.MF is

 

Manifest-Version: 1.0
Start-Class: com.manerfan.springboot.theory.WebApp
Main-Class: org.springframework.boot.loader.WarLauncher

At this time, the startup class changes to org.springframework.boot.loader.WarLauncher, and the implementation of WarLauncher is not very different from that of JarLauncher

 

public class WarLauncher extends ExecutableArchiveLauncher {
    private static final String WEB_INF = "WEB-INF/";
    private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
    private static final String WEB_INF_LIB = WEB_INF + "lib/";
    private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";

    public WarLauncher() {
    }

    @Override
    public boolean isNestedArchive(Archive.Entry entry) {
        if (entry.isDirectory()) {
            return entry.getName().equals(WEB_INF_CLASSES);
        }
        else {
            return entry.getName().startsWith(WEB_INF_LIB)
                    || entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
        }
    }

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

The only difference is that JarLauncher will search for jar in BOOT-INF/classes directory and BOOT-INF/lib directory when building LauncherURLClassLoader, and WarLauncher will search for jar in WEB-INFO/classes directory and WEB-INFO/lib and WEB-INFO/lib provided directory when building LauncherURLClassLoader

Based on this dependency, the built war supports two startup modes

  • Run. / spring-boot-theory-1.0.0.war start directly
  • Deploy to Tomcat container

PropertiesLauncher

The implementation of propertieslauncher is very similar to that of JarLauncher WarLauncher. Through propertieslauncher, you can realize a lighter thin jar. You can refer to the source code by yourself

summary

  • Spring boot implements the loading of resources in jar in jar by extending JarFile, JarURLConnection and URLStreamHandler
  • Spring boot implements the loading of class files in jar in jar by extending URLClassLoader--LauncherURLClassLoader
  • JarLauncher starts the fat jar by loading the jar files in the BOOT-INF/classes directory and the BOOT-INF/lib directory
  • WarLauncher realizes the direct start of war file and the start in web container by loading WEB-INF/classes directory and jar files under WEB-INF/lib and WEB-INF/lib provided directory

Topics: Programming Spring SpringBoot Maven Gradle