以编程方式启动Spring Boot应用程序
本文将演示如何从另一个Java程序中启动Spring Boot应用程序。一个Spring Boot应用程序通常被构建为一个可执行的JAR归档文件。它包含所有的依赖性,以嵌套JAR的形式打包。
同样,Spring Boot项目通常由一个提供的maven插件构建成一个可执行的JAR文件,该插件完成所有的脏活累活。其结果是一个方便的、单一的JAR文件,便于与他人分享,部署在服务器上,等等。
启动Spring Boot应用程序就像输入java -jar mySpringProg.jar一样简单,应用程序会在控制台打印一些格式良好的信息。
但是,如果Spring Boot开发人员想从另一个Java程序中运行一个应用程序,而不需要人工干预,该怎么办?
嵌套的JARs是如何工作的
为了将一个Java程序与所有依赖关系打包成一个可运行的JAR文件,必须提供也是JAR文件的依赖关系,并以某种方式存储在最终可运行的JAR文件中。
"阴影 "是一种选择。阴影依赖是包括和重命名依赖的过程,重新定位类,重写受影响的字节码和资源,以便创建一个与应用程序(项目)自身代码捆绑在一起的副本。
Shading允许用户从依赖关系中解压所有的类和资源,并将它们打包到可运行的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/classes 和BOOT-INF/lib 。
一个典型的Spring Boot JAR文件包含三种类型的条目
- 项目类
- 嵌套的JAR库
- Spring Boot加载器类
Spring Boot Classloader将首先在classpath中设置JAR库,然后是项目类,这使得从IDE(Eclipse、IntelliJ)和从控制台运行Spring Boot应用程序之间略有不同。
启动Spring Boot应用程序
从命令行或shell手动启动Spring Boot应用程序很简单,只需输入以下内容。
java -jar example.jar
然而,从另一个Java程序中以编程方式启动Spring Boot应用程序需要更多的努力。有必要加载org/springframework/boot/loader/*.class 代码,使用一点Java反射来实例化JarFileArchive 、JarLauncher ,并调用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 Classloader的说明
检查我们的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将首先把类内容放在classpath中,然后是库(依赖关系)。
LaunchedURLClassLoader 扩展了 ,它是用一组将用于类加载的URL创建的。该URL可能指向一个位置,如JAR归档文件或类文件夹。当执行类加载时,所有由URLs指定的资源将按照URLs提供的顺序被遍历,并且第一个包含搜索到的类的资源将被使用。java.net.URLClassLoader
收尾工作
一个经典的Java应用程序需要在classpath参数中列举所有的依赖关系,这使得启动程序有些繁琐和复杂。
相比之下,Spring Boot应用程序很方便,很容易从命令行启动。它们管理所有的依赖关系,终端用户不需要担心这些细节问题。
然而,从另一个Java程序中启动Spring Boot应用程序会使程序更加复杂,因为它需要加载Spring Boot的加载器类,创建专门的对象,如JarFileArchive 和JarLauncher ,然后使用Java反射来调用launch 方法。
一句话:Spring Boot可以处理引擎盖下的许多琐碎任务,使开发人员能够腾出时间,专注于更有用的工作,如创建新功能、测试等。
了解基础知识
Spring和Spring Boot之间有什么区别?
Spring Boot使创建独立的、生产级的、基于Spring的应用程序变得简单,易于运行或部署,而Spring框架是一套全面的Java库,用于开发丰富的Web、桌面或移动应用程序。
什么是Spring Boot,它是如何工作的?
Spring Boot提供了maven模板,内置Tomcat网络服务器,以及一些预定义的配置,以简化Spring的使用。大多数Spring Boot应用程序只需要很少的Spring配置。Spring Boot用于创建Java应用程序,可以通过使用java -jar或更传统的war部署来启动。
什么是Spring Boot架构?
Spring Boot架构提供了启动器、自动配置和组件扫描,以便开始使用Spring,而不需要复杂的XML配置文件。