用spring-boot很长时间了,解放了对一堆tomcat的配置,一个可执行jar包,比以前弄一堆xml方便了很多,大大简化微服务开发,也为java容器化提供了更方便的方式。
一直没好好看怎么启动的。本文一步步来看,基于spring-boot 2.1.1.RELEASE
简单示例
按照官doc新建一个简单的web应用:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>simple-demo</artifactId>
<version>1.0.0-SNAPSHOT.jar </version>
<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
</parent>
<!-- Add typical dependencies for a web application -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- Package as an executable jar -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
mvn 打包:
mvn clean package
在target目录生产两个包:
# 可执行fat jar
simple-demo-1.0.0-SNAPSHOT.jar
# 本项目的jar包
simple-demo-1.0.0-SNAPSHOT.jar.original
启动:
java -jar simple-demo-1.0.0-SNAPSHOT.jar
就这么简单。下面解压simple-demo-1.0.0-SNAPSHOT.jar ,看它的目录结构:
jar tvf simple-demo-1.0.0-SNAPSHOT.jar
# 解压后的3个目录
# tree -L 2
├── BOOT-INF # 项目class和依赖的jar包
│ ├── classes
│ └── lib
├── META-INF # jarMANIFEST和maven信息
│ ├── MANIFEST.MF
│ └── maven
└── org # spring-boot启动相关的class
└── springframework
一个jar能通过java -jar
执行需要jar指定了main方法,文档见
总结一句话,需要在META-INF/MANIFEST.MF
中指定Main-Class
。下面我们看看解压后的文件是否有相关配置:
➜ META-INF (master) ✗ cat MANIFEST.MF
Manifest-Version: 1.0
Implementation-Title: simple-demo
Implementation-Version: 1.0-SNAPSHOT
Built-By: zhaozhou
Implementation-Vendor-Id: com.thoreauz.bootlearn
Spring-Boot-Version: 2.0.1.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.thoreauz.bootlearn.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_91
Implementation-URL: http://maven.apache.org
没错,Main-Class就是org.springframework.boot.loader.JarLauncher
。所以最先启动的是它。
Archives
先撇开JarLauncher两分钟,认识下Archive,看代码:
// 可以被Launcher 启动的一个抽象
public interface Archive extends Iterable<Archive.Entry> {
// 用于定位Archive
URL getUrl() throws MalformedURLException;
// 返回Archive的manifest
Manifest getManifest() throws IOException;
// 返回Archive所包装的Archive
List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}
Archive,即归档,spring-boot把可以被启动器(Launcher)加载的抽象成一个Archive,目前有两个实现:
- JarFileArchive:即一个jarFile(jar包或者jar目录)
- ExplodedArchive:可以理解一个目录
再来看JarLauncher类,他是用来启动JarFileArchive类型的启动器,指定了从BOOT-INF/classes和BOOT-INF/lib/路径下加载。
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
JarLauncher初始化
main方法中new JarLauncher()
初始化JarLauncher,主要做的一件事就是把它属性archive实例化。这个archive正是之前提到的fatjar。获取方式如下:
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = this.getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = codeSource == null ? null : codeSource.getLocation().toURI();
String path = location == null ? null : location.getSchemeSpecificPart();
// 省略了一些判断的代码
File root = new File(path);
return (Archive)(root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
JarLauncher的archive就是JarFileArchive类型。
查找fat-jar中的类
上面提到JarLauncher的main方法调用了父类Launcher
的launch(String[] args)
方法,方法如下:
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = this.createClassLoader(this.getClassPathArchives());
this.launch(args, this.getMainClass(), classLoader);
它怎么判断从fat jar的哪个目录开始查找?通过上文中isNestedArchive()方法判断查找入口,查找类目录和jar,创建Archives。其实就是JarLauncher的archive(JarFileArchive)实现的getNestedArchives()
方法的工作。返回的一个JarFileArchive的list,实际数据保存在JarFileArchive.JarFile,他的URL协议如下:
# NESTED_DIRECTORY类型
jar:file:/tmp/simple-demo/target/simple-demo-1.0-SNAPSHOT.jar!/BOOT-INF/classes!/
# NESTED_JAR类型
jar:file:/tmp/simple-demo/target/simple-demo-1.0-SNAPSHOT.jar!/BOOT-INF/lib/spring-boot-starter-web-2.0.1.RELEASE.jar!/
LaunchedURLClassLoader
查找并创建的archives,最后转化为urls(List)。spring boot 通过LaunchedURLClassLoader,根据urls加载类,它继承jdk提供的URLClassLoader。URLClassLoader可以通过以下方式加载类:
- 从文件目录加载
- 从jar包中加载
- 从网络加载 LaunchedURLClassLoader作了扩展,能根据JarFileArchive的urls,从fatjar中加载类。最后通过他反射调用项目的main方法:
//classLoader就是LaunchedURLClassLoader
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
Thread.currentThread().setContextClassLoader(classLoader);
this.createMainMethodRunner(mainClass, args, classLoader).run();
}
public void run() throws Exception {
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke((Object)null, this.args);
}
main方法类来自MANIFEST.MF的Start-Class。
总结
至此,spring boot为什么能通过fat jar执行的基本原理就介绍完了,总结起来有几个关键点:
- maven插件按照一定规则组织jar包
- 把可以被spring boot加载的jar或者目录抽象为Archive。
- LaunchedURLClassLoader提供了从Archive加载class的能力。
当然,这是通过jar包运行的情况,如果在idea运行main方法,入口毫无疑问就是应用的main。 项目class在target/classes/
目录,依赖jar包都被idea一股脑扔到classpath下了。