以编程方式启动 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 可以在后台处理许多琐碎的任务,让开发人员腾出时间并专注于更有用的工作,例如创建新功能、测试等。