ApplicationContext 的附加功能

正如章节介绍中所述,org.springframework.beans.factory 包提供了管理和操作 Bean 的基本功能,包括以编程方式。org.springframework.context 包添加了 ApplicationContext 接口,它扩展了 BeanFactory 接口,并扩展了其他接口以提供更多应用框架风格的附加功能。许多人完全以声明式的方式使用 ApplicationContext,甚至不需要以编程方式创建它,而是依靠诸如 ContextLoader 等支持类作为 Jakarta EE Web 应用程序正常启动过程的一部分来自动实例化 ApplicationContext

为了以更框架化的风格增强 BeanFactory 功能,context 包还提供了以下功能:

  • 通过 MessageSource 接口,以 i18n(国际化)风格访问消息。

  • 通过 ResourceLoader 接口,访问资源,例如 URL 和文件。

  • 事件发布,即通过使用 ApplicationEventPublisher 接口向实现 ApplicationListener 接口的 Bean 发布事件。

  • 加载多个(分层的)上下文,通过 HierarchicalBeanFactory 接口让每个上下文专注于一个特定层,例如应用程序的 Web 层。

使用 MessageSource 进行国际化

ApplicationContext 接口扩展了一个名为 MessageSource 的接口,因此提供了国际化(“i18n”)功能。Spring 还提供了 HierarchicalMessageSource 接口,它可以分层解析消息。这些接口共同构成了 Spring 实现消息解析的基础。这些接口上定义的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc): 从 MessageSource 中检索消息的基本方法。当找不到指定区域设置的消息时,使用默认消息。传入的任何参数都将成为替换值,使用标准库提供的 MessageFormat 功能。

  • String getMessage(String code, Object[] args, Locale loc): 基本与上一个方法相同,但有一个区别:不能指定默认消息。如果找不到消息,则抛出 NoSuchMessageException

  • String getMessage(MessageSourceResolvable resolvable, Locale locale): 前述方法中使用的所有属性也都包装在一个名为 MessageSourceResolvable 的类中,你可以将它与此方法一起使用。

当加载 ApplicationContext 时,它会自动搜索上下文中定义的名为 messageSource 的 Bean。如果找到这样的 Bean,所有对前面方法的调用都会委托给该消息源。如果没有找到消息源,ApplicationContext 会尝试查找包含同名 Bean 的父上下文。如果找到,它将使用该 Bean 作为 MessageSource。如果 ApplicationContext 找不到任何消息源,则会实例化一个空的 DelegatingMessageSource,以便能够接受对上述方法的调用。

Spring 提供了三种 MessageSource 实现:ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource。它们都实现了 HierarchicalMessageSource,以便进行嵌套消息传递。StaticMessageSource 很少使用,但提供了以编程方式向源添加消息的方法。下面的示例展示了 ResourceBundleMessageSource

<beans>
	<bean id="messageSource"
			class="org.springframework.context.support.ResourceBundleMessageSource">
		<property name="basenames">
			<list>
				<value>format</value>
				<value>exceptions</value>
				<value>windows</value>
			</list>
		</property>
	</bean>
</beans>

该示例假设你在类路径中定义了三个名为 formatexceptionswindows 的资源包。任何解析消息的请求都以 JDK 标准方式处理,即通过 ResourceBundle 对象解析消息。为了示例的目的,假设上述两个资源包文件的内容如下:

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

下一个示例展示了运行 MessageSource 功能的程序。请记住,所有 ApplicationContext 实现也都是 MessageSource 实现,因此可以转换为 MessageSource 接口。

  • Java

  • Kotlin

public static void main(String[] args) {
	MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
	String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
	System.out.println(message);
}
fun main() {
	val resources = ClassPathXmlApplicationContext("beans.xml")
	val message = resources.getMessage("message", null, "Default", Locale.ENGLISH)
	println(message)
}

上述程序的输出结果如下:

Alligators rock!

总之,MessageSource 定义在名为 beans.xml 的文件中,该文件位于类路径的根目录下。messageSource bean 定义通过其 basenames 属性引用了多个资源包。传递给 basenames 属性列表中的这三个文件作为文件存在于类路径的根目录下,分别命名为 format.propertiesexceptions.propertieswindows.properties

下一个示例展示了传递给消息查找方法的参数。这些参数会被转换为 String 对象,并插入到查找消息的占位符中。

<beans>

	<!-- this MessageSource is being used in a web application -->
	<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
		<property name="basename" value="exceptions"/>
	</bean>

	<!-- lets inject the above MessageSource into this POJO -->
	<bean id="example" class="com.something.Example">
		<property name="messages" ref="messageSource"/>
	</bean>

</beans>
  • Java

  • Kotlin

public class Example {

	private MessageSource messages;

	public void setMessages(MessageSource messages) {
		this.messages = messages;
	}

	public void execute() {
		String message = this.messages.getMessage("argument.required",
			new Object [] {"userDao"}, "Required", Locale.ENGLISH);
		System.out.println(message);
	}
}
	class Example {

	lateinit var messages: MessageSource

	fun execute() {
		val message = messages.getMessage("argument.required",
				arrayOf("userDao"), "Required", Locale.ENGLISH)
		println(message)
	}
}

调用 execute() 方法的输出结果如下:

The userDao argument is required.

关于国际化(“i18n”),Spring 的各种 MessageSource 实现遵循与标准 JDK ResourceBundle 相同的区域设置解析和回退规则。简而言之,继续前面定义的 messageSource 示例,如果你想针对英式英语(en-GB)区域设置解析消息,你需要分别创建名为 format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties 的文件。

通常,区域设置的解析由应用程序的周边环境管理。在以下示例中,手动指定了用于解析(英式英语)消息的区域设置:

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
  • Java

  • Kotlin

public static void main(final String[] args) {
	MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
	String message = resources.getMessage("argument.required",
		new Object [] {"userDao"}, "Required", Locale.UK);
	System.out.println(message);
}
fun main() {
	val resources = ClassPathXmlApplicationContext("beans.xml")
	val message = resources.getMessage("argument.required",
			arrayOf("userDao"), "Required", Locale.UK)
	println(message)
}

运行上述程序的输出结果如下:

Ebagum lad, the 'userDao' argument is required, I say, required.

你还可以使用 MessageSourceAware 接口来获取已定义的任何 MessageSource 的引用。在 ApplicationContext 中定义的任何实现 MessageSourceAware 接口的 Bean,在创建和配置时都会被注入应用程序上下文的 MessageSource

因为 Spring 的 MessageSource 基于 Java 的 ResourceBundle,所以它不会合并具有相同基本名称的捆绑包,而只会使用找到的第一个捆绑包。后续具有相同基本名称的消息捆绑包将被忽略。
作为 ResourceBundleMessageSource 的替代方案,Spring 提供了一个 ReloadableResourceBundleMessageSource 类。这个变体支持相同的捆绑包文件格式,但比标准的基于 JDK 的 ResourceBundleMessageSource 实现更灵活。特别是,它允许从任何 Spring 资源位置(不仅仅是类路径)读取文件,并支持捆绑包属性文件的热重载(同时在两次重载之间高效地缓存它们)。有关详细信息,请参阅 ReloadableResourceBundleMessageSource 的 Javadoc。

标准和自定义事件

ApplicationContext 中的事件处理通过 ApplicationEvent 类和 ApplicationListener 接口提供。如果在上下文中部署了一个实现 ApplicationListener 接口的 Bean,那么每次有 ApplicationEvent 发布到 ApplicationContext 时,该 Bean 都会收到通知。本质上,这是标准的 Observer(观察者)设计模式。

从 Spring 4.2 开始,事件基础设施得到了显著改进,提供了基于注解的模型,以及发布任何任意事件(即不一定继承自 ApplicationEvent 的对象)的能力。当发布此类对象时,我们会将其包装在一个事件中。

下表描述了 Spring 提供的标准事件:

表 1. 内置事件
事件 说明

ContextRefreshedEvent

ApplicationContext 初始化或刷新时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。这里的“初始化”意味着所有 Bean 都已加载,后置处理器 Bean 已被检测并激活,单例 Bean 已预实例化,并且 ApplicationContext 对象已准备就绪。只要上下文尚未关闭,刷新可以被多次触发,前提是所选的 ApplicationContext 确实支持这种“热刷新”。例如,XmlWebApplicationContext 支持热刷新,而 GenericApplicationContext 不支持。

ContextStartedEvent

ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 start() 方法启动时发布。这里的“启动”意味着所有 Lifecycle Bean 都接收到一个明确的启动信号。通常,此信号用于在显式停止后重新启动 Bean,但它也可以用于启动尚未配置为自动启动的组件(例如,在初始化时尚未启动的组件)。

ContextStoppedEvent

ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 stop() 方法停止时发布。这里的“停止”意味着所有 Lifecycle Bean 都接收到一个明确的停止信号。一个停止的上下文可以通过调用 start() 来重新启动。

ContextClosedEvent

ApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM 关闭钩子关闭时发布。这里的“关闭”意味着所有单例 Bean 都将被销毁。上下文一旦关闭,就到达其生命周期结束,不能再刷新或重新启动。

RequestHandledEvent

一个 Web 特定的事件,通知所有 Bean 已处理完一个 HTTP 请求。此事件在请求完成后发布。此事件仅适用于使用 Spring 的 DispatcherServlet 的 Web 应用程序。

ServletRequestHandledEvent

RequestHandledEvent 的一个子类,添加了 Servlet 特定的上下文信息。

你还可以创建和发布自己的自定义事件。下面的示例展示了一个继承自 Spring 的 ApplicationEvent 基类的简单类:

  • Java

  • Kotlin

public class BlockedListEvent extends ApplicationEvent {

	private final String address;
	private final String content;

	public BlockedListEvent(Object source, String address, String content) {
		super(source);
		this.address = address;
		this.content = content;
	}

	// accessor and other methods...
}
class BlockedListEvent(source: Any,
					val address: String,
					val content: String) : ApplicationEvent(source)

要发布自定义的 ApplicationEvent,可以在 ApplicationEventPublisher 上调用 publishEvent() 方法。通常,这通过创建一个实现 ApplicationEventPublisherAware 的类并将其注册为 Spring Bean 来完成。下面的示例展示了这样一个类:

  • Java

  • Kotlin

public class EmailService implements ApplicationEventPublisherAware {

	private List<String> blockedList;
	private ApplicationEventPublisher publisher;

	public void setBlockedList(List<String> blockedList) {
		this.blockedList = blockedList;
	}

	public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
		this.publisher = publisher;
	}

	public void sendEmail(String address, String content) {
		if (blockedList.contains(address)) {
			publisher.publishEvent(new BlockedListEvent(this, address, content));
			return;
		}
		// send email...
	}
}
class EmailService : ApplicationEventPublisherAware {

	private lateinit var blockedList: List<String>
	private lateinit var publisher: ApplicationEventPublisher

	fun setBlockedList(blockedList: List<String>) {
		this.blockedList = blockedList
	}

	override fun setApplicationEventPublisher(publisher: ApplicationEventPublisher) {
		this.publisher = publisher
	}

	fun sendEmail(address: String, content: String) {
		if (blockedList!!.contains(address)) {
			publisher!!.publishEvent(BlockedListEvent(this, address, content))
			return
		}
		// send email...
	}
}

在配置时,Spring 容器会检测到 EmailService 实现了 ApplicationEventPublisherAware,并自动调用 setApplicationEventPublisher()。实际上,传入的参数就是 Spring 容器本身。你正在通过其 ApplicationEventPublisher 接口与应用程序上下文进行交互。

要接收自定义的 ApplicationEvent,你可以创建一个实现 ApplicationListener 的类并将其注册为 Spring Bean。下面的示例展示了这样一个类:

  • Java

  • Kotlin

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

	private String notificationAddress;

	public void setNotificationAddress(String notificationAddress) {
		this.notificationAddress = notificationAddress;
	}

	public void onApplicationEvent(BlockedListEvent event) {
		// notify appropriate parties via notificationAddress...
	}
}
class BlockedListNotifier : ApplicationListener<BlockedListEvent> {

	lateinit var notificationAddress: String

	override fun onApplicationEvent(event: BlockedListEvent) {
		// notify appropriate parties via notificationAddress...
	}
}

注意,ApplicationListener 通过你的自定义事件类型(在前面的示例中是 BlockedListEvent)进行了泛型参数化。这意味着 onApplicationEvent() 方法可以保持类型安全,避免了向下转型的需要。你可以注册任意数量的事件监听器,但请注意,默认情况下,事件监听器是同步接收事件的。这意味着 publishEvent() 方法会阻塞,直到所有监听器都处理完事件。这种同步单线程方法的一个优点是,当监听器接收到事件时,如果存在事务上下文,它会在发布者的事务上下文中操作。如果需要另一种事件发布策略,例如默认异步事件处理,请参阅 Spring 的 ApplicationEventMulticaster 接口和 SimpleApplicationEventMulticaster 实现的 Javadoc,了解可应用于自定义“applicationEventMulticaster”bean 定义的配置选项。在这些情况下,ThreadLocals 和日志上下文不会传播到事件处理中。有关可观测性问题的更多信息,请参阅 @EventListener 可观测性部分

以下示例展示了用于注册和配置上述每个类的 Bean 定义:

<bean id="emailService" class="example.EmailService">
	<property name="blockedList">
		<list>
			<value>[email protected]</value>
			<value>[email protected]</value>
			<value>[email protected]</value>
		</list>
	</property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
	<property name="notificationAddress" value="[email protected]"/>
</bean>

<!-- optional: a custom ApplicationEventMulticaster definition -->
<bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">
	<property name="taskExecutor" ref="..."/>
	<property name="errorHandler" ref="..."/>
</bean>

总之,当调用 emailService Bean 的 sendEmail() 方法时,如果存在任何应该被阻止的电子邮件消息,就会发布一个类型为 BlockedListEvent 的自定义事件。blockedListNotifier Bean 被注册为 ApplicationListener,并接收到 BlockedListEvent,此时它可以通知相关方。

Spring 的事件机制旨在实现同一应用程序上下文内 Spring Bean 之间的简单通信。然而,对于更复杂的企业集成需求,独立维护的 Spring Integration 项目提供了完整支持,用于构建轻量级、模式驱动的事件驱动架构,这些架构基于众所周知的 Spring 编程模型。

基于注解的事件监听器

你可以使用 @EventListener 注解在托管 Bean 的任何方法上注册事件监听器。BlockedListNotifier 可以重写如下:

  • Java

  • Kotlin

public class BlockedListNotifier {

	private String notificationAddress;

	public void setNotificationAddress(String notificationAddress) {
		this.notificationAddress = notificationAddress;
	}

	@EventListener
	public void processBlockedListEvent(BlockedListEvent event) {
		// notify appropriate parties via notificationAddress...
	}
}
class BlockedListNotifier {

	lateinit var notificationAddress: String

	@EventListener
	fun processBlockedListEvent(event: BlockedListEvent) {
		// notify appropriate parties via notificationAddress...
	}
}
不要将此类 Bean 定义为延迟初始化(lazy),因为 ApplicationContext 会遵循此设置,并且不会注册该方法来监听事件。

方法签名再次声明了它监听的事件类型,但这次名称更灵活,并且无需实现特定的监听器接口。事件类型也可以通过泛型进行收窄,只要实际的事件类型在其实现层次结构中解析了你的泛型参数。

如果你的方法应该监听多个事件,或者你想将方法定义为不带任何参数,事件类型也可以在注解本身上指定。以下示例展示了如何操作:

  • Java

  • Kotlin

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
	// ...
}
@EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class)
fun handleContextStart() {
	// ...
}

还可以通过使用注解的 condition 属性添加额外的运行时过滤,该属性定义了一个SpEL 表达式,该表达式必须匹配才能实际为特定事件调用该方法。

以下示例展示了如何重写我们的通知器,使其仅在事件的 content 属性等于 my-event 时才被调用:

  • Java

  • Kotlin

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
	// notify appropriate parties via notificationAddress...
}
@EventListener(condition = "#blEvent.content == 'my-event'")
fun processBlockedListEvent(blEvent: BlockedListEvent) {
	// notify appropriate parties via notificationAddress...
}

每个 SpEL 表达式都在一个专用上下文中进行评估。下表列出了可用于上下文的项,以便您可以使用它们进行条件事件处理

表 2. SpEL 表达式中可用的事件元数据
名称 位置 描述 示例

事件

根对象

实际的 ApplicationEvent

#root.eventevent

参数数组

根对象

用于调用方法的参数(作为对象数组)。

#root.argsargsargs[0] 用于访问第一个参数,以此类推。

参数名称

评估上下文

特定方法参数的名称。如果名称不可用(例如,因为代码编译时没有使用 -parameters 标志),也可以使用 #a<#arg> 语法访问单个参数,其中 <#arg> 代表参数索引(从 0 开始)。

#blEvent#a0(您也可以使用 #p0#p<#arg> 参数表示法作为别名)

请注意,即使您的方法签名实际引用的是发布的任意对象,#root.event 也能让您访问底层事件。

如果您需要将处理另一个事件的结果发布为一个事件,您可以更改方法签名以返回应发布的事件,如下例所示

  • Java

  • Kotlin

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
	// notify appropriate parties via notificationAddress and
	// then publish a ListUpdateEvent...
}
@EventListener
fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent {
	// notify appropriate parties via notificationAddress and
	// then publish a ListUpdateEvent...
}
异步监听器不支持此功能。

handleBlockedListEvent() 方法会为它处理的每个 BlockedListEvent 发布一个新的 ListUpdateEvent。如果您需要发布多个事件,您可以返回一个 Collection 或事件数组来代替。

异步监听器

如果您希望特定的监听器异步处理事件,您可以重用常规的 @Async 支持。以下示例展示了如何这样做

  • Java

  • Kotlin

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
	// BlockedListEvent is processed in a separate thread
}
@EventListener
@Async
fun processBlockedListEvent(event: BlockedListEvent) {
	// BlockedListEvent is processed in a separate thread
}

使用异步事件时请注意以下限制

  • 如果异步事件监听器抛出 Exception,它不会传播给调用者。有关更多详细信息,请参阅 AsyncUncaughtExceptionHandler

  • 异步事件监听方法不能通过返回值来发布后续事件。如果您需要将处理结果作为另一个事件发布,请注入 ApplicationEventPublisher 以手动发布事件。

  • 事件处理默认不会传播 ThreadLocals 和日志上下文。有关可观察性问题的更多信息,请参阅 @EventListener 可观察性部分

监听器排序

如果您需要一个监听器在另一个监听器之前被调用,您可以在方法声明上添加 @Order 注解,如下例所示

  • Java

  • Kotlin

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
	// notify appropriate parties via notificationAddress...
}
@EventListener
@Order(42)
fun processBlockedListEvent(event: BlockedListEvent) {
	// notify appropriate parties via notificationAddress...
}

泛型事件

您还可以使用泛型来进一步定义事件的结构。考虑使用一个 EntityCreatedEvent<T>,其中 T 是实际创建的实体的类型。例如,您可以创建以下监听器定义,以便只接收针对 PersonEntityCreatedEvent

  • Java

  • Kotlin

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
	// ...
}
@EventListener
fun onPersonCreated(event: EntityCreatedEvent<Person>) {
	// ...
}

由于类型擦除,这仅在触发的事件解析了事件监听器过滤所基于的泛型参数时才起作用(即,类似于 class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ } 的东西)。

在某些情况下,如果所有事件都遵循相同的结构(如前面示例中的事件那样),这可能会变得非常繁琐。在这种情况下,您可以实现 ResolvableTypeProvider 来引导框架超越运行时环境所提供的能力。以下事件展示了如何这样做

  • Java

  • Kotlin

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

	public EntityCreatedEvent(T entity) {
		super(entity);
	}

	@Override
	public ResolvableType getResolvableType() {
		return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
	}
}
class EntityCreatedEvent<T>(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider {

	override fun getResolvableType(): ResolvableType? {
		return ResolvableType.forClassWithGenerics(javaClass, ResolvableType.forInstance(getSource()))
	}
}
这不仅适用于 ApplicationEvent,也适用于您作为事件发送的任何任意对象。

最后,与经典的 ApplicationListener 实现一样,实际的多播是通过运行时在上下文范围内的 ApplicationEventMulticaster 进行的。默认情况下,这是一个 SimpleApplicationEventMulticaster,它在调用线程中同步发布事件。可以通过“applicationEventMulticaster”bean 定义来替换/定制它,例如,用于异步处理所有事件和/或处理监听器异常

@Bean
ApplicationEventMulticaster applicationEventMulticaster() {
	SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
	multicaster.setTaskExecutor(...);
	multicaster.setErrorHandler(...);
	return multicaster;
}

便捷访问低级资源

为了最佳地使用和理解应用程序上下文,您应该熟悉 Spring 的 Resource 抽象,如 资源 中所述。

应用程序上下文是一个 ResourceLoader,可用于加载 Resource 对象。Resource 本质上是 JDK java.net.URL 类的更丰富版本。实际上,Resource 的实现会在适当情况下包装一个 java.net.URL 实例。Resource 可以透明地从几乎任何位置获取低级资源,包括类路径、文件系统位置、可用标准 URL 描述的任何位置以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,则这些资源来源于何处特定于实际的应用程序上下文类型并与其相符。

您可以配置部署到应用程序上下文中的 bean 来实现特殊的 callback 接口 ResourceLoaderAware,以便在初始化时自动回调,并将应用程序上下文本身作为 ResourceLoader 传入。您还可以暴露 Resource 类型的属性,用于访问静态资源。它们像其他任何属性一样被注入。您可以将这些 Resource 属性指定为简单的 String 路径,并在部署 bean 时依靠自动转换将这些文本字符串转换为实际的 Resource 对象。

提供给 ApplicationContext 构造函数的位置路径实际上是资源字符串,以简单形式表示时,会根据具体的上下文实现进行适当处理。例如,ClassPathXmlApplicationContext 将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或 URL 加载定义,无论实际上下文类型如何。

应用程序启动跟踪

ApplicationContext 管理 Spring 应用程序的生命周期,并提供围绕组件的丰富编程模型。因此,复杂的应用程序可以拥有同样复杂的组件图和启动阶段。

使用特定指标跟踪应用程序启动步骤有助于理解启动阶段的时间花费在哪里,它也可以作为更好地理解整个上下文生命周期的一种方式。

AbstractApplicationContext(及其子类)通过 ApplicationStartup 进行检测,它收集有关各种启动阶段的 StartupStep 数据

  • 应用程序上下文生命周期(基础包扫描、配置类管理)

  • beans 生命周期(实例化、智能初始化、后处理)

  • 应用程序事件处理

以下是 AnnotationConfigApplicationContext 中检测的一个示例

  • Java

  • Kotlin

// create a startup step and start recording
StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();
// create a startup step and start recording
val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages))
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages)
// end the current step
scanPackages.end()

应用程序上下文已经通过多个步骤进行了检测。一旦记录下来,这些启动步骤就可以通过特定工具进行收集、显示和分析。有关现有启动步骤的完整列表,您可以查阅专门的附录部分

默认的 ApplicationStartup 实现是一个空操作变体,用于最小开销。这意味着默认情况下在应用程序启动期间不会收集任何指标。Spring Framework 提供了一个使用 Java Flight Recorder 跟踪启动步骤的实现:FlightRecorderApplicationStartup。要使用此变体,您必须在 ApplicationContext 创建后立即为其配置一个实例。

如果开发者提供自己的 AbstractApplicationContext 子类,或者希望收集更精确的数据,他们也可以使用 ApplicationStartup 基础设施。

ApplicationStartup 仅用于应用程序启动期间和核心容器;它绝不是 Java 分析器或像 Micrometer 这样的指标库的替代品。

要开始收集自定义的 StartupStep,组件可以直接从应用程序上下文获取 ApplicationStartup 实例,让其组件实现 ApplicationStartupAware,或在任何注入点请求 ApplicationStartup 类型。

开发者在创建自定义启动步骤时,不应使用 "spring.*" 命名空间。此命名空间保留用于 Spring 内部使用,并且可能会更改。

Web 应用中便捷的 ApplicationContext 实例化

您可以通过声明方式创建 ApplicationContext 实例,例如使用 ContextLoader。当然,您也可以通过使用 ApplicationContext 实现之一以编程方式创建 ApplicationContext 实例。

您可以通过使用 ContextLoaderListener 注册 ApplicationContext,如下例所示

<context-param>
	<param-name>contextConfigLocation</param-name>
	<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器检查 contextConfigLocation 参数。如果该参数不存在,监听器会默认使用 /WEB-INF/applicationContext.xml。如果该参数存在,监听器会使用预定义的定界符(逗号、分号和空格)分隔 String,并使用这些值作为搜索应用程序上下文的位置。也支持 Ant 风格的路径模式。示例包括 /WEB-INF/*Context.xml(用于 WEB-INF 目录下所有以 Context.xml 结尾的文件)和 /WEB-INF/**/*Context.xml(用于 WEB-INF 的任何子目录中的所有此类文件)。

将 Spring ApplicationContext 部署为 Jakarta EE RAR 文件

可以将 Spring ApplicationContext 部署为 RAR 文件,将上下文及其所有必需的 bean 类和库 JAR 封装在一个 Jakarta EE RAR 部署单元中。这相当于引导一个独立的 ApplicationContext(仅托管在 Jakarta EE 环境中),并且能够访问 Jakarta EE 服务器设施。RAR 部署是部署无头 WAR 文件场景的更自然替代方案——实际上,无头 WAR 文件是一个没有任何 HTTP 入口点,仅用于在 Jakarta EE 环境中引导 Spring ApplicationContext 的 WAR 文件。

RAR 部署非常适合不需要 HTTP 入口点,而仅包含消息端点和计划任务的应用程序上下文。此类上下文中的 bean 可以使用应用服务器资源,例如 JTA 事务管理器以及 JNDI 绑定的 JDBC DataSource 实例和 JMS ConnectionFactory 实例,并且还可以向平台的 JMX 服务器注册——所有这些都通过 Spring 的标准事务管理、JNDI 和 JMX 支持设施实现。应用程序组件还可以通过 Spring 的 TaskExecutor 抽象与应用服务器的 JCA WorkManager 进行交互。

有关 RAR 部署所涉及的配置详细信息,请参阅 SpringContextResourceAdapter 类的 javadoc。

简单地将 Spring ApplicationContext 部署为 Jakarta EE RAR 文件

  1. 将所有应用程序类打包到 RAR 文件中(RAR 文件是带有不同文件扩展名的标准 JAR 文件)。

  2. 将所有必需的库 JAR 添加到 RAR 归档的根目录中。

  3. 添加 META-INF/ra.xml 部署描述符(如 SpringContextResourceAdapter 的 javadoc 中所示)和相应的 Spring XML bean 定义文件(通常是 META-INF/applicationContext.xml)。

  4. 将生成的 RAR 文件放到您的应用服务器部署目录中。

此类 RAR 部署单元通常是自包含的。它们不会将组件暴露给外部世界,甚至不会暴露给同一应用程序的其他模块。与基于 RAR 的 ApplicationContext 的交互通常通过它与其他模块共享的 JMS 目标发生。基于 RAR 的 ApplicationContext 还可以,例如,调度一些任务或对文件系统中的新文件做出反应(或类似操作)。如果需要允许来自外部的同步访问,它可以(例如)导出 RMI 端点,这些端点可供同一机器上的其他应用程序模块使用。