以編程方式啟動 Spring Boot 應用程序

已發表: 2022-03-11

本文將演示如何從另一個 Java 程序啟動 Spring Boot 應用程序。 Spring Boot 應用程序通常構建在單個可執行 JAR 歸檔文件中。 它包含內部的所有依賴項,打包為嵌套的 JAR。

同樣,Spring Boot 項目通常由提供的 maven 插件構建為可執行 JAR 文件,該插件完成所有臟活。 結果是一個方便的單一 JAR 文件,易於與他人共享、部署在服務器上等等。

啟動 Spring Boot 應用程序就像鍵入java -jar mySpringProg.jar一樣簡單,應用程序將在控制台上打印一些格式良好的信息消息。

但是,如果 Spring Boot 開發人員想要在沒有人工干預的情況下從另一個 Java 程序運行應用程序怎麼辦?

嵌套 JAR 的工作原理

要將具有所有依賴項的 Java 程序打包到一個可運行的 JAR 文件中,必須提供也是 JAR 文件的依賴項,並以某種方式存儲在最終的可運行 JAR 文件中。

“陰影”是一種選擇。 著色依賴項是包含和重命名依賴項、重新定位類以及重寫受影響的字節碼和資源以創建與應用程序(項目)自己的代碼捆綁在一起的副本的過程。

著色允許用戶從依賴項中解壓縮所有類和資源,並將它們打包回可運行的 JAR 文件中。 這可能適用於簡單的場景,但是,如果兩個依賴項包含具有完全相同名稱和路徑的相同資源文件或類,它們將重疊並且程序可能無法運行。

Spring Boot 採用不同的方法並將依賴 JAR 打包到可運行 JAR 中,作為嵌套 JAR。

 example.jar | +-META-INF | +-MANIFEST.MF +-org | +-springframework | +-boot | +-loader | +-<spring boot loader classes> +-BOOT-INF +-classes | +-mycompany | +-project | +-YourClasses.class +-lib +-dependency1.jar +-dependency2.jar

JAR 歸檔被組織為標準的 Java 可運行 JAR 文件。 Spring Boot 加載器類位於org/springframework/boot/loader路徑,而用戶類和依賴項位於BOOT-INF/classesBOOT-INF/lib

注意:如果您是 Spring 新手,您可能還想看看我們的 Top 10 Most Common Spring Framework Mistakes 文章

一個典型的 Spring Boot JAR 文件包含三種類型的條目:

  • 項目類
  • 嵌套的 JAR 庫
  • Spring Boot 加載器類

Spring Boot Classloader 將首先在類路徑中設置 JAR 庫,然後是項目類,這在從 IDE(Eclipse、IntelliJ)和從控制台運行 Spring Boot 應用程序之間略有不同。

有關類覆蓋和類加載器的更多信息,您可以查閱這篇文章。

啟動 Spring Boot 應用程序

從命令行或 shell 手動啟動 Spring Boot 應用程序很容易,只需鍵入以下內容:

 java -jar example.jar

但是,從另一個 Java 程序以編程方式啟動 Spring Boot 應用程序需要更多的努力。 需要加載org/springframework/boot/loader/*.class代碼,使用一點 Java 反射來實例化JarFileArchiveJarLauncher並調用launch(String[])方法。

我們將在以下各節中更詳細地了解這是如何實現的。

加載 Spring Boot 加載器類

正如我們已經指出的,Spring Boot JAR 文件就像任何 JAR 存檔一樣。 可以加載org/springframework/boot/loader/*.class條目,創建 Class 對象,然後使用它們來啟動 Spring Boot 應用程序。

 import java.net.URLClassLoader; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.jar.JarEntry; import java.util.jar.JarFile; . . . public static void loadJar(final String pathToJar) throws IOException . . . { // Class name to Class object mapping. final Map<String, Class<?>> classMap = new HashMap<>(); final JarFile jarFile = new JarFile(pathToJar); final Enumeration<JarEntry> jarEntryEnum = jarFile.entries(); final URL[] urls = { new URL("jar:file:" + pathToJar + "!/") }; final URLClassLoader urlClassLoader = URLClassLoader.newInstance(urls);

在這裡我們可以看到classMap將保存Class 對象映射到它們各自的包名,例如,字符串值org.springframework.boot.loader.JarLauncher將映射到JarLauncher.class對象。

 while (jarEntryEnum.hasMoreElements()) { final JarEntry jarEntry = jarEntryEnum.nextElement(); if (jarEntry.getName().startsWith("org/springframework/boot") && jarEntry.getName().endsWith(".class") == true) { int endIndex = jarEntryName.lastIndexOf(".class"); className = jarEntryName.substring(0, endIndex).replace('/', '.'); try { final Class<?> loadedClass = urlClassLoader.loadClass(className); result.put(loadedClass.getName(), loadedClass); } catch (final ClassNotFoundException ex) { } } } jarFile.close();

while 循環的最終結果是一個填充有 Spring Boot 加載器類對象的映射。

自動化實際啟動

完成加載後,我們可以繼續完成自動啟動並使用它來實際啟動我們的應用程序。

Java 反射允許從加載的類中創建對象,這在我們教程的上下文中非常有用。

第一步是創建一個JarFileArchive對象。

 // Create JarFileArchive(File) object, needed for JarLauncher. final Class<?> jarFileArchiveClass = result.get("org.springframework.boot.loader.archive.JarFileArchive"); final Constructor<?> jarFileArchiveConstructor = jarFileArchiveClass.getConstructor(File.class); final Object jarFileArchive = jarFileArchiveConstructor.newInstance(new File(pathToJar));

JarFileArchive對象的構造函數將File(String)對像作為參數,因此必須提供它。

下一步是創建一個JarLauncher對象,該對像在其構造函數中需要Archive

 final Class<?> archiveClass = result.get("org.springframework.boot.loader.archive.Archive"); // Create JarLauncher object using JarLauncher(Archive) constructor. final Constructor<?> jarLauncherConstructor = mainClass.getDeclaredConstructor(archiveClass); jarLauncherConstructor.setAccessible(true); final Object jarLauncher = jarLauncherConstructor.newInstance(jarFileArchive);

為避免混淆,請注意Archive實際上是一個接口,而JarFileArchive是其中一種實現。

該過程的最後一步是在我們新創建的jarLauncher對像上調用launch(String[])方法。 這相對簡單,只需要幾行代碼。

 // Invoke JarLauncher#launch(String[]) method. final Class<?> launcherClass = result.get("org.springframework.boot.loader.Launcher"); final Method launchMethod = launcherClass.getDeclaredMethod("launch", String[].class); launchMethod.setAccessible(true); launchMethod.invoke(jarLauncher, new Object[]{new String[0]});

invoke(jarLauncer, new Object[]{new String[0]})方法最終將啟動 Spring Boot 應用程序。 請注意,主線程將在此處停止並等待 Spring Boot 應用程序終止。

關於 Spring Boot 類加載器的一句話

檢查我們的 Spring Boot JAR 文件將顯示以下結構:

 +--- mySpringApp1-0.0.1-SNAPSHOT.jar +--- META-INF +--- BOOT-INF | +--- classes # 1 - project classes | | | | | +--- com.example.mySpringApp1 | | \--- SpringBootLoaderApplication.class | | | +--- lib # 2 - nested jar libraries | +--- javax.annotation-api-1.3.1 | +--- spring-boot-2.0.0.M7.jar | \--- (...) | +--- org.springframework.boot.loader # 3 - Spring Boot loader classes +--- JarLauncher.class +--- LaunchedURLClassLoader.class \--- (...)

請注意三種類型的條目:

  • 項目類
  • 嵌套的 JAR 庫
  • Spring Boot 加載器類

項目類 ( BOOT-INF/classes ) 和嵌套 JAR ( BOOT-INF/lib ) 都由同一個類加載器LaunchedURLClassLoader處理。 此加載器位於 Spring Boot JAR 應用程序的根目錄中。

LaunchedURLClassLoader會在庫內容 ( BOOT-INF/lib ) 之後加載類內容 ( BOOT-INF/classes ),這與 IDE 不同。 例如,Eclipse 將首先將類內容放在類路徑中,然後是庫(依賴項)。

LaunchedURLClassLoader擴展了java.net.URLClassLoader ,它是使用一組將用於類加載的 URL 創建的。 URL 可能指向一個位置,如 JAR 存檔或類文件夾。 執行類加載時,會按照 URL 提供的順序遍歷 URL 指定的所有資源,並使用包含搜索到的類的第一個資源。

包起來

經典的 Java 應用程序需要在 classpath 參數中枚舉所有依賴項,這使得啟動過程有些麻煩和復雜。

相比之下,Spring Boot 應用程序方便且易於從命令行啟動。 他們管理所有依賴項,最終用戶無需擔心細節。

但是,從另一個 Java 程序啟動 Spring Boot 應用程序會使過程更加複雜,因為它需要加載 Spring Boot 的加載器類,創建專門的對象,例如JarFileArchiveJarLauncher ,然後使用 Java 反射來調用launch方法。

底線:Spring Boot 可以在後台處理許多瑣碎的任務,讓開發人員騰出時間並專注於更有用的工作,例如創建新功能、測試等。