天天看點

Springboot打成jar包後的啟動流程分析

作者:技術免費線上學習
springboot項目打成jar包之後,為何直接通過在指令行中輸入“java -jar 項目名稱.jar"指令就可以啟動整個項目呢?
  1. 我們先來看看springboot項目打成jar包之後的整體目錄結構
Springboot打成jar包後的啟動流程分析

jar包目錄結構說明

-- BOOT-INF
		-- classes (存放的是項目編譯後的所有的檔案包括資源配置檔案以及class檔案)
		-- lib (目錄是該項目所有依賴的第三方的jar包)
	-- META-INF
		-- MANIFEST.MF (存放的項目清單,是動态被建立出來的,包含了啟動類、項目版本号等)
	-- org
		-- springframework
			-- boot
				-- loader (将spring-boot-loader.jar包中的内容複制到了jar包中的頂層目錄中)           

在指令行中執行“java -jar springboot.jar”時,到底執行了什麼?

在指令行輸入該指令時,應用類加載器會從MANIFEST.MF檔案中加載Main-Class屬性對應的值
(org.springframework.boot.loader.JarLauncher),然後執行其中的main方法,
在執行該main方法時,springboot會建立自己的類加載器,
然後從MANIFEST.MF檔案中擷取Start-Class屬性對應的值,
然後根據目前線程上下文中的類加載器從BOOT-INF/classes中
加載com.example.springboot.SpringbootApplication類,
以及從BOOT-INF/lib目錄下加載依賴的第三方類           

關于manifest詳細說明參考如下連結

https://www.cnblogs.com/applerosa/p/9736729.html

在指令行中執行“java -jar springboot.jar”時,會執行JarLauncher中的main方法

/**
* Launcher基于歸檔的jar檔案,這個啟動器假定它依賴的jar包是在“BOOT-INF/lib”目錄下,
* 應用程式類檔案是在“BOOF-INF/classes”目錄下,該類繼承自ExecutableArchiveLauncher類
*/
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);
	}

}           

main函數中,執行個體化JarLauncher,然後調用父類Launcher中的launch方法

/**
* 是啟動器的父類,它可以啟動一個應用程式使用一個完全已配置好的類路徑,通過一個或多個的歸檔檔案
*/
public abstract class Launcher {
	
	/**
	* 啟動一個應用程式,這個方法是初始的入口點,
	* 可以被子類中的“public static void main(String[] args)”方法所調用
	*/
	protected void launch(String[] args) throws Exception {
		JarFile.registerUrlProtocolHandler();
		// 根據從classpath目錄下擷取的歸檔類集合,來建立一個自定義類加載器
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
		// getMainClass():從manifest檔案中擷取屬性為Start-Class對應的值
		launch(args, getMainClass(), classLoader);
	}
	
	/**
	 * 為指定的歸檔類集合建立一個來加載器
	 */
	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]));
	}

	/**
	 * Create a classloader for the specified URLs.
	 * 根據指定的URL集合(該url是在伺服器上的絕對路徑),
	 * 建立一個類加載器LaunchedURLClassLoader
	 */
	protected ClassLoader createClassLoader(URL[] urls) throws Exception {
		// 将目前啟動器類的類加載作為父加載器(目前啟動器類的加載器是應用類加載器)來建立自定義類加載器
		return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
	}

	/**
	 * Returns the main class that should be launched.
	 * 從manifest檔案中擷取屬性為Start-Class對應的值
	 * Start-Class: com.example.springboot.SpringbootApplication
	 */
	protected abstract String getMainClass() throws Exception;

	/**
	 * 根據給定的歸檔類檔案和完全已配置好的類加載器來啟動一個應用程式
	 * @param args 輸入的參數
	 * @param mainClass 從manifest中擷取Start-Class屬性的值(com.example.springboot.SpringbootApplication)
	 * @param classLoader 該類加載器用于加載從manifest中擷取Start-Class屬性的值
	 * @throws Exception if the launch fails
	 */
	protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
		// 将類加載器設定到目前線程的上線文加載器中
		Thread.currentThread().setContextClassLoader(classLoader);
		// 建立一個主函數的運作期
		createMainMethodRunner(mainClass, args, classLoader).run();
	}

	/**
	 * 建立MainMethodRunner的執行個體用于啟動應用程式
	 */
	protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
		return new MainMethodRunner(mainClass, args);
	}
}           

接下來我們分析launch(String[] args)方法

/**
 * 傳回一個歸檔類的集合,可以用于構造一個類路徑,
 * 這個是一個抽象方法,交由子類(ExecutableArchiveLauncher)實作
 */
protected abstract List<Archive> getClassPathArchives() throws Exception;           

Launcher的子類ExecutableArchiveLauncher實作了getClassPathArchives方法

/**
* 為可執行(JarLauncher)歸檔類的父類
*/
public abstract class ExecutableArchiveLauncher extends Launcher {
	@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;
	}
	
	@Override
	protected List<Archive> getClassPathArchives() throws Exception {
		List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
		postProcessClassPathArchives(archives);
		return archives;
	}

	/**
	 * 确定指定的JarEntry是否為嵌套項,可以被添加到classpath下,該方法會被每個entry調用一次
	 * 該方法是一個抽象方法,最終會調用JarLauncher中的isNestedArchive方法
	 */
	protected abstract boolean isNestedArchive(Archive.Entry entry);
	
	/**
	 * 此方法沒有任何處理,可以不用考慮
	 */
	protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
	}
}           

Archive是一個接口,最終會調用實作類JarFileArchive中的getNestedArchives

/**
 * 一個歸檔類可以被啟動器中的啟動,該類是一個接口,
 * 由對應的實作類(JarFileArchive 和 ExplodedArchive)去實作
 */
public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
	/**
	 * 傳回一個嵌套的歸檔類集合,通過比對特定的過濾器
	 */
	List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}           

JarFileArchive類中的部分源碼

public class JarFileArchive implements Archive {
	
	/**
	* 傳回一個歸檔類集合,根據filter過濾器,來比對對應的entry
	*/
	@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);
	}

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

輔助類“MainMethodRunner”

/**
 * 輔助類,被用于啟動類去調用main方法,該啟動器類包含一個main方法,使用線程上下文類加載器加載
 */
public class MainMethodRunner {

	private final String mainClassName;

	private final String[] args;

	/**
	 * Create a new {@link MainMethodRunner} instance.
	 * @param mainClass the main class
	 * @param args incoming arguments
	 */
	public MainMethodRunner(String mainClass, String[] args) {
		this.mainClassName = mainClass;
		this.args = (args != null) ? args.clone() : null;
	}
	
	/**
	* 通過反射的方式去調用SpringbootApplication類中的main方法
	*/
	public void run() throws Exception {
		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.invoke(null, new Object[] { this.args });
	}

}           

最後,在MainMethodRunner類中會調用run方法,先擷取目前線程的上線文類加載器,然後使用該類加載器将“com.example.springboot.SpringbootApplication”啟動類來進行加載,然後以反射的方式擷取啟動類中的main方法,最終執行該main方法進行啟動。