(转)springboot应⽤启动原理(⼆)扩展URLClassLoader实现嵌
套jar加载
转:segmentfault/a/1190000013532009
在上篇⽂章中介绍了springboot如何将启动脚本与Runnable Jar整合为Executable Jar的原理,使得⽣成的jar/war⽂件可以直接启动
本篇将介绍springboot如何扩展URLClassLoader实现嵌套jar的类(资源)加载,以启动我们的应⽤。
本篇⽰例使⽤ java8 + grdle4.2 + springboot2.lease 环境
⾸先,从⼀个简单的⽰例开始
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 'maven.aliyun/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 'maven.aliyun/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:、、
springboot使⽤Launcher代理启动,其最重要的⼀点便是可以⾃定义ClassLoader,以实现对jar⽂件内(jar in jar)或其他路径下jar、class或资源⽂件的加载
关于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 = CodeSource();
URI location = (codeSource == null ? null : Location().toURI());
String path = (location == null ? null : SchemeSpecificPart());
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!ists()) {
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 {
// ⽣成⾃定义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<>(
NestedArchives(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 = DeclaredMethod("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协议(见)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⽅法时调⽤了isterUrlProtocolHandler()以注册⾃定义的
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 = Property(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的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⽂件
通过扩展的jar协议,以实现jar in jar这种情况下的class⽂件加载
WarLauncher
构建war包很简单
1. adle中引⼊插件apply plugin: 'war'
2. adle中将内嵌容器相关依赖设为provided providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
3. 修改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
springboot aop
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()) {
Name().equals(WEB_INF_CLASSES);
}
else {
Name().startsWith(WEB_INF_LIB)
|| Name().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容器下

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。