15、IOC 之ApplicationContext 的附加功能
15、IOC 之 ApplicationContext
的附加功能
正如引言中所讨论的,org.springframework.beans.factory
包提供了管理和操作Bean的基本功能,包括以编程的方式。org.springframework.context
包添加了ApplicationContext
接口,该接口扩展了 BeanFactory
接口,此外还扩展了其他接口,以更面向应用程序框架的风格提供额外的功能。许多人以完全声明式的方式使用 ApplicationContext
,甚至不是通过编程方式创建它,而是依赖于像 ContextLoader
这样的支持类来自动实例化一个 ApplicationContext
,作为Java EE web应用程序正常启动过程的一部分。
为了以更面向框架的方式增强 BeanFactory
的功能,context包还提供了以下功能:
- 通过
MessageSource
接口访问 i18n 风格的消息。 - 通过
ResourceLoader
接口访问资源,如 URL 和文件。 - 事件发布,即通过使用
ApplicationListener
接口向实现ApplicationEventPublisher
接口的 Bean发布。 - 加载多个(分层)上下文,让每个上下文都通过
HierarchicalBeanFactory
接口聚焦在一个特定层上,例如应用程序的 Web 层。
15.1、国际化使用 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
的资源包。任何解析消息的请求都以通过 ResourceBundle
对象解析消息的jdk标准方式处理。在本例中,假设上述两个资源包文件的内容如下:
# in format.properties
message=Alligators rock!
# in 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);
}
上述程序的结果输出如下:
Alligators rock!
总之,MessageSource
定义在一个名为 beans.xml
的文件中,该文件存在于类路径的根目录中。messageSource
Bean定义通过其 basenames
属性引用许多资源包。在列表中传递给 basenames
属性的三个文件作为文件存在于类路径的根目录中,它们被称为 format.properties
、exceptions.properties
和 windows.properties
。
下一个示例显示传递给消息查找的参数。这些参数被转换为 String
对象,并插入到查找消息中的占位符中。
<beans>
<!-- 此 MessagesSource 正在web应用程序中使用 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="exceptions"/>
</bean>
<!-- 让我们将上述消息源注入到这个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);
}
}
调用 execute()
方法的结果输出如下:
The userDao argument is required.
关于国际化(“i18n”),Spring的各种 MessageSource
实现遵循与标准JDK ResourceBundle
相同的地区解析和回退规则。简而言之,继续前面定义的 messageSource
示例,如果你希望针对英国(en-GB
)语言环境解析消息,你将创建名为 format_en_GB.properties
、exceptions_en_GB.properties
和 windows_en_GB.properties
文件。
通常,区域设置解析是由应用程序的周围环境管理的。在下面的例子中,手动指定了(英国)消息解析的语言环境:
# in 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);
}
运行上述程序的结果输出如下:
Ebagum lad, the 'userDao' argument is required, I say, required.
你还可以使用 MessageSourceAware
接口来获取对已定义的任何 MessageSource
的引用。当创建和配置Bean时,在实现 MessageSourceAware
接口的 ApplicationContext
中定义的任何Bean都将被注入应用程序上下文的 MessageSource
。
因为 Spring的
MessageSource
基于 Java 的ResourceBundle
,所以它不会合并具有相同基本名称的包,而只会使用找到的第一个包。具有相同基名称的后续消息包将被忽略。
作为
ResourceBundleMessageSource
的替代方案,Spring提供了一个ReloadableResourceBundleMessageSource
类。该变体支持相同的包文件格式,但比基于标准 JDK的ResourceBundleMessageSource
实现更灵活。特别是,它允许从任何 Spring资源位置读取文件 (不仅仅是从类路径 ),并支持 bundle属性文件的热加载 ( 同时高效地缓存它们 )。详见ReloadableResourceBundleMessageSource
Java 文档。
15.2、标准和自定义事件
ApplicationContext
中的事件处理是通过 ApplicationEvent
类和 ApplicationListener
接口提供的。如果将实现 ApplicationListener
接口的Bean部署到上下文中,那么每当 ApplicationEvent
发布到 ApplicationContext
时,该Bean就会收到通知。本质上,这是标准的观察者设计模式。
到 Spring 4.2为止,事件基础结构已经得到了显著的改进,并提供了一个基于注释的模型,以及发布任意事件的能力 ( 也就是说,一个对象不一定是从
ApplicationEvent
扩展而来 )。当这样的对象被发布时,我们将它包装在一个事件中。
下表描述了 Spring 提供的标准事件:
Event | Explanation |
---|---|
ContextRefreshedEvent | 当 ApplicationContext 被初始化或刷新时发布 ( 例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法 )。在这里,“初始化”意味着加载了所有Bean,检测并激活了后处理器Bean,预实例化了单例,ApplicationContext 对象已经准备好可以使用。只要上下文没有关闭,就可以多次触发刷新,前提是所选的 ApplicationContext 实际上支持这种“热”刷新。例如, XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。 |
ContextStartedEvent | 通过使用 ConfigurableApplicationContext 接口上的 start() 方法,在 ApplicationContext 启动时发布。在这里,“started”意味着所有Lifecycle Bean都收到一个显式的开始信号。通常,此信号用于在显式停止后重新启动Bean,但也可以用于启动未配置为自动启动的组件(例如,在初始化时尚未启动的组件)。 |
ContextStoppedEvent | 通过使用 ConfigurableApplicationContext 接口上的 stop() 方法,在 ApplicationContext 停止时发布。在这里,“stopped”意味着所有Lifecycle Bean接收一个显式的停止信号。停止的上下文可以通过 start() 调用重新启动。 |
ContextClosedEvent | 通过使用ConfigurableApplicationContext 接口上的close() 方法或通过JVM关闭挂钩,在ApplicationContext 关闭时发布。在这里,“关闭”意味着所有的单例Bean将被销毁。一旦上下文被关闭,它将到达生命的尽头,并且不能被刷新或重新启动。 |
RequestHandledEvent | 一个特定于web的事件,告诉所有Bean HTTP请求已得到服务。此事件在请求完成后发布。此事件仅适用于使用Spring的DispatcherServlet 的web应用程序。 |
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;
}
// accessor and other methods...
}
要发布一个自定义的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;
}
// send email...
}
}
在配置时,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) {
// notify appropriate parties via notificationAddress...
}
}
请注意,ApplicationListener
通常是用自定义事件的类型参数化的 ( 在前面的例子中是BlockedListEvent
)。这意味着onApplicationEvent()
方法可以保持类型安全,避免任何向下转换的需要。你可以注册任意数量的事件监听器,但是请注意,在默认情况下,事件监听器是同步接收事件的。这意味着publishEvent()
方法会阻塞,直到所有侦听器都完成了对事件的处理。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文内操作。如果需要另一种事件发布策略,请参阅javadoc获取Spring的 ApplicationEventMulticaster
接口和SimpleApplicationEventMulticaster
实现的配置选项。
下面的示例显示了用于注册和配置上面每个类的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>
综合起来,当调用emailService
Bean的sendEmail()
方法时,如果有任何应该被阻止的电子邮件消息,就会发布一个BlockedListEvent
类型的自定义事件。blockedListNotifier
Bean被注册为ApplicationListener
并接收BlockedListEvent
,此时它可以通知适当的方。
基于注释的事件侦听器
你可以使用@EventListener
注释在托管Bean的任何方法上注册事件监听器。BlockedListNotifier
可以重写如下:
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...
}
}
方法签名再次声明它侦听的事件类型,但这次使用灵活的名称,而不实现特定的侦听器接口。还可以通过泛型缩小事件类型的范围,只要实际的事件类型在其实现层次结构中解析泛型参数即可。
如果你的方法应侦听多个事件,或者要定义它时根本不使用参数,则还可以在注释本身上指定事件类型。下面的示例演示如何执行此操作:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
还可以通过使用定义SpEL表达式的注释的 condition
属性来添加额外的运行时筛选,该注释应该与针对特定事件的方法的实际调用相匹配。
下面的例子展示了如何重写我们的通知符,使其仅在事件的content
属性等于my-event
时才被调用:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
// notify appropriate parties via notificationAddress...
}
每个SpEL
表达式在专用上下文中计算。下表列出了上下文可用的项目,以便你可以将它们用于条件事件处理:
Name | Location | Description | Example |
---|---|---|---|
Event | root object | 实际的 ApplicationEvent . | #root.event 或 event |
Arguments array | root object | 用于调用方法的参数(作为对象数组) | #root.args 或 args ; args[0] 来访问第一个参数,等等。 |
Argument name | evaluation context | 任何方法参数的名称。如果由于某种原因,名称不可用 ( 例如,因为在编译的字节码中没有调试信息 ),单个参数也可以使用#a<#arg> 语法,其中 <#arg> 表示参数索引(从0开始) | #blEvent 或 #a0 (你也可以使用 #p0 或 #p<#arg> 参数表示法作为别名) |
请注意 #root.event
使你能够访问底层事件,即使你的方法签名实际上引用了已发布的任意对象。
如果你需要发布一个事件作为处理另一个事件的结果,你可以改变方法签名来返回应该发布的事件,如下所示:
@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress and
// then publish a ListUpdateEvent...
}
异步监听器不支持此特性。
handleBlockedListEvent()
方法为它处理的每个BlockedListEvent
发布一个新的ListUpdateEvent
。如果需要发布多个事件,可以返回一个Collection
或事件数组。
异步侦听器
如果希望特定侦听器异步处理事件,则可以重用常规@Async
支持。下面的示例演示如何执行此操作:
@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
// BlockedListEvent is processed in a separate thread
}
使用异步事件时,请注意以下限制:
- 如果异步事件侦听器抛出
Exception
,它不会传播给调用方。有关更多详细信息AsyncUncaughtExceptionHandler
。 - 异步事件侦听器方法无法通过返回值来发布后续事件。如果需要发布另一个事件作为处理的结果,请注入
ApplicationEventPublisher
以手动发布事件。
对侦听器进行排序
如果需要先调用一个侦听器,然后再调用另一个侦听器,则可以将@Order
注释添加到方法声明中,如下面的示例所示:
@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
// notify appropriate parties via notificationAddress...
}
普通事件
你还可以使用泛型来进一步定义事件的结构。考虑使用EntityCreatedEvent<T>
,其中T
是被创建的实际实体的类型。例如,你可以创建以下侦听器定义来仅接收Person
的EntityCreatedEvent
:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
// ...
}
由于类型擦除,只有当触发的事件解析事件监听器筛选的泛型参数(即类似于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()));
}
}
这不仅适用于
ApplicationEvent
,而且适用于任何作为事件发送的对象。
15.3、方便地访问低级资源
为了最优地使用和理解应用程序上下文,你应该熟悉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加载定义,而不管实际的上下文类型是什么。
15.4、应用程序启动跟踪
ApplicationContext
管理Spring应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可以具有同样复杂的组件图和启动阶段。
使用特定指标跟踪应用程序启动步骤有助于了解启动阶段所花费的时间,但也可用于更好地了解整个上下文生命周期。
AbstractApplicationContext
(及其子类)是用ApplicationStartup
来检测的,它收集关于不同启动阶段的StartupStep
数据:
- 应用程序上下文生命周期(基本包扫描、配置类管理)
- Bean生命周期(实例化、智能初始化、后处理)
- 应用程序事件处理
下面是AnnotationConfigApplicationContext
中的检测示例:
// 创建启动步骤并开始录制
StartupStep scanPackages =
this.getApplicationStartup().start("spring.context.base-packages.scan");
// 向当前步骤添加标记信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行我们正在检测的实际阶段
this.scanner.scan(basePackages);
// 结束当前步骤
scanPackages.end();
应用程序上下文已经通过多个步骤进行了插装。一旦记录下来,就可以用特定的工具收集、显示和分析这些启动步骤。要获得现有启动步骤的完整列表,请查看专用附录部分。
为了最小化开销,ApplicationStartup
的默认实现是一个无操作变体。这意味着在默认情况下,应用程序启动期间不会收集任何指标。Spring框架附带了一个Java飞行记录器跟踪启动步骤的实现:FlightRecorderApplicationStartup
。要使用此变体,你必须在ApplicationContext
创建后立即将其实例配置到ApplicationContext
。
如果开发人员提供了自己的AbstractApplicationContext
子类,或者希望收集更精确的数据,他们也可以使用ApplicationStartup
基础结构。
ApplicationStartup
只用于应用程序启动期间和核心容器;这绝不是 Java分析器或像 Micrometer这样的指标库的替代品。
要开始收集自定义StartupStep
,组件可以直接从应用程序上下文获取ApplicationStartup
实例,让它们的组件实现ApplicationStartupAware
,或者在任何注入点请求ApplicationStartup
类型。
开发人员在创建自定义启动步骤时不应使用
"spring.*"
命名空间。此命名空间保留给内部Spring使用,可能会更改。
15.5、方便的应用程序上下文实例化Web应用程序
你可以通过使用,例如,ContextLoader
来声明性地创建ApplicationContext
实例。当然,你也可以通过使用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
,并使用这些值作为搜索应用程序上下文的位置。也支持反样式的路径模式。例如/WEB-INF/*Context.xml
(用于所有名称以Context.xml
结尾且位于WEB-INF
目录下的文件)和/WEB-INF/**/*Context.xml
(用于WEB-INF
任意子目录下的所有此类文件)。
15.6、将Spring ApplicationContext
部署为 Java EE RAR文件
可以将Spring ApplicationContext
部署为RAR文件,将上下文及其所需的所有bean类和库jar封装在Java EE RAR部署单元中。这相当于启动一个独立的ApplicationContext
(仅托管在Java EE环境中),使其能够访问Java EE服务器设施。RAR部署是部署无头WAR文件的一种更自然的替代方案——实际上,没有任何HTTP入口点的WAR文件仅用于在Java EE环境中引导Spring ApplicationContext
。
RAR部署对于不需要HTTP入口点,而只包含消息端点和计划作业的应用程序上下文非常理想。这种上下文中的Bean可以使用应用服务器资源,比如JTA事务管理器、JNDI绑定的 JDBC DataSource
实例和 JMS ConnectionFactory
实例,还可以注册到平台的JMX服务器——所有这些都是通过Spring的标准事务管理以及JNDI和JMX支持工具实现的。应用程序组件还可以通过Spring的TaskExecutor
抽象与应用服务器的JCA WorkManager
交互。
有关RAR部署中涉及的配置细节,请参阅 SpringContextResourceAdapter
类的 Java文档。
对于一个简单的Spring ApplicationContext作为Java EE RAR文件的部署:
- 将所有应用程序类打包到一个RAR文件中 ( 这是一个具有不同文件扩展名的标准JAR文件 )。
- 将所有必需的库 jar 添加到RAR存档的根目录中。
- 添加一个
META-INF/ra.xml
部署描述符(如SpringContextResourceAdapter
的 Java文档所示)和相应的Spring XML Bean定义文件 ( 通常是META-INF/applicationContext.xml
)。 - 将生成的RAR文件放入应用程序服务器的部署目录中。
这种 RAR部署单元通常是自给自足的。它们不向外界公开组件,甚至不向同一应用程序的其他模块公开组件。与基于 RAR 的
ApplicationContext
的交互通常通过与其他模块共享的 JMS目的地发生。例如,基于 RAR 的ApplicationContext
还可以调度一些作业或响应文件系统中的新文件(或类似的)。如果它需要允许来自外部的同步访问,它可以(例如)导出 RMI端点,这可能由同一机器上的其他应用程序模块使用。