1.15。ApplicationContext的附加功能
正如在引言中所讨论的,org.springframework.beans.factory 包提供了管理和操作bean的基本功能,包括以编程的方式。org.springframework.context 包添加了ApplicationContext接口,它扩展了BeanFactory接口,此外还扩展了其他接口,以更面向应用程序框架的风格提供额外的功能。许多人以一种完全声明式的方式使用ApplicationContext,甚至不是通过编程来创建它,而是依赖于支持类(如ContextLoader)来自动实例化一个ApplicationContext,作为Java EE web应用程序的正常启动过程的一部分。
为了以更面向框架的风格增强BeanFactory的功能,上下文包还提供了以下功能:
- 通过MessageSource接口访问i18n风格的消息。(i18n即 internationalization 国际化 类似的还有 L10n localization 本地化)
- 通过ResourceLoader接口访问资源,例如url和文件。
- 事件发布,即通过使用ApplicationEventPublisher接口发布到实现ApplicationListener接口的bean。
- 通过HierarchicalBeanFactory接口加载多个(分层的)上下文,让每个上下文都关注于一个特定的层,比如应用程序的web层。
1.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作为消息源。如果ApplicationContext找不到任何消息源,则实例化一个空的DelegatingMessageSource,以便能够接受对上面定义的方法的调用。
Spring提供了两个消息源实现,ResourceBundleMessageSource和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、exception和windows。解析消息的任何请求都以通过ResourceBundle对象解析消息的jdk标准方法处理。对于示例,假设上述两个资源包文件的内容如下:
# in format.propertiesmessage=Alligators rock!
# in exceptions.propertiesargument.required=The {0} argument is required.
下一个示例显示了执行MessageSource功能的程序。记住,所有ApplicationContext实现也是MessageSource实现,因此可以cast到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是在一个名为bean.xml的文件中定义的。它存在于类路径的根。messageSource bean定义通过其basenames属性引用许多资源包。在列表中传递给basenames属性的三个文件作为类路径的根文件存在,分别称为format.properties, exceptions.properties, 和 windows.properties。
下一个示例显示了传递给消息查找的参数。这些参数被转换为字符串对象并插入到查找消息中的占位符中。
<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>
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接口来获取对已定义的任何消息源的引用。当创建和配置bean时,在实现MessageSourceAware接口的ApplicationContext中定义的任何bean都被注入应用上下文的MessageSourceAware接口。
作为ResourceBundleMessageSource的替代方案,Spring提供了一个ReloadableResourceBundleMessageSource类。这个变体支持相同的bundle文件格式,但是比基于JDK的标准ResourceBundleMessageSource实现更加灵活。特别是,它允许从任何Spring资源位置读取文件(不仅仅是从类路径),并支持bundle属性文件的热重新加载(同时有效地缓存它们)。有关详细信息,请参见ReloadableResourceBundleMessageSource 。
1.15.2。标准和自定义事件
ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。如果实现ApplicationListener接口的bean被部署到上下文中,那么每当一个ApplicationEvent被发布到ApplicationContext时,该bean就会得到通知。本质上,这就是标准的观察者设计模式。
从Spring 4.2开始,事件基础设施已经得到了显著改进,并提供了一个基于注释的模型,以及发布任意事件的能力(也就是说,不需要从ApplicationEvent扩展的对象)。当发布这样的对象时,我们为您将其包装在事件中。
下表描述了Spring提供的标准事件:
事件 | 说明 |
---|---|
ContextRefreshedEvent | 当初始化或刷新ApplicationContext时发布(例如,通过使用ConfigurableApplicationContext接口上的refresh()方法)。在这里,“initialized”意味着加载所有bean,检测并激活后处理器bean,预实例化单例,并且ApplicationContext对象已经准备好可以使用了。只要上下文没有被关闭,刷新就可以被触发多次,前提是所选的ApplicationContext实际上支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。 |
ContextStartedEvent | 通过使用ConfigurableApplicationContext接口上的start()方法启动ApplicationContext时发布。这里,“started”意味着所有生命周期bean都接收一个显式的启动信号。通常,这个信号用于在显式停止后重新启动bean,但也可以用于启动尚未配置为自动启动的组件(例如,尚未在初始化时启动的组件)。 |
ContextStoppedEvent | 在使用ConfigurableApplicationContext接口上的stop()方法停止ApplicationContext时发布。这里,“stopped”意味着所有生命周期bean都接收一个显式的停止信号。可以通过调用start()重新启动已停止的上下文。 |
ContextClosedEvent | 通过使用ConfigurableApplicationContext接口上的close()方法或通过JVM关机钩子关闭ApplicationContext时发布。在这里,“关闭”意味着所有单例bean将被销毁。一旦上下文关闭,它就会到达生命的终点,无法刷新或重新启动。 |
RequestHandledEvent | 一个特定于web的事件,告诉所有bean一个HTTP请求已经得到服务。此事件在请求完成后发布。此事件仅适用于使用Spring的DispatcherServlet的web应用程序。 |
ServletRequestHandledEvent | RequestHandledEvent的一个子类,用于添加特定于servlet的上下文信息。 |
您还可以创建和发布自己的自定义事件。下面的示例展示了一个简单的类,它扩展了Spring的ApplicationEvent基类:
public class BlackListEvent extends ApplicationEvent {private final String address;private final String content;public BlackListEvent(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> blackList;private ApplicationEventPublisher publisher;public void setBlackList(List<String> blackList) {this.blackList = blackList;}public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {this.publisher = publisher;}public void sendEmail(String address, String content) {if (blackList.contains(address)) {publisher.publishEvent(new BlackListEvent(this, address, content));return;}// send email...}
}
在配置时,Spring容器检测到EmailService实现了ApplicationEventPublisherAware,并自动调用setApplicationEventPublisher()。实际上,传入的参数就是Spring容器本身。您通过应用程序的ApplicationEventPublisher接口与应用程序上下文交互。
要接收自定义ApplicationEvent,您可以创建一个实现ApplicationListener的类,并将其注册为一个Spring bean。下面的例子展示了这样一个类:
public class BlackListNotifier implements ApplicationListener<BlackListEvent> {private String notificationAddress;public void setNotificationAddress(String notificationAddress) {this.notificationAddress = notificationAddress;}public void onApplicationEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress...}
}
注意,ApplicationListener通常是用自定义事件的类型(前面示例中的BlackListEvent)参数化的。这意味着onApplicationEvent()方法可以保持类型安全,避免向下强制转换。您可以注册任意数量的事件监听器,但是请注意,默认情况下,事件监听器同步接收事件。这意味着publishEvent()方法会阻塞,直到所有监听器都完成了事件的处理。这种同步和单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布程序的事务上下文内操作。如果需要另一种事件发布策略,请参阅javadoc获取Spring的ApplicationEventMulticaster接口和SimpleApplicationEventMulticaster实现以获得配置选项。
下面的示例显示了用于注册和配置上面每个类的bean定义:
<bean id="emailService" class="example.EmailService"><property name="blackList"><list><value>known.spammer@example.org</value><value>known.hacker@example.org</value><value>john.doe@example.org</value></list></property>
</bean><bean id="blackListNotifier" class="example.BlackListNotifier"><property name="notificationAddress" value="blacklist@example.org"/>
</bean>
将它们放在一起,当调用emailService bean的sendEmail()方法时,如果有任何应该被列入黑名单的电子邮件消息,则发布一个BlackListEvent类型的自定义事件。blackListNotifier bean注册为一个ApplicationListener并接收BlackListEvent,此时它可以通知相关方。
Spring的事件机制是为相同应用程序上下文中的Spring bean之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的Spring integration项目提供了构建轻量级、面向模式、事件驱动架构的完整支持,这些架构构建在众所周知的Spring编程模型之上。
基于注解的事件监听器
从Spring 4.2开始,您可以使用@EventListener注释在托管bean的任何公共方法上注册事件监听器。BlackListNotifier可以重写如下:
public class BlackListNotifier {private String notificationAddress;public void setNotificationAddress(String notificationAddress) {this.notificationAddress = notificationAddress;}@EventListenerpublic void processBlackListEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress...}
}
方法签名再次声明它侦听的事件类型,但是这次使用了灵活的名称,并且没有实现特定的侦听器接口。只要实际事件类型在其实现层次结构中解析泛型参数,就可以通过泛型缩小事件类型。
如果您的方法应该侦听多个事件,或者您希望在不使用任何参数的情况下定义它,那么还可以在注释本身上指定事件类型。下面的例子展示了如何做到这一点:
@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {// ...
}
还可以通过使用定义SpEL表达式的注释的条件属性来添加额外的运行时过滤,该注释应该与针对特定事件实际调用方法相匹配。
下面的例子展示了如何重写通知程序,使其仅在事件的内容属性等于my-event时调用:
@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {// notify appropriate parties via notificationAddress...
}
每个SpEL表达式根据一个专用上下文计算。下表列出了上下文可用的项目,以便您可以使用它们进行条件事件处理:
请注意,#root.event允许您访问底层事件,即使您的方法签名实际上引用了已发布的任意对象。
如果你需要发布一个事件作为处理另一个事件的结果,你可以改变方法签名来返回应该发布的事件,如下面的例子所示:
@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress and// then publish a ListUpdateEvent...
}
异步监听器不支持此特性。asynchronous listeners
这个新方法为上面方法处理的每个BlackListEvent发布一个新的ListUpdateEvent。如果需要发布多个事件,则可以返回一个事件集合。
异步监听器
如果需要一个特定的侦听器异步处理事件,可以重用常规的@Async支持。下面的例子展示了如何做到这一点:
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {// BlackListEvent is processed in a separate thread
}
在使用异步事件时要注意以下限制:
- 如果异步事件侦听器抛出异常,它不会传播到调用者。有关更多细节,请参阅AsyncUncaughtExceptionHandler。
- 异步事件侦听器方法不能通过返回值来发布后续事件。如果您需要发布另一个事件作为处理的结果,注入ApplicationEventPublisher来手动发布事件。
监听器排序
如果需要先调用一个监听器,可以在方法声明中添加@Order注释,如下例所示:
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {// notify appropriate parties via notificationAddress...
}
通用的事件
还可以使用泛型进一步定义事件的结构。考虑使用EntityCreatedEvent,其中T是所创建的实际实体的类型。例如,您可以创建以下侦听器定义来只为一个人接收EntityCreatedEvent:
@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {// ...
}
由于类型擦除,只有在触发的事件解析了事件侦听器筛选的泛型参数(即,类似类PersonCreatedEvent扩展了EntityCreatedEvent{…})时,这种方法才有效。
在某些情况下,如果所有事件都遵循相同的结构(前面示例中的事件也应该如此),那么这可能会变得非常乏味。在这种情况下,您可以实现ResolvableTypeProvider来指导运行时环境所提供的框架。下面的事件展示了如何做到这一点:
public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {public EntityCreatedEvent(T entity) {super(entity);}@Overridepublic ResolvableType getResolvableType() {return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));}
}
这不仅适用于ApplicationEvent,也适用于任何作为事件发送的对象。
1.15.3。方便地访问底层资源。
为了优化使用和理解应用程序上下文,您应该熟悉Spring的资源抽象,如参考资料中所述。
应用程序上下文是一个ResourceLoader,可用于加载资源对象。资源本质上是JDK java.net.URL类的功能丰富版本。事实上,资源的实现在适当的时候包装了一个java.net.URL的实例。资源可以以透明的方式从几乎任何位置获取底层资源,包括类路径、文件系统位置、可用标准URL描述的任何位置,以及其他一些变体。如果资源位置字符串是没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。
您可以配置部署到应用程序上下文中的bean,以实现特殊的回调接口ResourceLoaderAware,在初始化时自动回调,而应用程序上下文本身作为ResourceLoader传入。您还可以公开Resource类型的属性,用于访问静态资源。它们像其他属性一样被注入。您可以将这些资源属性指定为简单的字符串路径,并在部署bean时依赖于从这些文本字符串到实际资源对象的自动转换。
提供给ApplicationContext构造函数的位置路径或路径实际上是资源字符串,并且以简单的形式,根据特定的上下文实现进行适当的处理。例如,ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不管实际上下文类型是什么。
1.15.4。方便的ApplicationContext实例化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
作为默认值。当参数确实存在时,侦听器将使用预定义的分隔符(逗号、分号和空格)分隔字符串,并将这些值用作搜索应用程序上下文的位置。也支持ant样式的路径模式。例如/WEB-INF/*Context.xml
(用于所有名称以Context.xml结尾并位于WEB-INF目录中的文件)和/WEB-INF/**/*Context.xml
(用于WEB-INF的任何子目录中的所有此类文件)。
1.15.5。将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数据源实例和JMS ConnectionFactory实例,还可以注册平台的JMX服务器——所有这些都通过Spring的标准事务管理和JNDI和JMX支持工具完成。应用程序组件还可以通过Spring的TaskExecutor抽象与应用服务器的JCA WorkManager交互。
有关RAR部署中涉及的配置细节,请参阅SpringContextResourceAdapter类的javadoc
对于一个简单的部署Spring ApplicationContext作为Java EE RAR文件:
- 所有应用程序的类打包成一个RAR文件(这是一个标准的JAR文件与不同的文件扩展名)。阀门所有必需的库JAR
RAR存档的根源。阀门一个META-INF/ra.xml
部署描述符(SpringContextResourceAdapter
javadoc所示)和相应的Spring XML bean定义文件(s)(通常是meta - inf /中)。 - 将生成的RAR文件放到应用程序服务器的部署目录中。
这样的RAR部署单元通常是自包含的。它们不向外部世界公开组件,甚至不向同一应用程序的其他模块公开组件。与基于rpc的ApplicationContext的交互通常是通过JMS目的地进行的,JMS目的地与其他模块共享。例如,基于rar的ApplicationContext还可以调度一些作业或对文件系统中的新文件(或类似的东西)作出反应。如果它需要允许从外部进行同步访问,那么它可以(例如)导出RMI端点,这可能被同一机器上的其他应用程序模块使用。