Spring Boot 源碼分析 - Jar 包的啟動實現


參考 知識星球芋道源碼 星球的源碼解析,一個活躍度非常高的 Java 技術社群,感興趣的小伙伴可以加入 芋道源碼 星球,一起學習😄

該系列文章是筆者在學習 Spring Boot 過程中總結下來的,里面涉及到相關源碼,可能對讀者不太友好,請結合我的源碼注釋 Spring Boot 源碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 源碼有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點擊一下“推薦”,也可以關注博主,感激不盡~

該系列其他文章請查看:《精盡 Spring Boot 源碼分析 - 文章導讀》

概述

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的將我們的 Spring Boot 項目打成 jar 包或者 war 包。

考慮到部署的便利性,我們絕大多數(99.99%)的場景下,都會選擇打成 jar 包,這樣一來,我們就無需將項目部署於 Tomcat、Jetty 等 Servlet 容器中。

那么,通過 Spring Boot 插件生成的 jar 包是如何運行,並啟動 Spring Boot 應用的呢?這個就是本文的目的,我們一起來弄懂 Spring Boot jar 包的運行原理

這里,我通過 Spring Boot Maven Plugin 生成了一個 jar 包,其里面的結構如下所示:

  1. BOOT-INF 目錄,里面保存了我們自己 Spring Boot 項目編譯后的所有文件,其中 classes 目錄下面就是編譯后的 .class 文件,包括項目中的配置文件等,lib 目錄下就是我們引入的第三方依賴
  2. META-INF 目錄,通過 MANIFEST.MF 文件提供 jar 包的元數據,聲明 jar 的啟動類等信息。每個 Java jar 包應該是都有這個文件的,參考 Oracle 官方對於 jar 的說明,里面有一個 Main-Class 配置用於指定啟動類
  3. org.springframework.boot.loader 目錄,也就是 Spring Boot 的 spring-boot-loader 工具模塊,它就是 java -jar xxx.jar 啟動 Spring Boot 項目的秘密所在,上面的 Main-Class 指定的就是該工具模塊中的一個類

MANIFEST.MF

META-INF/MANIFEST.MF 文件如下:

Manifest-Version: 1.0
Implementation-Title: spring-boot-study
Implementation-Version: 1.0.0-SNAPSHOT
Built-By: jingping
Implementation-Vendor-Id: org.springframework.boot.demo
Spring-Boot-Version: 2.0.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher # spring-boot-loader 中的啟動類
Start-Class: org.springframework.boot.demo.Application # 你的 Spring Boot 項目中的啟動類
Spring-Boot-Classes: BOOT-INF/classes/ # 你的 Spring Boot 項目編譯后的 .class 文件所在目錄
Spring-Boot-Lib: BOOT-INF/lib/ # 你的 Spring Boot 項目所引入的第三方依賴所在目錄
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_251
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/info-dependencies/dwzq-info/info-stock-project/sp-provider

參考 Oracle 官方對該的說明:

  • Main-Class:Java 規定的 jar 包的啟動類,這里設置為 spring-boot-loader 項目的 JarLauncher 類,進行 Spring Boot 應用的啟動
  • Start-Class:Spring Boot 規定的啟動類,這里通過 Spring Boot Maven Plugin 插件打包時,會設置為我們定義的 Application 啟動類

為什么不直接將我們的 Application 啟動類設置為 Main-Class 啟動呢?

因為通過 Spring Boot Maven Plugin 插件打包后的 jar 包,我們的 .class 文件在 BOOT-INF/classes/ 目錄下,在 Java 默認的 jar 包加載規則下找不到我們的 Application 啟動類,也就需要通過 JarLauncher 啟動加載。

當然,還有一個原因,Java 規定可執行器的 jar 包禁止嵌套其它 jar 包,在 BOOT-INF/lib 目錄下有我們 Spring Boot 應用依賴的所有第三方 jar 包,因此spring-boot-loader 項目自定義實現了 ClassLoader 實現類 LaunchedURLClassLoader,支持加載 BOOT-INF/classes 目錄下的 .class 文件,以及 BOOT-INF/lib 目錄下的 jar 包。

接下來,我們一起來看看 Spring Boot 的 JarLauncher 這個類

1. JarLauncher

類圖:

上面的 WarLauncher 是針對 war 包的啟動類,和 JarLauncher 差不多,感興趣的可以看一看,這里我們直接來看到 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) {
		// 只接受 `BOOT-INF/classes/` 目錄
		if (entry.isDirectory()) {
			return entry.getName().equals(BOOT_INF_CLASSES);
		}
		// 只接受 `BOOT-INF/lib/` 目錄下的 jar 包
		return entry.getName().startsWith(BOOT_INF_LIB);
	}

	/**
	 * 這里是 java -jar 啟動 SpringBoot 打包后的 jar 包的入口
	 * 可查看 jar 包中的 META-INF/MANIFEST.MF 文件(該文件用於對 Java 應用進行配置)
	 * 參考 Oracle 官方對於 jar 的說明(https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html)
	 * 該文件其中會有一個配置項:Main-Class: org.springframework.boot.loader.JarLauncher
	 * 這個配置表示會調用 JarLauncher#main(String[]) 方法,也就當前方法
	 */
	public static void main(String[] args) throws Exception {
		// <1> 創建當前類的實例對象,會創建一個 Archive 對象(當前應用),可用於解析 jar 包(當前應用)中所有的信息
		// <2> 調用其 launch(String[]) 方法
		new JarLauncher().launch(args);
	}
}

可以看到它有個 main(String[]) 方法,前面說到的 META-INF/MANIFEST.MF 文件中的 Main-Class 配置就是指向了這個類,也就會調用這里的 main 方法,會做下面兩件事:

  1. 創建一個 JarLauncher 實例對象,在 ExecutableArchiveLauncher 父類中會做以下事情:

    public abstract class ExecutableArchiveLauncher extends Launcher {
    
    	private final Archive archive;
    
    	public ExecutableArchiveLauncher() {
    		try {
    			// 為當前應用創建一個 Archive 對象,可用於解析 jar 包(當前應用)中所有的信息
    			this.archive = createArchive();
    		}
    		catch (Exception ex) {
    			throw new IllegalStateException(ex);
    		}
    	}
    	
    	protected final Archive createArchive() throws Exception {
    		// 獲取 jar 包(當前應用)所在的絕對路徑
    		ProtectionDomain protectionDomain = getClass().getProtectionDomain();
    		CodeSource codeSource = protectionDomain.getCodeSource();
    		URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
    		String path = (location != null) ? location.getSchemeSpecificPart() : null;
    		if (path == null) {
    			throw new IllegalStateException("Unable to determine code source archive");
    		}
    		// 當前 jar 包
    		File root = new File(path);
    		if (!root.exists()) {
    			throw new IllegalStateException("Unable to determine code source archive from " + root);
    		}
    		// 為當前 jar 包創建一個 JarFileArchive(根條目),需要通過它解析出 jar 包中的所有信息
    		// 如果是文件夾的話則創建 ExplodedArchive(根條目)
    		return (root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root));
    	}
    }
    

    會為當前應用創建一個 Archive 對象,可用於解析 jar 包(當前應用)中所有的信息,可以把它理解為一個“根”對象,可以通過它獲取我們所需要的類信息

  2. 調用 JarLauncher#launch(String[]) 方法,也就是調用父類 Launcher 的這個方法

2. Launcher

org.springframework.boot.loader.Launcher,Spring Boot 應用的啟動器

2. launch 方法

public abstract class Launcher {

	/**
	 * Launch the application. This method is the initial entry point that should be
	 * called by a subclass {@code public static void main(String[] args)} method.
	 * @param args the incoming arguments
	 * @throws Exception if the application fails to launch
	 */
	protected void launch(String[] args) throws Exception {
		// <1> 注冊 URL(jar)協議的處理器
		JarFile.registerUrlProtocolHandler();
		// <2> 先從 `archive`(當前 jar 包應用)解析出所有的 JarFileArchive
		// <3> 創建 Spring Boot 自定義的 ClassLoader 類加載器,可加載當前 jar 中所有的類
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
		// <4> 獲取當前應用的啟動類(你自己寫的那個 main 方法)
		// <5> 執行你的那個 main 方法
		launch(args, getMainClass(), classLoader);
	}
}

會做以下幾件事:

  1. 調用 JarFile#registerUrlProtocolHandler() 方法,注冊 URL(jar)協議的處理器,主要是使用自定義的 URLStreamHandler 處理器處理 jar 包
  2. 調用 getClassPathArchives() 方法,先從 archive(當前 jar 包應用)解析出所有的 JarFileArchive,這個 archive 就是在上面創建 JarLauncher 實例對象過程中創建的
  3. 調用 createClassLoader(List<Archive>) 方法,創建 Spring Boot 自定義的 ClassLoader 類加載器,可加載當前 jar 包中所有的類,包括依賴的第三方包
  4. 調用 getMainClass() 方法,獲取當前應用的啟動類(你自己寫的那個 main 方法所在的 Class 類對象)
  5. 調用 launch(...) 方法,執行你的項目中那個啟動類的 main 方法(反射)

你可以理解為會創建一個自定義的 ClassLoader 類加載器,主要可加載 BOOT-INF/classes 目錄下的類,以及 BOOT-INF/lib 目錄下的 jar 包中的類,然后調用你 Spring Boot 應用的啟動類的 main 方法

接下來我們逐步分析上面的每個步驟

2.1 registerUrlProtocolHandler 方法

備注:注冊 URL(jar)協議的處理器

這個方法在 org.springframework.boot.loader.jar.JarFile 中,這個類是 java.util.jar.JarFile 的子類,對它進行擴展,提供更多的功能,便於操作 jar

public static void registerUrlProtocolHandler() {
    // <1> 獲取系統變量中的 `java.protocol.handler.pkgs` 配置的 URLStreamHandler 路徑
    String handlers = System.getProperty(PROTOCOL_HANDLER, "");
    // <2> 將 Spring Boot 自定義的 URL 協議處理器路徑(`org.springframework.boot.loader`)添加至系統變量中
    // JVM 啟動時會獲取 `java.protocol.handler.pkgs` 屬性,多個用 `|` 分隔,以他們作為包名前綴,然后使用 `包名前綴.協議名.Handler` 作為該協議的實現
    // 那么這里就會將 `org.springframework.boot.loader.jar.Handler` 作為 jar 包協議的實現
    System.setProperty(PROTOCOL_HANDLER,
            ("".equals(handlers) ? HANDLERS_PACKAGE : handlers + "|" + HANDLERS_PACKAGE));
    // <3> 重置已緩存的 URLStreamHandler 處理器們,避免重復創建
    resetCachedUrlHandlers();
}

方法的處理過程如下:

  1. 獲取系統變量中的 java.protocol.handler.pkgs 配置的 URLStreamHandler 路徑

  2. 將 Spring Boot 自定義的 URL 協議處理器路徑(org.springframework.boot.loader)添加至系統變量中

    JVM 啟動時會獲取 java.protocol.handler.pkgs 屬性,多個用 | 分隔,以他們作為包名前綴,然后使用 包名前綴.協議名.Handler 作為該協議的實現

    那么這里就會將 org.springframework.boot.loader.jar.Handler 作為 jar 包協議的實現,用於處理 jar 包

  3. 重置已緩存的 URLStreamHandler 處理器們,避免重復創建

    private static void resetCachedUrlHandlers() {
        try {
            URL.setURLStreamHandlerFactory(null);
        } catch (Error ex) {
            // Ignore
        }
    }
    

2.2 getClassPathArchives 方法

備注:從 archive(當前 jar 包應用)解析出所有的 JarFileArchive

該方法在 org.springframework.boot.loader.ExecutableArchiveLauncher 子類中實現,如下:

@Override
protected List<Archive> getClassPathArchives() throws Exception {
    // <1> 創建一個 Archive.EntryFilter 類,用於判斷 Archive.Entry 是否匹配,過濾 jar 包(當前應用)以外的東西
    // <2> 從 `archive`(當前 jar 包)解析出所有 Archive 條目信息
    List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
    postProcessClassPathArchives(archives);
    // <3> 返回找到的所有 JarFileArchive
    // `BOOT-INF/classes/` 目錄對應一個 JarFileArchive(因為就是當前應用中的內容)
    // `BOOT-INF/lib/` 目錄下的每個 jar 包對應一個 JarFileArchive
    return archives;
}

過程如下:

  1. 創建一個 Archive.EntryFilter 實現類,用於判斷 Archive.Entry 是否匹配,過濾掉 jar 包(當前應用)以外的東西

    public class JarLauncher extends ExecutableArchiveLauncher {
    
    	static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
    	static final String BOOT_INF_LIB = "BOOT-INF/lib/";
    
    	@Override
    	protected boolean isNestedArchive(Archive.Entry entry) {
    		// 只接受 `BOOT-INF/classes/` 目錄
    		if (entry.isDirectory()) {
    			return entry.getName().equals(BOOT_INF_CLASSES);
    		}
    		// 只接受 `BOOT-INF/lib/` 目錄下的 jar 包
    		return entry.getName().startsWith(BOOT_INF_LIB);
    	}
    }
    
  2. archive(當前 jar 包)解析出所有 Archive 條目信息,這個 archive 在上面 1. JarLauncher 講到過,創建 JarLauncher 實例化對象的時候會初始化 archive,是一個 JarFileArchive 對象,也就是我們打包后的 jar 包,那么接下來需要從中解析出所有的 Archive 對象

    // JarFileArchive.java
    @Override
    public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
        List<Archive> nestedArchives = new ArrayList<>();
        // 遍歷 jar 包(當前應用)中所有的 Entry
        for (Entry entry : this) {
            // 進行過濾,`BOOT-INF/classes/` 目錄或者 `BOOT-INF/lib/` 目錄下的 jar 包
            if (filter.matches(entry)) {
                // 將 Entry 轉換成 JarFileArchive
                nestedArchives.add(getNestedArchive(entry));
            }
        }
        // 返回 jar 包(當前應用)找到的所有 JarFileArchive
        // `BOOT-INF/classes/` 目錄對應一個 JarFileArchive(因為就是當前應用中的內容)
        // `BOOT-INF/lib/` 目錄下的每個 jar 包對應一個 JarFileArchive
        return Collections.unmodifiableList(nestedArchives);
    }
    

    返回 jar 包(當前應用)找到的所有 JarFileArchive:

    • BOOT-INF/classes/ 目錄對應一個 JarFileArchive(因為就是當前 Spring Boot 應用編譯后的內容)
    • BOOT-INF/lib/ 目錄下的每個 jar 包對應一個 JarFileArchive
  3. 返回從 jar 包中找到的所有 JarFileArchive

這一步驟就是從 jar 包中解析出我們需要的東西來,如上描述,每個 JarFileArchive 會對應一個 JarFile 對象

2.3 createClassLoader 方法

備注:創建 Spring Boot 自定義的 ClassLoader 類加載器,可加載當前 jar 中所有的類

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    // <1> 獲取所有 JarFileArchive 對應的 URL
    List<URL> urls = new ArrayList<>(archives.size());
    for (Archive archive : archives) {
        urls.add(archive.getUrl());
    }
    // <2> 創建 Spring Boot 自定義的 ClassLoader 類加載器,並設置父類加載器為當前線程的類加載器
    // 通過它解析這些 URL,也就是加載 `BOOT-INF/classes/` 目錄下的類和 `BOOT-INF/lib/` 目錄下的所有 jar 包
    return createClassLoader(urls.toArray(new URL[0]));
}

protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}

該過程如下:

  1. 獲取所有 JarFileArchive 對應的 URL
  2. 創建 Spring Boot 自定義的 ClassLoader 類加載器,並設置父類加載器為當前線程的類加載器

可以看到 LaunchedURLClassLoader 為自定義類加載器,這樣就能從我們 jar 包中的 BOOT-INF/classes/ 目錄下和 BOOT-INF/lib/ 目錄下的所有三方依賴包中加載出 Class 類對象

2.4 getMainClass 方法

備注:獲取當前應用的啟動類(你自己寫的那個 main 方法)

// ExecutableArchiveLauncher.java
@Override
protected String getMainClass() throws Exception {
    // 獲取 jar 包(當前應用)的 Manifest 對象,也就是 META-INF/MANIFEST.MF 文件中的屬性
    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;
}

過程如下:

  1. 獲取 jar 包(當前應用)的 Manifest 對象,也就是 META-INF/MANIFEST.MF 文件中的屬性
  2. 獲取啟動類(當前應用自己的啟動類),也就是 Start-Class 配置,並返回

可以看到,這一步就是找到你 Spring Boot 應用的啟動類,前面 ClassLoader 類加載器都准備好了,那么現在不就可以直接調用這個類的 main 方法來啟動應用了

2.5 launch 方法

備注:執行你的 Spring Boot 應用的啟動類的 main 方法

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
    // 設置當前線程的 ClassLoader 為剛創建的類加載器
    Thread.currentThread().setContextClassLoader(classLoader);
    // 創建一個 MainMethodRunner 對象(main 方法執行器)
    // 執行你的 main 方法(反射)
    createMainMethodRunner(mainClass, args, classLoader).run();
}

protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
    return new MainMethodRunner(mainClass, args);
}

整個過程很簡單,先設置當前線程的 ClassLoader 為剛創建的類加載器,然后創建一個 MainMethodRunner 對象(main 方法執行器),執行你的 main 方法(反射),啟動 Spring Boot 應用

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 {
		// 根據名稱加載 main 方法所在類的 Class 對象
		Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
		// 獲取 main 方法
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		// 執行這個 main 方法(反射)
		mainMethod.invoke(null, new Object[] { this.args });
	}
}

這里就是通過反射調用你的 Spring Boot 應用的啟動類的 main 方法

LaunchedURLClassLoader

org.springframework.boot.loader.LaunchedURLClassLoaderspring-boot-loader 中自定義的類加載器,實現對 jar 包中 BOOT-INF/classes 目錄下的BOOT-INF/lib 下第三方 jar 包中的加載

public class LaunchedURLClassLoader extends URLClassLoader {

	static {
		ClassLoader.registerAsParallelCapable();
	}
	/**
	 * Create a new {@link LaunchedURLClassLoader} instance.
	 * @param urls the URLs from which to load classes and resources
	 * @param parent the parent class loader for delegation
	 */
	public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
		super(urls, parent);
	}

	/**
	 * 重寫類加載器中加載 Class 類對象方法
	 */
	@Override
	protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		Handler.setUseFastConnectionExceptions(true);
		try {
			try {
				// 判斷這個類是否有對應的 Package 包
				// 沒有的話會從所有 URL(包括內部引入的所有 jar 包)中找到對應的 Package 包並進行設置
				definePackageIfNecessary(name);
			}
			catch (IllegalArgumentException ex) {
				// Tolerate race condition due to being parallel capable
				if (getPackage(name) == null) {
					// This should never happen as the IllegalArgumentException indicates
					// that the package has already been defined and, therefore,
					// getPackage(name) should not return null.
					throw new AssertionError("Package " + name + " has already been defined but it could not be found");
				}
			}
			// 加載對應的 Class 類對象
			return super.loadClass(name, resolve);
		}
		finally {
			Handler.setUseFastConnectionExceptions(false);
		}
	}

	/**
	 * Define a package before a {@code findClass} call is made. This is necessary to
	 * ensure that the appropriate manifest for nested JARs is associated with the
	 * package.
	 * @param className the class name being found
	 */
	private void definePackageIfNecessary(String className) {
		int lastDot = className.lastIndexOf('.');
		if (lastDot >= 0) {
			// 獲取包名
			String packageName = className.substring(0, lastDot);
			// 沒找到對應的 Package 包則進行解析
			if (getPackage(packageName) == null) {
				try {
					// 遍歷所有的 URL,從所有的 jar 包中找到這個類對應的 Package 包並進行設置
					definePackage(className, packageName);
				}
				catch (IllegalArgumentException ex) {
					// Tolerate race condition due to being parallel capable
					if (getPackage(packageName) == null) {
						// This should never happen as the IllegalArgumentException
						// indicates that the package has already been defined and,
						// therefore, getPackage(name) should not have returned null.
						throw new AssertionError(
								"Package " + packageName + " has already been defined but it could not be found");
					}
				}
			}
		}
	}

	private void definePackage(String className, String packageName) {
		try {
			AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
				// 把類路徑解析成類名並加上 .class 后綴
				String packageEntryName = packageName.replace('.', '/') + "/";
				String classEntryName = className.replace('.', '/') + ".class";
				// 遍歷所有的 URL(包括應用內部引入的所有 jar 包)
				for (URL url : getURLs()) {
					try {
						URLConnection connection = url.openConnection();
						if (connection instanceof JarURLConnection) {
							JarFile jarFile = ((JarURLConnection) connection).getJarFile();
							// 如果這個 jar 中存在這個類名,且有對應的 Manifest
							if (jarFile.getEntry(classEntryName) != null && jarFile.getEntry(packageEntryName) != null
									&& jarFile.getManifest() != null) {
								// 定義這個類對應的 Package 包
								definePackage(packageName, jarFile.getManifest(), url);
								return null;
							}
						}
					}
					catch (IOException ex) {
						// Ignore
					}
				}
				return null;
			}, AccessController.getContext());
		}
		catch (java.security.PrivilegedActionException ex) {
			// Ignore
		}
	}
}

上面的代碼就不一一講述了,LaunchedURLClassLoader 重寫了 ClassLoader 的 loadClass(String, boolean) 加載 Class 類對象方法,在加載對應的 Class 類對象之前新增了一部分邏輯,會嘗試從 jar 包中定義 Package 包對象,這樣就能加載到對應的 Class 類對象。

總結

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以很方便的將我們的 Spring Boot 項目打成 jar 包,jar 包中主要分為三個模塊:

  • BOOT-INF 目錄,里面保存了我們自己 Spring Boot 項目編譯后的所有文件,其中 classes 目錄下面就是編譯后的 .class 文件,包括項目中的配置文件等,lib 目錄下就是我們引入的第三方依賴
  • META-INF 目錄,通過 MANIFEST.MF 文件提供 jar 包的元數據,聲明 jar 的啟動類等信息。每個 Java jar 包應該是都有這個文件的,參考 Oracle 官方對於 jar 的說明,里面有一個 Main-Class 配置用於指定啟動類
  • org.springframework.boot.loader 目錄,也就是 Spring Boot 的 spring-boot-loader 子模塊,它就是 java -jar xxx.jar 啟動 Spring Boot 項目的秘密所在,上面的 Main-Class 指定的就是里面的一個類

通過 java -jar 啟動應用時,根據 Main-Class 配置會調用 org.springframework.boot.loader.JarLaunchermain(String[]) 方法;其中會先創建一個自定義的 ClassLoader 類加載器,可從BOOT-INF目錄下加載出我們 Spring Boot 應用的 Class 類對象,包括依賴的第三方 jar 包中的 Class 類對象;然后根據 Start-Class 配置調用我們 Spring Boot 應用啟動類的 main(String[]) 方法(反射),這樣也就啟動了應用,至於我們的 main(String[]) 方法中做了哪些事情,也就是后續所講的內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM