在上篇文章《springboot应用启动原理(一) 将启动脚本嵌入jar》中介绍了springboot如何将启动脚本与Runnable Jar整合为Executable Jar的原理,使得生成的jar/war文件能够直接启动
本篇将介绍springboot如何扩展URLClassLoader实现嵌套jar的类(资源)加载,以启动咱们的应用。java
本篇示例使用 java8 + grdle4.2 + springboot2.0.0.release 环境
首先,从一个简单的示例开始git
build.gradlegithub
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.javaweb
@SpringBootApplication @RestController public class WebApp { public static void main(String[] args) { SpringApplication.run(WebApp.class, args); } @RequestMapping("/") @GetMapping public String hello() { return "Hello You!"; } }
执行gradle build
构建jar包,里面包含应用程序、第三方依赖以及springboot启动程序,其目录结构以下spring
spring-boot-theory-1.0.0.jar ├── META-INF │ └── MANIFEST.MF ├── BOOT-INF │ ├── classes │ │ └── 应用程序 │ └── lib │ └── 第三方依赖jar └── org └── springframework └── boot └── loader └── springboot启动程序
查看MANIFEST.MF的内容(MANIFEST.MF文件的做用请自行GOOGLE)segmentfault
Manifest-Version: 1.0 Start-Class: com.manerfan.springboot.theory.WebApp Main-Class: org.springframework.boot.loader.JarLauncher
能够看到,jar的启动类为org.springframework.boot.loader.JarLauncher
,而并非咱们的com.manerfan.springboot.theory.WebApp
,应用程序入口类被标记为了Start-Classapi
jar启动并非经过应用程序入口类,而是经过JarLauncher代理启动。其实SpringBoot拥有3中不一样的Launcher:JarLauncher 、WarLauncher 、PropertiesLaunchertomcat
springboot使用Launcher代理启动,其最重要的一点即是能够自定义ClassLoader,以实现对jar文件内(jar in jar)或其余路径下jar、class或资源文件的加载
关于ClassLoader的更多介绍可参考《深刻理解JVM之ClassLoader》springboot
SpringBoot抽象了Archive的概念,一个Archive能够是jar(JarFileArchive),能够是一个文件目录(ExplodedArchive),能够抽象为统一访问资源的逻辑层。oracle
上例中,spring-boot-theory-1.0.0.jar既为一个JarFileArchive,spring-boot-theory-1.0.0.jar!/BOOT-INF/lib下的每个jar包也是一个JarFileArchive
将spring-boot-theory-1.0.0.jar解压到目录spring-boot-theory-1.0.0,则目录spring-boot-theory-1.0.0为一个ExplodedArchive
public interface Archive extends Iterable<Archive.Entry> { // 获取该归档的url URL getUrl() throws MalformedURLException; // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF Manifest getManifest() throws IOException; // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar List<Archive> getNestedArchives(EntryFilter filter) throws IOException; }
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.
按照定义,JarLauncher能够加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class
其实JarLauncher实现很简单
public class JarLauncher extends ExecutableArchiveLauncher { public JarLauncher() {} public static void main(String[] args) throws Exception { new JarLauncher().launch(args); } }
其主入口新建了JarLauncher并调用父类Launcher中的launch方法启动程序
再建立JarLauncher时,父类ExecutableArchiveLauncher找到本身所在的jar,并建立archive
public abstract class ExecutableArchiveLauncher extends Launcher { private final Archive archive; public ExecutableArchiveLauncher() { try { // 找到本身所在的jar,并建立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)); } }
在Launcher的launch方法中,经过以上archive的getNestedArchives方法找到/BOOT-INF/lib下全部jar及/BOOT-INF/classes目录所对应的archive,经过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用
public abstract class Launcher { protected void launch(String[] args) throws Exception { JarFile.registerUrlProtocolHandler(); // 生成自定义ClassLoader ClassLoader classLoader = createClassLoader(getClassPathArchives()); // 启动应用 launch(args, getMainClass(), classLoader); } protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { // 将自定义ClassLoader设置为当前线程上下文类加载器 Thread.currentThread().setContextClassLoader(classLoader); // 启动应用 createMainMethodRunner(mainClass, args, classLoader).run(); } } public abstract class ExecutableArchiveLauncher extends Launcher { protected List<Archive> getClassPathArchives() throws Exception { // 获取/BOOT-INF/lib下全部jar及/BOOT-INF/classes目录对应的archive 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 { // 加载应用程序主入口类 Class<?> mainClass = Thread.currentThread().getContextClassLoader() .loadClass(this.mainClassName); // 找到main方法 Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); // 调用main方法,并启动 mainMethod.invoke(null, new Object[] { this.args }); } }
至此,才执行咱们应用程序主入口类的main方法,全部应用程序类文件都可经过/BOOT-INF/classes加载,全部依赖的第三方jar都可经过/BOOT-INF/lib加载
在分析LaunchedURLClassLoader前,首先了解一下URLStreamHandler
java中定义了URL的概念,并实现多种URL协议(见URL) http file ftp jar 等,结合对应的URLConnection能够灵活地获取各类协议下的资源
public URL(String protocol, String host, int port, String file, URLStreamHandler handler) throws MalformedURLException
对于jar,每一个jar都会对应一个url,如jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/
jar中的资源,也会对应一个url,并以'!/'分割,如jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class
对于原始的JarFile URL,只支持一个'!/',SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源,如jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class
自定义URL的类格式为[pkgs].[protocol].Handler,在运行Launcher的launch方法时调用了JarFile.registerUrlProtocolHandler()
以注册自定义的 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(); }
在处理以下URL时,会循环处理'!/'分隔符,从最上层出发,先构造spring-boot-theory.jar的JarFile,再构造spring-aop-5.0.4.RELEASE.jar的JarFile,最后构造指向SpringProxy.class的
JarURLConnection ,经过JarURLConnection的getInputStream方法获取SpringProxy.class内容
jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class
从一个URL,到读取其中的内容,整个过程为
URLClassLoader能够经过原始的jar协议,加载jar中从class文件
LaunchedURLClassLoader 经过扩展的jar协议,以实现jar in jar这种状况下的class文件加载
构建war包很简单
apply plugin: 'war'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
@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!"; } }
构建出的war包,其目录机构为
spring-boot-theory-1.0.0.war ├── META-INF │ └── MANIFEST.MF ├── WEB-INF │ ├── classes │ │ └── 应用程序 │ └── lib │ └── 第三方依赖jar │ └── lib-provided │ └── 与内嵌容器相关的第三方依赖jar └── org └── springframework └── boot └── loader └── springboot启动程序
MANIFEST.MF内容为
Manifest-Version: 1.0 Start-Class: com.manerfan.springboot.theory.WebApp Main-Class: org.springframework.boot.loader.WarLauncher
此时,启动类变为了org.springframework.boot.loader.WarLauncher
,查看WarLauncher实现,其实与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); } }
差异仅在于,JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar
如此依赖,构建出的war便支持两种启动方式
./spring-boot-theory-1.0.0.war start
PropretiesLauncher 的实现与 JarLauncher WarLauncher 的实现极为类似,经过PropretiesLauncher能够实现更为轻量的thin jar,其实现方式可自行查阅源码