(转)springboot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载
转:https://segmentfault.com/a/1190000013532009
在上篇文章《springboot应用启动原理(一) 将启动脚本嵌入jar》中介绍了springboot如何将启动脚本与Runnable Jar整合为Executable Jar的原理,使得生成的jar/war文件可以直接启动
本篇将介绍springboot如何扩展URLClassLoader实现嵌套jar的类(资源)加载,以启动我们的应用。
本篇示例使用 java8 + grdle4.2 + springboot2.0.0.release 环境
首先,从一个简单的示例开始
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!";
}
}
执行gradle build
构建jar包,里面包含应用程序、第三方依赖以及springboot启动程序,其目录结构如下
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)
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-Class
jar启动并不是通过应用程序入口类,而是通过JarLauncher代理启动。其实SpringBoot拥有3中不同的Launcher:JarLauncher 、WarLauncher 、PropertiesLauncher
springboot使用Launcher代理启动,其最重要的一点便是可以自定义ClassLoader,以实现对jar文件内(jar in jar)或其他路径下jar、class或资源文件的加载
关于ClassLoader的更多介绍可参考《深入理解JVM之ClassLoader》
Archive
- 归档文件
- 通常为tar/zip等格式压缩包
- jar为zip格式归档文件
SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层。
上例中,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;
}
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.
按照定义,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
在分析LaunchedURLClassLoader前,首先了解一下URLStreamHandler
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,到读取其中的内容,整个过程为
- 注册一个Handler处理‘jar:’这种协议
- 扩展JarFile、JarURLConnection,处理jar in jar的情况
- 循环处理,找到内层资源
- 通过getInputStream获取资源内容
URLClassLoader可以通过原始的jar协议,加载jar中从class文件
LaunchedURLClassLoader 通过扩展的jar协议,以实现jar in jar这种情况下的class文件加载
WarLauncher
构建war包很简单
- build.gradle中引入插件
apply plugin: 'war'
- build.gradle中将内嵌容器相关依赖设为provided
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
- 修改WebApp内容,重写SpringBootServletInitializer的configure方法
@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
- 部署到Tomcat容器下
PropertiesLauncher
PropretiesLauncher 的实现与 JarLauncher WarLauncher 的实现极为相似,通过PropretiesLauncher可以实现更为轻量的thin jar,其实现方式可自行查阅源码
总结
- SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载
- SpringBoot通过扩展URLClassLoader--LauncherURLClassLoader,实现了jar in jar中class文件的加载
- JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动
- WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动
(转)springboot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载的更多相关文章
- SpringBoot FatJar启动原理
目录 SpringBoot FatJar启动原理 背景 储备知识 URLStreamHandler Archive 打包 SpringBoot启动 扩展 SpringBoot FatJar启动原理 背 ...
- springboot之启动原理解析
前言 SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开SpringBoot的神秘面 ...
- springboot之启动原理解析及源码阅读
前言 SpringBoot为我们做的自动配置,确实方便快捷,但是对于新手来说,如果不大懂SpringBoot内部启动原理,以后难免会吃亏.所以这次博主就跟你们一起一步步揭开SpringBoot的神秘面 ...
- 【SpringBoot】 理解SpringBoot的启动原理
前言 前文已经介绍了Spring Bean的生命周期,那么使用过程中发现SpringBoot 的启动非常快捷,本文将介绍SpringBoot的内部启动原理. 启动过程 如上图所示,我们先分析下Spri ...
- DB数据源之SpringBoot+MyBatis踏坑过程(二)手工配置数据源与加载Mapper.xml扫描
DB数据源之SpringBoot+MyBatis踏坑过程(二)手工配置数据源与加载Mapper.xml扫描 liuyuhang原创,未经允许进制转载 吐槽之后应该有所改了,该方式可以作为一种过渡方式 ...
- Linux内核启动代码分析二之开发板相关驱动程序加载分析
Linux内核启动代码分析二之开发板相关驱动程序加载分析 1 从linux开始启动的函数start_kernel开始分析,该函数位于linux-2.6.22/init/main.c start_ke ...
- IDEA是如何导入项目的,及启动导入项目遇到的问题:无法加载主类的一连串问题
1.启动报错误: 找不到或无法加载主类 org.spring.springboot.Application 可能在工程下面有多个module,然后,module里面的iml配置文件不止一个,删除留主的 ...
- pyspider 示例二 升级完整版绕过懒加载,直接读取图片
pyspider 示例二 升级完整版绕过懒加载,直接读取图片,见[升级写法处] #!/usr/bin/env python # -*- encoding: utf-8 -*- # Created on ...
- 创建spring boot项目并添加多个模块时,启动报 错误: 找不到或无法加载主类
最近建个项目发现启动报,找不到或无法加载主类,想想肯定是自己配置出问题了,经过排查确实出问题了,(根pom中的bulid为移到子模块中去导致的),下面演示下正确的创建子模块的步奏 1. 创 ...
随机推荐
- WCF权限认证多种方式
WCF身份验证一般常见的方式有:自定义用户名及密码验证.X509证书验证.ASP.NET成员资格(membership)验证.SOAP Header验证.Windows集成验证.WCF身份验证服务(A ...
- sts测试流程
测试目的: 测试安全补丁打上了没 测试前提: 1.发货版本,user debug版本,相应安全补丁已合入,测试工具与安全补丁是对应的 2.selinux:Enable 3.连接ADB,stay awa ...
- debian7下安装eclipse
apt-get install build-essential 完成后从eclipse官网上下载C++专用的版本,直接解压缩即可
- WildFly的学习
1. WildFly介绍: WildFly,前身是JBoss AS,从V8开始为区别于JBoss EAP,更名为WildFly. 由红帽 (Red Hat)开发,是另一个功能齐全且经过认证的应用服务器 ...
- ELK+Filebeat (1)
1 Filebeat介绍 Filebeat是Beat成员之一,基于Go语言,无任何依赖,并且比logstash更加轻量,非常适合安装在生产机器上,不会带来过高的资源占用,轻量意味着简单,所以Fileb ...
- jmeter 非GUI执行测试,没有响应数据保存到jtl文件办法
估计是jmeter为了减轻客户机负担,就没又默认把这些信息保存,如果想要保存,也可以,需要做出如下配置: 修改bin目录下的user.properties文件,追加配置: jmeter.save.sa ...
- 二叉树BinTree4种遍历及其应用
前序遍历 template<class T> void BinTree<T>::PreOrder(BinTreeNode<T>*subTree){ //前序遍历以s ...
- zabbix3.0自动发现磁盘并监控磁盘IO
Zabbix 版本:3.0 操作系统:Ubuntu16.04 操作环境,在被监控的主机上安装zabbix agent.安装方式为源码包安装. 简要安装步骤: 参考:https://www.zabbix ...
- k3 cloud中库存转移处理
有个苗木基地的苗木要转移到另一个,是做那个单据 解决办法:两个基地是同一组织 做直接调拨单就行了 ,不同组织做调拨申请单,然后做 分布式调出 分布式调入
- windows下nvm的安装及使用
由于更新了npm版本之后导致npm的命令都会报错,一顿百度,明白了nvm可以管理node版本的,下面是操作过程: 如果在安装nvm之前已经下载了node 需要把node卸载!!! 需要把node卸载! ...