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.
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
- Add in to build.gradle: apply plugin: 'war'
- build.gradle set the dependency of embedded container to providedprovidedruntime 'org. Springframework. Boot: spring boot starter Tomcat'
- 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