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。 - 加载多个(层次结构)上下文,让每个上下文专注于特定的层,例如应用程序的 Web 层,这是通过
HierarchicalBeanFactory接口实现的。
使用 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 的名称必须为 messageSource。如果找到了这样的 Bean,上述方法的所有调用都会委托给该消息源。如果未找到消息源,ApplicationContext 会尝试查找包含同名 Bean 的父上下文。如果有,它将使用该 Bean 作为 MessageSource。如果 ApplicationContext 找不到任何消息源,它将实例化一个空的 DelegatingMessageSource,以便能够接受对上述方法的调用。
Spring 提供了三个 MessageSource 实现:ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 和 StaticMessageSource。它们都实现了 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>该示例假设你在类路径中定义了三个名为 format、exceptions 和 windows 的资源包(Resource Bundle)。解析消息的任何请求都将按照 JDK 标准的方式通过 ResourceBundle 对象处理。为了示例,假设上述两个资源包文件的内容如下:
# 在 format.properties 中
message=Alligators rock!# 在 exceptions.properties 中
argument.required=The {0} argument is required.下一个示例展示了运行 MessageSource 功能的程序。请记住,所有 ApplicationContext 实现也是 MessageSource 实现,因此可以强制转换为 MessageSource 接口。
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.properties、exceptions.properties 和 windows.properties。
下一个示例展示了传递给消息查找的参数。这些参数被转换为 String 对象并插入到查找消息的占位符中。
<beans>
<!-- 此 MessageSource 正用于 Web 应用程序 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="exceptions"/>
</bean>
<!-- 让我们将上述 MessageSource 注入到此 POJO 中 -->
<bean id="example" class="com.something.Example">
<property name="messages" ref="messageSource"/>
</bean>
</beans>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 相同的区域设置(Locale)解析和回退规则。简而言之,继续之前的 messageSource 示例,如果你想针对英国(en-GB)区域设置解析消息,你需要分别创建名为 format_en_GB.properties、exceptions_en_GB.properties 和 windows_en_GB.properties 的文件。
通常,区域设置解析是由应用程序周围的环境管理的。在以下示例中,手动指定了针对其解析(英国)消息的区域设置:
# 在 exceptions_en_GB.properties 中
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.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,在创建和配置该 Bean 时会被注入应用上下文的 MessageSource。
TIP
由于 Spring 的 MessageSource 是基于 Java 的 ResourceBundle 的,它不会合并具有相同基名的资源包,而只会使用找到的第一个。后续具有相同基名的消息包将被忽略。
TIP
作为 ResourceBundleMessageSource 的替代方案,Spring 提供了一个 ReloadableResourceBundleMessageSource 类。该变体支持相同的包文件格式,但比基于标准 JDK 的 ResourceBundleMessageSource 实现更灵活。特别是,它允许从任何 Spring 资源位置读取文件(不仅是类路径),并支持包属性文件的热重载(同时在期间高效缓存)。有关详细信息,请参阅 ReloadableResourceBundleMessageSource 的 JavaDoc。
标准和自定义事件
ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果在上下文中部署了一个实现了 ApplicationListener 接口的 Bean,那么每当有 ApplicationEvent 发布到 ApplicationContext 时,该 Bean 都会收到通知。从本质上讲,这是标准的 观察者(Observer) 设计模式。
TIP
从 Spring 4.2 开始,事件基础设施得到了显著改进,提供了 基于注解的模型 以及发布任何任意事件的能力(即不需要继承 ApplicationEvent 的对象)。当这样的对象被发布时,我们会为你将其封装为事件。
下表描述了 Spring 提供的标准事件:
| 事件 | 说明 |
|---|---|
ContextRefreshedEvent | 当 ApplicationContext 被初始化或刷新时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。这里,“初始化”意味着所有 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 处理完毕。此事件在请求完成后发布。 |
ServletRequestHandledEvent | RequestHandledEvent 的子类,增加了特定于 Servlet 的上下文信息。 |
你也可以创建并发布自己的自定义事件。下面的示例展示了一个扩展自 Spring 的 ApplicationEvent 基类的简单类:
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;
}
// 访问器和其他方法...
}class BlockedListEvent(source: Any,
val address: String,
val content: String) : ApplicationEvent(source)要发布自定义 ApplicationEvent,请在 ApplicationEventPublisher 上调用 publishEvent() 方法。通常,这是通过创建一个实现 ApplicationEventPublisherAware 的类并将其注册为 Spring Bean 来实现的。下面的示例展示了这样一个类:
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;
}
// 发送邮件...
}
}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
}
// 发送邮件...
}
}在配置阶段,Spring 容器会检测到 EmailService 实现了 ApplicationEventPublisherAware,并自动调用 setApplicationEventPublisher()。实际上,传入的参数是 Spring 容器本身。你正在通过其 ApplicationEventPublisher 接口与应用上下文进行交互。
要接收自定义 ApplicationEvent,你可以创建一个实现 ApplicationListener 的类并将其注册为 Spring Bean。下面的示例展示了这样一个类:
public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
public void onApplicationEvent(BlockedListEvent event) {
// 通过 notificationAddress 通知相关方...
}
}class BlockedListNotifier : ApplicationListener<BlockedListEvent> {
lateinit var notificationAddress: String
override fun onApplicationEvent(event: BlockedListEvent) {
// 通过 notificationAddress 通知相关方...
}
}请注意,ApplicationListener 使用你的自定义事件类型(在上面的例子中是 BlockedListEvent)进行了泛型化。这意味着 onApplicationEvent() 方法可以保持类型安全,避免任何向下转型的需要。你可以根据需要注册任意数量的事件监听器,但请注意,默认情况下,事件监听器 同步 接收事件。这意味着 publishEvent() 方法会阻塞,直到所有监听器都处理完事件。这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果事务上下文可用,它会在发布者的事务上下文中运行。如果需要另一种事件发布策略,例如默认的异步事件处理,请参阅 Spring 的 ApplicationEventMulticaster 接口和 SimpleApplicationEventMulticaster 实现的 JavaDoc,了解可应用于自定义 "applicationEventMulticaster" Bean 定义的配置选项。在这些情况下,ThreadLocals 和日志上下文不会在事件处理中传播。有关可观测性(Observability)问题的更多信息,请参阅 @EventListener 可观测性部分。
下面的示例展示了用于注册和配置上述每个类的 Bean 定义:
<bean id="emailService" class="example.EmailService">
<property name="blockedList">
<list>
<value>known.spammer@example.org</value>
<value>known.hacker@example.org</value>
<value>john.doe@example.org</value>
</list>
</property>
</bean>
<bean id="blockedListNotifier" class="example.BlockedListNotifier">
<property name="notificationAddress" value="blockedlist@example.org"/>
</bean>
<!-- 可选:自定义 ApplicationEventMulticaster 定义 -->
<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,此时它可以通知相关方。
TIP
Spring 的事件机制是为同一应用上下文中 Spring Bean 之间的简单通信而设计的。然而,对于更复杂的企业集成需求,由社区维护的 Spring Integration 项目提供了完善的支持,可以构建基于成熟 Spring 编程模型的轻量级、面向模式的事件驱动架构。
基于注解的事件监听器
你可以通过使用 @EventListener 注解在托管 Bean 的任何方法上注册事件监听器。BlockedListNotifier 可以重写如下:
public class BlockedListNotifier {
private String notificationAddress;
public void setNotificationAddress(String notificationAddress) {
this.notificationAddress = notificationAddress;
}
@EventListener
public void processBlockedListEvent(BlockedListEvent event) {
// 通过 notificationAddress 通知相关方...
}
}class BlockedListNotifier {
lateinit var notificationAddress: String
@EventListener
fun processBlockedListEvent(event: BlockedListEvent) {
// 通过 notificationAddress 通知相关方...
}
}方法签名再次声明了它所监听的事件类型,但这一次使用了灵活的名称,并且没有实现特定的监听器接口。只要实际的事件类型在其实现层次结构中解析了你的泛型参数,事件类型也可以通过泛型进行缩小。
如果你的方法应该监听多个事件,或者你想定义一个完全没有参数的方法,也可以在注解本身上指定事件类型。下面的示例展示了如何操作:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}@EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class)
fun handleContextStart() {
// ...
}还可以通过使用注解的 condition 属性添加额外的运行时过滤,该属性定义了一个 SpEL 表达式,必须匹配该表达式才能实际为特定事件调用该方法。
下例展示了如何重写我们的通知程序,使其仅在事件的 content 属性等于 my-event 时才被调用:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
// 通过 notificationAddress 通知相关方...
}@EventListener(condition = "#blEvent.content == 'my-event'")
fun processBlockedListEvent(blEvent: BlockedListEvent) {
// 通过 notificationAddress 通知相关方...
}每个 SpEL 表达式都针对专用上下文进行计算。下表列出了该上下文中可用的项,以便你可以将它们用于条件事件处理:
| 名称 | 位置 | 说明 | 示例 |
|---|---|---|---|
| Event | 根对象 | 实际的 ApplicationEvent。 | #root.event 或 event |
| 参数数组 | 根对象 | 用于调用该方法的参数(作为对象数组)。 | #root.args 或 args;args[0] 访问第一个参数等。 |
| 参数名称 | 评估上下文 | 特定方法参数的名称。如果名称不可用(例如,因为代码在没有 -parameters 标志的情况下编译),也可以使用 #a<#arg> 语法访问单个参数,其中 <#arg> 代表参数索引(从 0 开始)。 | #blEvent 或 #a0(你也可以使用 #p0 或 #p<#arg> 参数记法作为别名) |
请注意,#root.event 允许你访问底层事件,即使你的方法签名实际上引用的是已发布的任意对象。
如果你需要发布一个事件作为处理另一个事件的结果,你可以更改方法签名以返回应该发布的事件,如下例所示:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// 通知相关方,然后发布一个 ListUpdateEvent...
}@EventListener
fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent {
// 通知相关方,然后发布一个 ListUpdateEvent...
}TIP
此功能不支持 异步监听器。
handleBlockedListEvent() 方法会为它处理的每个 BlockedListEvent 发布一个新的 ListUpdateEvent。如果你需要发布多个事件,可以改为返回一个 Collection 或事件数组。
异步监听器
如果你希望某个特定的监听器以异步方式处理事件,你可以复用 常规的 @Async 支持。下面的示例展示了如何操作:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent 在单独的线程中处理
}@EventListener
@Async
fun processBlockedListEvent(event: BlockedListEvent) {
// BlockedListEvent 在单独 the 线程中处理
}在使用异步事件时,请注意以下限制:
- 如果异步事件监听器抛出
Exception,它不会传播回调用者。有关更多详细信息,请参阅AsyncUncaughtExceptionHandler。 - 异步事件监听器方法不能通过返回值来发布后续事件。如果你需要发布另一个事件作为处理结果,请注入
ApplicationEventPublisher以手动发布事件。 - 默认情况下,ThreadLocals 和日志上下文不会在事件处理中传播。有关可观测性问题的更多信息,请参阅
@EventListener可观测性部分。
监听器排序
如果你需要一个监听器在另一个监听器之前被调用,你可以在方法声明中添加 @Order 注解,如下例所示:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// 通过 notificationAddress 通知相关方...
}@EventListener
@Order(42)
fun processBlockedListEvent(event: BlockedListEvent) {
// 通过 notificationAddress 通知相关方...
}泛型事件
你也可以使用泛型来进一步定义事件的结构。考虑使用 EntityCreatedEvent<T>,其中 T 是实际创建的实体类型。例如,你可以创建以下监听器定义,以便仅接收针对 Person 的 EntityCreatedEvent:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}@EventListener
fun onPersonCreated(event: EntityCreatedEvent<Person>) {
// ...
}由于类型擦除,这仅在触发的事件解析了事件监听器过滤的泛型参数时才有效(即,类似于 class PersonCreatedEvent extends EntityCreatedEvent<Person> { … } 的定义)。
在某些情况下,如果所有事件都遵循相同的结构,这可能会变得相当繁琐。在这种情况下,你可以实现 ResolvableTypeProvider 以引导框架完成运行时环境所能提供的功能。下例展示了如何操作:
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()))
}
}TIP
这不仅适用于 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 实现特殊的毁调接口 ResourceLoaderAware,以便在初始化时自动回调,并将应用上下文本身作为 ResourceLoader 传入。你还可以公开 Resource 类型属性,用于访问静态资源。它们会像其他任何属性一样被注入。你可以将这些 Resource 属性指定为简单的 String 路径,并在部署 Bean 时依靠从这些文本字符串到实际 Resource 对象的自动转换。
提供给 ApplicationContext 构造函数的路径实际上是资源字符串,并根据具体的上下文实现进行适当处理。例如,ClassPathXmlApplicationContext 将简单的位置路径视为类路径位置。你也可以使用带有特殊前缀的位置路径(资源字符串)强制从类路径或 URL 加载定义,而不受实际上下文类型的影响。
应用程序启动跟踪
ApplicationContext 管理 Spring 应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可能有同样复杂的组件图和启动阶段。
使用特定指标跟踪应用程序启动步骤有助于了解启动阶段的时间消耗分布,也可以作为更好理解整个上下文生命周期的一种方式。
AbstractApplicationContext(及其子类)配备了 ApplicationStartup,它收集有关各个启动阶段的 StartupStep 数据:
- 应用上下文生命周期(基包扫描、配置类管理)
- Bean 生命后期(实例化、智能初始化、后处理)
- 应用程序事件处理
以下是 AnnotationConfigApplicationContext 中度量的示例:
// 创建启动步骤并开始记录
try (StartupStep scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) {
// 向当前步骤添加标记信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行我们要度量的实际阶段
this.scanner.scan(basePackages);
}// 创建启动步骤并开始记录
try (val scanPackages = getApplicationStartup().start("spring.context.base-packages.scan")) {
// 向当前步骤添加标记信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行我们要度量的实际阶段
this.scanner.scan(basePackages);
}应用上下文已经配备了多个步骤。一旦记录下来,这些启动步骤就可以使用特定工具进行收集、显示和分析。有关现有启动步骤的完整列表,你可以查看专用附录章节。
默认的 ApplicationStartup 实现是一个无操作(no-op)变体,以实现最小开销。这意味着默认情况下启动期间不会收集任何指标。Spring Framework 附带了一个使用 Java Flight Recorder 跟踪启动步骤的实现:FlightRecorderApplicationStartup。要使用此变体,必须在 ApplicationContext 创建后尽快为其配置一个实例。
如果开发人员提供自己的 AbstractApplicationContext 子类,或者希望收集更精确的数据,也可以使用 ApplicationStartup 基础设施。
WARNING
ApplicationStartup 仅旨在用于应用程序启动期间和核心容器;这绝不是 Java 剖析器(Profiler)或像 Micrometer 这样的度量库的替代品。
要开始收集自定义 StartupStep,组件可以直接从应用上下文获取 ApplicationStartup 实例,让其组件实现 ApplicationStartupAware,或者在任何注入点请求 ApplicationStartup 类型。
TIP
开发人员在创建自定义启动步骤时不应使用 "spring.*" 命名空间。此命名空间保留给 Spring 内部使用,且可能会发生变化。
Web 应用程序的 ApplicationContext 实例化
你可以显式声明式地创建 ApplicationContext 实例,例如使用 ContextLoader。当然,你也可以使用 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。
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 文件进行简单部署:
- 将所有应用程序类打包到 RAR 文件中(这是一个具有不同文件扩展名的标准 JAR 文件)。
- 将所有必需的库 JAR 添加到 RAR 存档的根目录中。
- 添加
META-INF/ra.xml部署描述符(如SpringContextResourceAdapter的 JavaDoc 所示)以及相应的 Spring XML Bean 定义文件(通常为META-INF/applicationContext.xml)。 - 将生成的 RAR 文件放入应用服务器的部署目录中。
TIP
此类 RAR 部署单元通常是自包含的。它们不向外界公开组件,甚至不向同一应用程序的其他模块公开。与基于 RAR 的 ApplicationContext 的交互通常通过它与其他模块共享的 JMS 目的地发生。基于 RAR 的 ApplicationContext 也可以(例如)调度一些作业或对文件系统中的新文件做出反应(或类似操作)。如果它需要允许来自外部的同步访问,它可以(例如)导出 RMI 端点,这些端点可以由同一台机器上的其他应用程序模块使用。
补充教学
1. MessageSource 的现代实践
在 Spring Boot 时代,我们很少需要像 XML 时代那样手动配置 ResourceBundleMessageSource。
- 自动配置:Spring Boot 默认会自动配置一个
MessageSource。你只需要在src/main/resources下创建messages.properties(以及messages_zh_CN.properties等)即可。 - 配置属性:可以通过
spring.messages.basename配置资源包的基础名称。 - UTF-8 问题:在 Java 9 之前,属性文件默认使用 ISO-8859-1 编码。Spring 的
ResourceBundleMessageSource允许通过setDefaultEncoding("UTF-8")来解决乱码问题。在 Spring Boot 中,默认就是 UTF-8。
2. Spring 事件:解耦的利器
Spring 事件是实现 观察者模式 的极佳方式,它能让你的业务代码保持“干净”:
- 解耦:比如用户注册成功后,需要发送邮件、赠送积分、发送短信。你可以直接在注册方法里调用三个服务,但这会导致代码臃肿且耦合。更好的做法是发布一个
UserRegisteredEvent,让不同的监听器处理后续逻辑。 - 事务绑定 (
@TransactionalEventListener):这是生产环境中最常用的技巧。如果你希望在“注册成功(事务已提交)”后再发送邮件(防止事务回滚导致误发邮件),你应该使用@TransactionalEventListener而不是@EventListener。你可以指定phase = TransactionPhase.AFTER_COMMIT。 - Payload 事件:如果你不想定义专门的事件类,可以使用
PayloadApplicationEvent<T>或直接通过publisher.publishEvent(someObject)发布。Spring 会自动帮你包装。
3. Resource 抽象的常用前缀
Spring 的 Resource 抽象屏蔽了底层存储的差异,最常用的前缀包括:
classpath::从类路径加载。file::从文件系统路径加载(可以使用绝对或相对路径)。http:/https::通过标准协议从网络地址加载。ftp::通过 FTP 加载。- 无前缀:取决于具体的
ApplicationContext类型。比如FileSystemXmlApplicationContext会将其解释为文件路径,而ClassPathXmlApplicationContext会将其解释为类路径。为了代码的可移植性,建议始终显式使用前缀。
4. ApplicationStartup 的实战意义
ApplicationStartup 是 Spring 5.3 引入的功能,主要解决“大中型 Spring 项目启动慢,但不知道慢在哪”的问题。
- 可视化:配合 Spring Boot 的
/actuator/startup端点,你可以获取启动步骤的 JSON 数据。 - 工具链:你可以将数据导入到 Chrome DevTools 的 Performance 面板或者 Java Flight Recorder 中,以图形化的方式看到哪一个 Bean 的初始化耗时最长,或者哪一个
BeanPostProcessor拖慢了进度。 - AOT 准备:在 Spring 6 为 Native Image 做准备时,对启动过程的精细度量变得尤为重要。