1. 简介
Spring-Repackage是指SpringBoot应用打包时,会生成一个Spring-Boot应用的jar包,可以直接通过java -jar xxx.jar执行
其原因是因为在spring-boot-starter-parent.xml
中定义了Maven的Plugin:spring-boot-maven-plugin
接管了maven生命周期中的package
改变了打包的行为,不再生成默认作为类库形式的jar包,而是生成一个可执行jar包
2. 前景概要
2.1 如何生成可执行jar包
比较常见的做法是通过maven-jar-plugin
具体用法可以查看maven-生成可执行jar包
但是通过该方法生成的jar包并不是单一jar包( 依赖的jar包在可执行jar包的外部 )
这也就是为什么Spring-Boot会单独编写插件生成可执行jar包的原因
感兴趣的可以看下Spring-Boot生成jar包的文档说明
2.2 可执行jar包的执行原理
通过java -jar
命令执行jar包时,JVM会读取jar包META-INF/MANIFEST.MF文件
并且读取文件的Main-Class属性,作为JVM启动的入口类
2.3 可执行jar包执行原理的open-jdk源码跟踪 ( 可忽略 )
2.3.1 ParseArguments
ParseArguments方法位于jdk/src/java.base/share/native/libjli/java.c
文件中,用于解析java命令的参数
static jboolean ParseArguments(int *pargc, char ***pargv,
int *pmode, char **pwhat,
int *pret, const char *jrepath)
{
// ...省略其他代码
while (argc > 0 && *(arg = *argv) == '-') {
// 检查是否有 -jar参数, 如果有, 则将mode设为 LM_JAR
if (JLI_StrCmp(arg, "-jar") == 0) {
ARG_CHECK(argc, ARG_ERROR2, arg);
mode = checkMode(mode, LM_JAR, arg);
}
}
// 将mode赋值给pmode指针
*pmode = mode;
}
2.3.2 JavaMain
JavaMain方法位于jdk/src/java.base/share/native/libjli/java.c
,是启动JVM虚拟器的入口方法
JavaMain方法会调用LoadMainClass
方法,用于找到Java源代码Main方法的入口类
int JavaMain(void* _args)
{
// ...省略其他代码
JavaMainArgs *args = (JavaMainArgs *)_args;
// 1. 加载mainClass
mainClass = LoadMainClass(env, mode, what);
switch (mainType) {
case 0: {
// 2. 找到mainClass中main方法对应的引用
mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
"([Ljava/lang/String;)V");
CHECK_EXCEPTION_NULL_LEAVE(mainID);
// 3. 执行main方法
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
break;
}
}
LEAVE();
}
static jclass LoadMainClass(JNIEnv *env, int mode, char *name)
{
jmethodID mid;
jstring str;
jobject result;
jlong start = 0, end = 0;
jclass cls = GetLauncherHelperClass(env);
NULL_CHECK0(cls);
if (JLI_IsTraceLauncher()) {
start = CurrentTimeMicros();
}
// 1. 找到java编写的checkAndLoadMain方法
NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
"checkAndLoadMain",
"(ZILjava/lang/String;)Ljava/lang/Class;"));
NULL_CHECK0(str = NewPlatformString(env, name));
// 2. 调用checkAndLoadMain方法
NULL_CHECK0(result = (*env)->CallStaticObjectMethod(env, cls, mid,
USE_STDERR, mode, str));
return (jclass)result;
}
LoadMainClass方法委托给了java编写的checkAndLoadMain方法
并且将mode参数传递过去
如果是执行的是java -jar
命令,那么此时的mode为**LM_JAR **
2.3.2 checkAndLoadMain
checkAndLoadMain方法位于jdk/src/java.base/share/classes/sun/launcher/LauncherHelper.java
文件中,是由java编写的
public static Class<?> checkAndLoadMain(boolean printToStderr,
int mode,
String what) {
Class<?> mainClass = null;
switch (mode) {
case LM_MODULE: case LM_SOURCE:
mainClass = loadModuleMainClass(what);
break;
default:
mainClass = loadMainClass(mode, what);
break;
}
// ...
}
假设mode为LM_JAR,那么将转而调用loadMainClass
方法
private static Class<?> loadMainClass(int mode, String what) {
String cn;
switch (mode) {
case LM_CLASS:
cn = what;
break;
case LM_JAR:
// 调用getMainClassFromJar方法
cn = getMainClassFromJar(what);
break;
default:
// should never happen
throw new InternalError("" + mode + ": Unknown launch mode");
}
// ...
}
static String getMainClassFromJar(String jarname) {
String mainValue;
// 1. 读取jar包
try (JarFile jarFile = new JarFile(jarname)) {
// 2. 获取jar内的Manifest文件对应的对象
Manifest manifest = jarFile.getManifest();
if (manifest == null) {
abort(null, "java.launcher.jar.error2", jarname);
}
Attributes mainAttrs = manifest.getMainAttributes();
if (mainAttrs == null) {
abort(null, "java.launcher.jar.error3", jarname);
}
// 3. MAIN_CLASS是LauncherHelper中定义的字符串常量, 对应的字符串是: Main-Class
mainValue = mainAttrs.getValue(MAIN_CLASS);
if (mainValue == null) {
abort(null, "java.launcher.jar.error3", jarname);
}
// ... 省略其他代码
// 4. 返回 Main-Class对应的value
return mainValue.trim();
} catch (IOException ioe) {
abort(ioe, "java.launcher.jar.error1", jarname);
}
return null;
}
public final class LauncherHelper {
// ...
private static final String MAIN_CLASS = "Main-Class";
// ...
}
至此
为什么jar包中的META-INF/MANIFEST.MF文件的Main-Class
属性对应的类会作为java应用的入口类
就得以解释了
3. Spring-Boot可执行jar包的运行原理
3.1 Spring-Boot可执行jar包结构
上图画出了Spring-boot可执行jar报的关键结构
-
BOOT-INF/classes/
我们自己编写的java代码编译后生成的class文件 -
BOOT-INF/lib/
项目依赖的jar包 -
META-INF/MANIFEST.MF
jvm虚拟机规范中可执行jar包的定义文件,在前景概要中有提到过需要注意的是
Main-Class属于规范中定义的属性,执行java -jar
命令后,由JVM执行执行Main-Class属性值对应class的main方法
Start-Class是Spring-Boot的自定义属性,并不会被JVM所读取
根据Main-Class定义可知,JarLauncher
是整个程序的入口
3.2 JarLauncher启动流程
JarLauncher
执行流程主要有以下几个步骤
-
加载jar包中BOOT-INF/classes和BOOT-INF/lib,作为
java.net.URL
集合,并且通过这些URL
创建对应的URLClassLoader
因为ApplicationClassLoader 作为默认的类加载器,只会加载classpath下的class,以及委托给父类加载器(双亲委派机制)
无法加载到jar包中BOOT-INF/classes和BOOT-INF/lib的class,所以需要创建一个URLClassLoader,来替换ApplicationClassLoader
-
替换ApplicationClassLoader
如何替换的? 通过
Thread.currentThread().setContextClassLoader
来替换 -
反射执行 META-INF/MANIFEST.MF文件中定义的Start-Class属性值对应的class的main方法
执行流程如下
LaunchedURLClassLoader
继承自URLClassLoader
, Spring对其做了一些优化
4. JarLauncher源码分析
先熟悉下JarLauncher的继承关系
4.1 main
JarLauncher类中的main方法作为入口类,被JVM调用
执行
main
方法对应执行流程图的第一步
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
4.2 launch
launch
方法位于JarLauncher的父类Launcher中
protected void launch(String[] args) throws Exception {
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
第一步就是在创建ClassLoader
,并且创建ClassLoader也是最最关键的一环
创建
ClassLoader
对应执行流程图的第二步
先不继续向下分析createClassLoader
方法,而是跟踪getClassPathArchivesIterator
4.3 getClassPathArchivesIterator
4.3.1 this.archive
getClassPathArchivesIterator
方法位于JarLauncher的父类ExecutableArchiveLauncher中
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
Archive.EntryFilter searchFilter = this::isSearchCandidate;
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
return archives;
}
this.archive
:可以理解就是当前jar包对应对象,对应的类为JarFileArchive;在ExecutableArchiveLauncher的构造方法中被创建
// ExecutableArchiveLauncher的构造方法
public ExecutableArchiveLauncher() {
try {
this.archive = createArchive();
this.classPathIndex = getClassPathIndex(this.archive);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
protected final Archive createArchive() throws Exception {
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
String path = (location != null) ? location.getSchemeSpecificPart() : null;
File root = new File(path);
return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
}
ProtectionDomain 和CodeSource可能并不常见,属于java.security
包下
也无需去深入了解他们的细节
总之通过这两个类,可以拿到当前jar包的磁盘路径
通过磁盘路径创建File对象,当前File对象是一个jar包,执行root.isDirectory()
的结果是false
所以 this.archive
是一个JarFileArchive
JarFileArchive
JarFileArchive是由Spring编写的类,是对
java.util.jar.JarFile
的增强使其方便地获取到jar包内嵌套文件下,甚至是jar包内的jar包的文件
可以无限递归jar包中的jar包,直到找到指定的文件为止
4.3.2 getNestedArchives
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
Archive.EntryFilter searchFilter = this::isSearchCandidate;
// 经过上面的跟踪看, 现在已经知道 { this.archive } 是如何被创建的了
Iterator<Archive> archives = this.archive.getNestedArchives(searchFilter,
(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));
return archives;
}
调用getNestedArchives方法,递归jar包的文件夹和jar包,参数是两个过滤器
结果将只会返回匹配过滤器对应的Archive
对象
无需去关注该方法的实现细节
JarLauncher重写了isNestedArchive
方法
static final EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
};
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);
}
划重点
只在jar包中找位于 BOOT-INF/classes/ 和 BOOT-INF/lib/ 内的文件
那么this.archive.getNestedArchives
获取到的是 BOOT-INF/classes/ 和 BOOT-INF/lib/ 内的文件对应的Archive
集合
Archive
是一个接口,定义了获取URL
的方法public interface Archive extends Iterable<Archive.Entry>, AutoCloseable { // ...省略其他代码 URL getUrl() throws MalformedURLException; }
4.4 createClassLoader
// Iterator<Archive> archives就是上面调用 { getNestedArchives } 方法的返回值
@Override
protected ClassLoader createClassLoader(Iterator<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(guessClassPathSize());
// 1. 将所有的Iterator<Archive>转换为URL集合
while (archives.hasNext()) {
urls.add(archives.next().getUrl());
}
// 2. 通过这些URL, 创建URLClassLoader
return createClassLoader(urls.toArray(new URL[0]));
}
获取URL集合对应执行流程图的第三步
4.5 launch
此时URLClassLoader已经创建完成了
往下继续分析launch方法
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
JarFile.registerUrlProtocolHandler();
}
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
// 1. 替换当前线程的ClassLoader
Thread.currentThread().setContextClassLoader(classLoader);
// 2.
// - 创建MainMethodRunner
// - 调用MainMethodRunner的run方法
createMainMethodRunner(launchClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
return new MainMethodRunner(mainClass, args);
}
createMainMethodRunner方法第一个参数 mainClass
来自launch方法的launchClass
参数
mainClass
非常重要,所以再回头查看launch
方法
private static final String START_CLASS_ATTRIBUTE = "Start-Class";
protected void launch(String[] args) throws Exception {
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
// ===========================================================
// launchClass来自调用 { getMainClass() }
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
launch(args, launchClass, classLoader);
}
protected String getMainClass() throws Exception {
Manifest manifest = this.archive.getManifest();
String mainClass = null;
if (manifest != null) {
// 获取MANIFEST.MS的Start-Class
mainClass = manifest.getMainAttributes().getValue(START_CLASS_ATTRIBUTE);
}
if (mainClass == null) {
throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
}
return mainClass;
}
mainClass
属性的来源也知道了
来自MANIFEST.MS 的 Start-Class 属性
4.6 MainMethodRunner#run
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
public void run() throws Exception {
// 1. 反射获取mainClass, 也就是在MANIFEST.MS中指定的Start-Class
Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
// 2. 反射获取mainClass中的 public static void main (String[] args) 方法
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.setAccessible(true);
// 3. 反射执行main方法
mainMethod.invoke(null, new Object[] { this.args });
}
}
mainMethod.invoke(null, new Object[] { this.args })
对应执行流程图的第五步
至此,SpringBoot应用打包后生成的jar包的启动流程就分析完成了
5. spring-boot-maven-plugin源码分析
还剩下两个问题
-
Spring在打包时是如何知道Start-Class就是我们自己应用的入口类呢
-
Spring是如何将编译后的class文件和依赖的jar包打包到相应目录下的
这些问题只能从spring-boot-maven-plugin
寻找答案