Spring-Repackage:Spring-Boot项目打包原理

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启动的入口类

上述对应的Oracle文档链接

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执行流程主要有以下几个步骤

  1. 加载jar包中BOOT-INF/classesBOOT-INF/lib,作为java.net.URL集合,并且通过这些URL创建对应的URLClassLoader

    因为ApplicationClassLoader 作为默认的类加载器,只会加载classpath下的class,以及委托给父类加载器(双亲委派机制)

    无法加载到jar包中BOOT-INF/classesBOOT-INF/lib的class,所以需要创建一个URLClassLoader,来替换ApplicationClassLoader

  2. 替换ApplicationClassLoader

    如何替换的? 通过 Thread.currentThread().setContextClassLoader来替换

  3. 反射执行 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.MSStart-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源码分析

还剩下两个问题

  1. Spring在打包时是如何知道Start-Class就是我们自己应用的入口类呢

  2. Spring是如何将编译后的class文件和依赖的jar包打包到相应目录下的

这些问题只能从spring-boot-maven-plugin寻找答案

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值