SpringBoot可执行jar包启动原理

1、可执行jar目录结构

在使用spring-boot-maven-plugin插件执行mvn package命令构建可执行jar文件(Fat JAR)后用“java -jar”命令就可以直接运行应用程序。

<plugin>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

执行打包命令后实际在target目录下产生了两个jar文件,一个为application-name.version-SNAPSHOT.jar,一个为application-name.version-SNAPSHOT.jar.original。后者仅包含应用编译后的本地资源,而前者引入了相关的第三方依赖,这点从文件大小也能看出。
将前者jar包解压查看目录结构如下:
SpringBoot可执行jar包启动原理_第1张图片
该目录比使用传统jar命令打包结构更复杂一些,目录含义如下:

  • BOOT-INF/classes:目录存放应用编译后的class文件。
  • BOOT-INF/lib:目录存放应用依赖的第三方JAR包文件。
  • META-INF:目录存放应用打包信息(Maven坐标、pom文件)和MANIFEST.MF文件。
  • org:目录存放SpringBoot相关class文件。

2、可执行jar包启动器

当使用java -jar命令执行Spring Boot应用的可执行jar文件时,该命令引导标准可执行的jar文件,读取在jar中META-INF/MANIFEST.MF文件的Main-Class属性值,该值代表应用程序执行入口类也就是包含main方法的类。
打开spring-boot可执行jar包解压后的META-INF/MANIFEST.MF文件发现其Main-Class属性值为org.springframework.boot.loader.JarLauncher,并且项目的引导类定义在Start-Class属性中,该属性并非Java标准META-INF/MANIFEST.MF文件属性,而是spring-boot引导程序启动需要的,JarLauncher是对应jar文件的地动器,org.springframework.boot.loader.WarLauncher是可执行war包的启动器。
启动类org.springframework.boot.loader.JarLauncher并非为项目中引入类,而是spring-boot-maven-plugin插件repackage追加进去的。
当执行java -jar命令或执行解压后的org.springframework.boot.loader.JarLauncher类时,JarLauncher会将BOOT-INF/classes下的类文件和BOOT-INF/lib下依赖的jar加入到classpath下,后调用META-INF/MANIFEST.MF文件Start-Class属性完成应用程序的启动。

3、启动器实现原理

3.1、JarLauncher的实现原理

打开spring-boot-loader模块源码找到org.springframework.boot.loader.JarLauncher类。

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);
	}

	@Override
	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);
	}
}

此启动器假定依赖项jar包含在/BOOT-INF/lib目录中,并且应用程序类包含在/BOOT-INF/classes目录中。
isNestedArchive方法在父类ExecutableArchiveLauncher确定指定的JarEntry是否为应添加到类路径的嵌套项目。 该方法为jar包中每个条目调用一次。具体在ExecutableArchiveLauncher中再看。
在JarLauncher中定义了main方法,该方法直接调用JarLauncher实例的launch方法。launch方法是基类Launcher中定义的,而Launcher是ExecutableArchiveLauncher的父类,Launcher的子孙如下:
SpringBoot可执行jar包启动原理_第2张图片
前面也提到了WarLauncher是可执行war包的启动器,与JarLauncher区别以及PropertiesLauncher后面再讨论。

3.2、Launcher实现原理

public abstract class Launcher {
	...
	/**
	 * 启动应用程序。 此方法是子类公共静态void main(String [] args)方法应调用的初始入口点。
	 */
	protected void launch(String[] args) throws Exception {
		JarFile.registerUrlProtocolHandler();
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
		launch(args, getMainClass(), classLoader);
	}
	...
}

JarFile.registerUrlProtocolHandler()方法利用了Java URL协议实现扩展原理该方法将包名org.springframework.boot.loader追加到Java系统属性java.protocol.handler.pkgs中,该包下存在协议对应的Handler类,即org.springframework.boot.loader.jar.Handler其实现协议为jar。

/**
 *注册一个“java.protocol.handler.pkgs”属性,以便定位URLStreamHandler来处理jar URL。
 **/
public static void registerUrlProtocolHandler() {
	String handlers = System.getProperty(PROTOCOL_HANDLER, "");
	System.setProperty(PROTOCOL_HANDLER,
				("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
	resetCachedUrlHandlers();
}
/**
 * 重置URLStreamHandlerFactory以防万一已经使用了实现jar协议的URLStreamHandlerFactory
 **/
private static void resetCachedUrlHandlers() {
	try {
		URL.setURLStreamHandlerFactory(null);
	}
	catch (Error ex) {
		// Ignore
	}
}

URL#getURLStreamHandler(String)方法的实现先读取Java系统属性java.protocol.handler.pkgs,无论是否存在再追加sun.net.www.protocol包,所以JDK内建实现作为兜底实现。
问题的关键在于为什么Spring Boot要自定义URL的jar协议实现覆盖JDK内建的实现呢?
前面提到过,Spring Boot Fat JAR除包含传统Java Jar中的资源外还包含依赖的第三方Jar文件,当Spring Boot Fat Jar被java -jar命令引导时,其内部的Jar文件无法被内建实现sun.net.www.protocol.jar.Handler当做classpath。
讨论完JarFile.registerUrlProtocolHandler()方法扩展URL协议的目的,下一步执行createClassLoader(getClassPathArchives())创建ClassLoader,其中getClassPathArchives()方法返回值作为参数,该方法为抽象方法具体实现在子类ExecutableArchiveLauncher:

@Override
protected List<Archive> getClassPathArchives() throws Exception {
	List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
	// 此方法为空实现
	postProcessClassPathArchives(archives);
	return archives;
}

archive是Archive的实例,在构造方法中完成创建:

public ExecutableArchiveLauncher() {
	try {
		this.archive = createArchive();
	}
	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;
	//如果直接执行.class文件那么会得到当前class的绝对路径。
	//如果封装在jar包里面执行jar包那么会得到当前jar包的绝对路径。
	String path = (location != null) ? location.getSchemeSpecificPart() : null;
	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));
}

此方法主要通过当前启动器所在的介质返回一个相应的Archive归档实现:

  • ExplodedArchive:由展开的归档目录支持的归档实现。
  • JarFileArchive:Fat Jar归档文件的实现。

Archive接口定义了一个getNestedArchives方法返回与指定过滤器匹配的条目的嵌套存档,在JarLauncher实现中传入的过滤器就是JarLauncher#isNestedArchive方法引用,这里可以回顾一下3.1节的实现。
下面是JarFileArchive的构造方法,成员变量jarFile是在JarFileArchive构造方法中通过当前归档文件路径作为JarFile的构造参数创建的。

public JarFileArchive(File file) throws IOException {
	this(file, file.toURI().toURL());
}

public JarFileArchive(File file, URL url) throws IOException {
	this(new JarFile(file));
	this.url = url;
}

public JarFileArchive(JarFile jarFile) {
	this.jarFile = jarFile;
}

下面就以JarFileArchive为例看下getNestedArchives方法具体实现:

@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
	List<Archive> nestedArchives = new ArrayList<>();
	for (Entry entry : this) {
		if (filter.matches(entry)) {
			nestedArchives.add(getNestedArchive(entry));
		}
	}
	return Collections.unmodifiableList(nestedArchives);
}

由于Archive继承于Iterable,JarFileArchive实现了Archive接口所以也必须实现Iterable,所以可以在getNestedArchives()方法中循环归档文件中的条目,筛选出符合的条目,针对JarLauncher来说就是BOOT-INF/classes/目录和BOOT-INF/lib/目录下的jar文件,在通过getNestedArchive()方法将相应jar包(BOOT-INF/lib/)或目录(BOOT-INF/classes/)包装成JarFileArchive,下面是JarFileArchive#iterator方法的实现:

@Override
public Iterator<Entry> iterator() {
	return new EntryIterator(this.jarFile.entries());
}

下面是JarFileArchive#getNestedArchive方法的实现:

protected Archive getNestedArchive(Entry entry) throws IOException {
	JarEntry jarEntry = ((JarFileEntry) entry).getJarEntry();
	if (jarEntry.getComment().startsWith(UNPACK_MARKER)) {
		return getUnpackedNestedArchive(jarEntry);
	}
	try {
		JarFile jarFile = this.jarFile.getNestedJarFile(jarEntry);
		return new JarFileArchive(jarFile);
	}
	catch (Exception ex) {
		throw new IllegalStateException("Failed to get nested archive for entry " + entry.getName(), ex);
	}
}

jarFile是org.springframework.boot.loader.jar.JarFile的实例对象,继承于java.util.jar.JarFile,其行为方式相同,但提供以下附加功能:

  • 可以基于任何目录条目获取嵌套的JarFile。
  • 可以为嵌入式JAR文件获取嵌套的JarFile(只要未压缩其条目)。
    这个附加功能是getNestedJarFile方法实现的:
public synchronized JarFile getNestedJarFile(ZipEntry entry) throws IOException {
	return getNestedJarFile((JarEntry) entry);
}

public synchronized JarFile getNestedJarFile(JarEntry entry) throws IOException {
	try {
		return createJarFileFromEntry(entry);
	}
	catch (Exception ex) {
		throw new IOException("Unable to open nested jar file '" + entry.getName() + "'", ex);
	}
}

createJarFileFromEntry方法根据当前条目是目录还是文件来创建不同JarFileType的JarFile:
SpringBoot可执行jar包启动原理_第3张图片

entries()方法返回一个可迭代其所有的条目的Enumeration

@Override
public Enumeration<java.util.jar.JarEntry> entries() {
	final Iterator<JarEntry> iterator = this.entries.iterator();
	return new Enumeration<java.util.jar.JarEntry>() {

		@Override
		public boolean hasMoreElements() {
			return iterator.hasNext();
		}

		@Override
		public java.util.jar.JarEntry nextElement() {
			return iterator.next();
		}

	};
}

entries()中核心成员就是this.entries,它是在构造方法中赋值的,还有上面多出方法调用都会通过构造方法创建JarFile,下面就看一下JarFile的构造方法:

//创建一个由指定文件支持的JarFile
public JarFile(File file) throws IOException {
	this(new RandomAccessDataFile(file));
}
JarFile(RandomAccessDataFile file) throws IOException {
	this(file, "", file, JarFileType.DIRECT);
}

在JarFileArchive构造方法中就是通过上面这个构造方法创建的JarFile,由于是直接通过Jar文件的路径直接指定的,因此不需要其余构造方法中除file参数的额外参数,并且jarFileType为DIRECT(直接类型)。
上面的构造方法最终还是会调用带有6个参数的构造方法:

private JarFile(RandomAccessDataFile rootFile, String pathFromRoot, RandomAccessData data, JarEntryFilter filter,
		JarFileType type, Supplier<Manifest> manifestSupplier) throws IOException {
	super(rootFile.getFile());
	this.rootFile = rootFile;
	this.pathFromRoot = pathFromRoot;
	CentralDirectoryParser parser = new CentralDirectoryParser();
	this.entries = parser.addVisitor(new JarFileEntries(this, filter));
	this.type = type;
	parser.addVisitor(centralDirectoryVisitor());
	try {
		this.data = parser.parse(data, filter == null);
	}
	catch (RuntimeException ex) {
		close();
		throw ex;
	}
	this.manifestSupplier = (manifestSupplier != null) ? manifestSupplier : () -> {
		try (InputStream inputStream = getInputStream(MANIFEST_NAME)) {
			if (inputStream == null) {
				return null;
			}
			return new Manifest(inputStream);
		}
		catch (IOException ex) {
			throw new RuntimeException(ex);
		}
	};
}

这个方法被直接调动是在createJarFileFromEntry方法中,这样可以直接指定pathFromRoot和type等参数,在这个方法中可以跟通给定的JarFile得到代表jar文件的所有条目的JarFileEntries对象和代表MANIFEST.MF文件的Manifest引用对象。
以上就是ExecutableArchiveLauncher#getClassPathArchives()方法的全部实现,回过头来再看Launcher#createClassLoader(List)方法:

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
	List<URL> urls = new ArrayList<>(archives.size());
	for (Archive archive : archives) {
		urls.add(archive.getUrl());
	}
	return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
	return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

createClassLoader()方法目的是创建一个类加载器,而LaunchedURLClassLoader继承于URLClassLoader,URLClassLoader需要一个URL数组,URLClassLoader本身加载的类都是通过URL定位的资源转换成JVM的内存对象,关于类加载器原理参考《一篇搞懂Java ClassLoader》。而这组URL的来源就是通过参数List,下面看一下JarFileArchive#getUrl()方法的实现:

@Override
public URL getUrl() throws MalformedURLException {
	//通过URL直接new的JarFileArchive对象url就不为空,也就是最外侧的jar
	if (this.url != null) {
		return this.url;
	}
	//通过JarFileArchive#getNestedArchive返回的,url为空,也就是内部的jar
	return this.jarFile.getUrl();
}

如果url!=null时直接返回代表最外层的Jar文件URL给URLClassLoader,URLClassLoader在加载时通过读取Java系统属性java.protocol.handler.pkgs使用org.springframework.boot.loader.jar.Handler来得到该URL代表的资源,关于URLClassLoader使用URL加载类的过程请自行参考java.net.URLClassLoader#findClass()方法。
如果url==null时,JarFile#getUrl()方法内部使用了org.springframework.boot.loader.jar.Handler来处理重新构建的URL,看一下具体实现:

public URL getUrl() throws MalformedURLException {
	if (this.url == null) {
		Handler handler = new Handler(this);
		String file = this.rootFile.getFile().toURI() + this.pathFromRoot + "!/";
		file = file.replace("file:", "file://"); // Fix UNC paths
		this.url = new URL("jar", "", -1, file, handler);
	}
	return this.url;
}

org.springframework.boot.loader.jar.Handler#openConnection(java.net.URL)方法的实现:

@Override
protected URLConnection openConnection(URL url) throws IOException {
	if (this.jarFile != null && isUrlInJarFile(url, this.jarFile)) {
		return JarURLConnection.get(url, this.jarFile);
	}
	try {
		return JarURLConnection.get(url, getRootJarFileFromUrl(url));
	}
	catch (Exception ex) {
		return openFallbackConnection(url, ex);
	}
}

可以看到org.springframework.boot.loader.jar.Handler返回的URLConnection是一个org.springframework.boot.loader.jar.JarURLConnection,而这个JarURLConnection和JDK内建jar协议实现sun.net.www.protocol.jar.Handler返回的sun.net.www.protocol.jar.JarURLConnection区别是前者支持嵌套jar文件的类加载,下面通过一个例子说明这个情况:

public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {
	System.setProperty("java.protocol.handler.pkgs","org.springframework.boot.loader");
	URLClassLoader normal=new URLClassLoader(new URL[]{new URL("jar:file:///Users/test-1.0.0-SNAPSHOT.jar!/BOOT-INF/lib/guava-28.1-jre.jar!/")},null);
	Class<?> aClass = normal.loadClass("com.google.common.base.JdkPattern");
	System.out.println(aClass.getSimpleName());
}

test-1.0.0-SNAPSHOT.jar是使用spring-boot-maven-plugin插件打包的Fat Jar,guava-28.1-jre.jar是程序依赖的第三方jar包,在上面代码开启spring boot方式的jar加载,代码顺利打印"JdkPattern",而将第一行代码注释掉就会报如下异常:
在这里插入图片描述
根本原因就是Spring Boot提供的org.springframework.boot.loader.jar.JarFile可以提供对嵌套jar数据读取,而内建的sun.net.www.protocol.jar.URLJarFile不支持。
以上就是Spring Boot扩展URL协议实现的原因了,在得到可以加载嵌套式的jar文件后调用Launcher#launch(String[], String, ClassLoader)方法,读取Manifest的Start-Class属性利用反射执行main方法完成应用程序的启动。

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
	Thread.currentThread().setContextClassLoader(classLoader);
	createMainMethodRunner(mainClass, args, classLoader).run();
}
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
	//反射执行mainClass.main
	return new MainMethodRunner(mainClass, args);
}

ExecutableArchiveLauncher#getMainClass:

@Override
protected String getMainClass() throws Exception {
	Manifest manifest = this.archive.getManifest();
	String mainClass = null;
	if (manifest != null) {
		mainClass = manifest.getMainAttributes().getValue("Start-Class");
	}
	if (mainClass == null) {
		throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
	}
	return mainClass;
}

3.3、WarLauncher

WarLauncher是可执行war包的启动器,与JarLauncher一样继承于ExecutableArchiveLauncher,唯一区别就是选取classpath文件。

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() {
	}

	protected WarLauncher(Archive archive) {
		super(archive);
	}

	@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);
	}

}

WEB-INF/classes/、WEB-INF/lib/、WEB-INF/lib-provided/均为LaunchedURLClassLoader的classpath,其中WEB-INF/classes/、WEB-INF/lib/是传统的Servlet应用的classpath路径,而WEB-INF/lib-provided/属于Spring Boot WarLauncher定制实现,这个目录专门存放Maven依赖provided的jar,这么做的目的是为什么呢?
传统的Servlet应用的classpath路径仅关注WEB-INF/classes/和WEB-INF/lib/,因此WEB-INF/lib-provided/中的jar将被Servlet容器忽略,如Servlet API,该API由Servlet容器提供。这样的设计的好处在于打包后war文件能够在Servlet容器中兼容运行。
总而言之,打包war文件是一种兼容措施,既能被WarLauncher启动又能兼容Servlet容器环境。换言之,WarLauncher与JarLauncher并无本质差别,所以建议Spring Boot应用使用非传统Web部署时尽可能地使用Jar归档方式。

3.4、PropertiesLauncher

通过属性文件使用用户配置的类路径和主类进行归档的启动器。与基于可执行jar的模型相比,该模型通常更灵活并且更适合创建行为良好的OS级别服务。
在不同位置查找属性文件以提取加载程序设置,默认情况下在当前类路径或当前工作目录中为loader.properties。可以通过设置系统属性loader.config.name来更改属性文件的名称(例如-Dloader.config.name=foo将查找foo.properties。如果该文件不存在,请尝试读取系统属性loader.config.location (带有允许的前缀classpath:和file:或任何有效的URL)。找到该文件后,将其转换为Properties并提取可选值(如果文件不存在,也可以将其重写为System属性):

  • loader.path:逗号分隔的目录列表(包含资源文件和嵌套归档或归档的*.jar或*.zip)或要追加到类路径的归档。始终使用应用档案中的BOOT-INF/classes,BOOT-INF/lib
  • loader.main:设置类加载器后将执行委派给的主要方法。没有缺省值,但是如果${loader.home}/META-INF中包含一个,则将退回到在MANIFEST.MF中寻找Start-Class。
    以上是PropertiesLauncher的java doc描述,可以看出与JarLauncher和WarLauncher的区别是可以自定义classpath和Start-Class。
    先看一下PropertiesLauncher定义的Java系统属性常量:
	/**
	 * Properties key for main class. As a manifest entry can also be specified as
	 * {@code Start-Class}.
	 */
	public static final String MAIN = "loader.main";

	/**
	 * Properties key for classpath entries (directories possibly containing jars or
	 * jars). Multiple entries can be specified using a comma-separated list. {@code
	 * BOOT-INF/classes,BOOT-INF/lib} in the application archive are always used.
	 */
	public static final String PATH = "loader.path";

	/**
	 * Properties key for home directory. This is the location of external configuration
	 * if not on classpath, and also the base path for any relative paths in the
	 * {@link #PATH loader path}. Defaults to current working directory (
	 * ${user.dir}).
	 */
	public static final String HOME = "loader.home";

	/**
	 * Properties key for default command line arguments. These arguments (if present) are
	 * prepended to the main method arguments before launching.
	 */
	public static final String ARGS = "loader.args";

	/**
	 * Properties key for name of external configuration file (excluding suffix). Defaults
	 * to "application". Ignored if {@link #CONFIG_LOCATION loader config location} is
	 * provided instead.
	 */
	public static final String CONFIG_NAME = "loader.config.name";

	/**
	 * Properties key for config file location (including optional classpath:, file: or
	 * URL prefix).
	 */
	public static final String CONFIG_LOCATION = "loader.config.location";

	/**
	 * Properties key for boolean flag (default false) which if set will cause the
	 * external configuration properties to be copied to System properties (assuming that
	 * is allowed by Java security).
	 */
	public static final String SET_SYSTEM_PROPERTIES = "loader.system";

构造方法加载properties文件转换成Properties对象。

public PropertiesLauncher() {
	try {
		this.home = getHomeDirectory();
		initializeProperties();
		initializePaths();
		this.parent = createArchive();
	}
	catch (Exception ex) {
		throw new IllegalStateException(ex);
	}
}

home目录为loader.home系统属性如果没设置则读取环境变量${user.dir}。

private void initializeProperties() throws Exception, IOException {
	List<String> configs = new ArrayList<>();
	if (getProperty(CONFIG_LOCATION) != null) {
		configs.add(getProperty(CONFIG_LOCATION));
	}
	else {
		String[] names = getPropertyWithDefault(CONFIG_NAME, "loader").split(",");
		for (String name : names) {
			configs.add("file:" + getHomeDirectory() + "/" + name + ".properties");
			configs.add("classpath:" + name + ".properties");
			configs.add("classpath:BOOT-INF/classes/" + name + ".properties");
		}
	}
	for (String config : configs) {
		try (InputStream resource = getResource(config)) {
			if (resource != null) {
				debug("Found: " + config);
				loadResource(resource);
				// Load the first one we find
				return;
			}
			else {
				debug("Not found: " + config);
			}
		}
	}
}

initializeProperties()方法的功能就是读取系统属性loader.config.location作为一个properties文件,如果该系统属性不存在则读取home目录下系统属性loader.config.name代表的文件名,如果loader.config.name属性未指定默认为loader,读取的属性文件加载到成员变量properties中供后面使用,在loadResource方法中提供了一个方便,如果设置系统属性loader.system=true,则还会将properties中的键值对设置到Java系统属性中。
initializePaths()方法读取系统属性和properties的loader.path的值逗号分隔作为归档文件或文件所在的目录。
createArchive()方法还是调用父类Launcher的,返回一个代表PropertiesLauncher所在的Jar文件或目录形式的回档文件对象JarFileArchive或ExplodedArchive。
下面还是按着Launcher的launch()方法的调用顺序,接下来是获取构成classpath的归档文件对象:

@Override
protected List<Archive> getClassPathArchives() throws Exception {
	List<Archive> lib = new ArrayList<>();
	for (String path : this.paths) {
		for (Archive archive : getClassPathArchives(path)) {
			if (archive instanceof ExplodedArchive) {
				List<Archive> nested = new ArrayList<>(archive.getNestedArchives(new ArchiveEntryFilter()));
				nested.add(0, archive);
				lib.addAll(nested);
			}
			else {
				lib.add(archive);
			}
		}
	}
	addNestedEntries(lib);
	return lib;
}
private List<Archive> getClassPathArchives(String path) throws Exception {
	String root = cleanupPath(handleUrl(path));
	List<Archive> lib = new ArrayList<>();
	File file = new File(root);
	if (!"/".equals(root)) {
		if (!isAbsolutePath(root)) {
			file = new File(this.home, root);
		}
		if (file.isDirectory()) {
			debug("Adding classpath entries from " + file);
			Archive archive = new ExplodedArchive(file, false);
			lib.add(archive);
		}
	}
	Archive archive = getArchive(file);
	if (archive != null) {
		debug("Adding classpath entries from archive " + archive.getUrl() + root);
		lib.add(archive);
	}
	List<Archive> nestedArchives = getNestedArchives(root);
	if (nestedArchives != null) {
		debug("Adding classpath entries from nested " + root);
		lib.addAll(nestedArchives);
	}
	return lib;
}

以上方法就是从loader.path指定的目录读取归档文件,如果目录不是绝对路径形式(“/”开头或“file:///”开头或"jar:file:"开头)则从home下读取这些路径(或文件)。
下面是使用上面得到的Archive创建类加载器,在这个方法内支持自定义类加载器通过读取属性loader.classLoader。

@Override
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
	Set<URL> urls = new LinkedHashSet<>(archives.size());
	for (Archive archive : archives) {
		urls.add(archive.getUrl());
	}
	ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(NO_URLS), getClass().getClassLoader());
	debug("Classpath: " + urls);
	String customLoaderClassName = getProperty("loader.classLoader");
	if (customLoaderClassName != null) {
		loader = wrapWithCustomClassLoader(loader, customLoaderClassName);
		debug("Using custom class loader: " + customLoaderClassName);
	}
	return loader;
}

读取属性loader.main作为主类。

@Override
protected String getMainClass() throws Exception {
	String mainClass = getProperty(MAIN, "Start-Class");
	if (mainClass == null) {
		throw new IllegalStateException("No '" + MAIN + "' or 'Start-Class' specified");
	}
	return mainClass;
}

你可能感兴趣的:(Spring,Boot源码分析)