AI写作智能体 自主规划任务,支持联网查询和网页读取,多模态高效创作各类分析报告、商业计划、营销方案、教学内容等。 广告
正如本章介绍中所述,`org.springframework.beans.factory`包提供了管理和操作bean的基本功能,包括编程的方式。 `org.springframework.context`包添加了一个ApplicationContext接口,该接口继承自BeanFactory接口以及其他一些接口,以一种更加面向应用程序的形式提供了很多额外的功能。许多开发者以一种完全的声明式的方式使用`ApplicationContext`,甚至不会以编程方式创建它,而是依赖诸如`ContextLoader`之类的支持类来自动实例化一个`ApplicationContext`,作为Java EE Web应用程序正常启动过程的一部分。 为了以一种更加面向应用程序的形式来增强`BeanFactory`的功能,context包还提供以下功能: * 通过`MessageSource`接口以i18n的形式访问消息。 * 通过`ResourceLoader`接口访问资源,如URL和文件。 * 通过使用`ApplicationEventPublisher`接口向实现了`ApplicationListener`接口的bean发布事件。 * 通过`HierarchicalBeanFactory`接口加载多个(具有继承层次结构的)上下文,允许每个上下文只关注某个特定的层。 ### 7.15.1 使用MessageSource进行国际化 `ApplicationContext`接口继承了一个名为`MessageSource`的接口,因此提供了国际化(internationalization,i18n)的功能。 Spring还提供了`HierarchicalMessageSource`接口,可以按照层次结构来解析消息。这些接口一起为Spring的消息解析提供了基础。在这些接口上定义的方法包括: * `String getMessage(String code,Object[] args,String default,Locale loc)`:用于从MessageSource中检索消息的基本方法。当在指定区域(locale)中找不到消息时,将使用默认消息。传入的任何args参数都将成为消息模板中的替换值,使用标准库提供的`MessageFormat`功能。 * `String getMessage(String code,Object[] args,Locale loc)`:与前一个方法基本相同,但有一个区别:没有指定默认消息;如果找不到该消息,则抛出`NoSuchMessageException`。 * String getMessage(MessageSourceResolvable resolvable,Locale locale):上述方法中使用的所有属性也都可以被封装在一个名为`MessageSourceResolvable`的类中,此时您可以使用此方法。 当加载`ApplicationContext`时,它会自动搜索在上下文中定义的`MessageSource` bean。该bean必须具有`messageSource`这个名称。 如果找到这样一个bean,那么对所有上述方法的调用都将被委派给这个消息源(message source)。如果没有找到消息源,`ApplicationContext`将尝试去父容器中查找这个同名的bean。如果找到,则将使用该bean作为`MessageSource`。 如果`ApplicationContext`找不到任何消息源,则将实例化一个空的`DelegatingMessageSource`,以便能够接受对上述方法的调用委派。 Spring提供了两个`MessageSource`实现,`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> ~~~ 在该示例中,假定您在类路径中定义了三个资源束(resource bundle),分别为format、exceptions和windows。解析消息的任何请求将以JDK标准方式通过ResourceBundles来处理。为了示例的目的,假设两个上述资源束文件的内容是... ~~~ # 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", null); System.out.println(message); } ~~~ 上面的程序将输出: Alligators rock! 所以总结一下,`MessageSource`是在一个位于类路径根目录下的名为`beans.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.foo.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", null); System.out.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`的文件。 通常,区域解析是由应用程序的周围环境进行管理的。 在此示例中,针对英国区域的消息解析是手动指定的。 ~~~ # 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`的引用。 > 作为`ResourceBundleMessageSource`的替代方案,Spring提供了一个`ReloadableResourceBundleMessageSource`类。 此类支持相同的资源束文件格式,但比基于标准JDK的`ResourceBundleMessageSource`实现更灵活。 特别是,它允许从任何Spring资源位置(不仅仅是类路径)读取文件,并且支持对bundle属性文件进行热重新加载(同时在它们之间高效缓存)。 查看`ReloadableResourceBundleMessageSource` javadocs以获取更详细的信息。 ### 7.15.2 标准事件和自定义事件 `ApplicationContext`中的事件处理是通过`ApplicationEvent`类和`ApplicationListener`接口提供的。 如果将实现`ApplicationListener`接口的bean部署到上下文中,则每次将`ApplicationEvent`发布到`ApplicationContext`时,都会通知该bean。 本质上,这是标准的观察者设计模式。 > 从Spring 4.2开始,事件基础设施得到了重大改进,并提供了基于注解的模型以及发布任意事件(事件对象不再必须要继承`ApplicationEvent`了)的功能。 当发布这样的任意事件对象时,我们自动将它包装在一个事件中。 spring提供了以下几个标准的事件: |事件 |说明 | | --- | --- | |`ContextRefreshedEvent` |在`ApplicationContext`初始化或刷新时发布,例如,调用`ConfigurableApplicationContext`接口的`refresh()`方法时。 这里的“初始化”意味着所有的bean都已经被加载,后处理器bean已经被检测并激活,单例已经被预先实例化,并且`ApplicationContext`对象已经可以使用。 只要上下文尚未关闭,可以多次触发刷新(refresh),前提是所选的`ApplicationContext`实际上支持这种“热”刷新。 例如,`XmlWebApplicationContext`支持热刷新,但`GenericApplicationContext`不支持。 | |`ContextStartedEvent` |在`ApplicationContext`启动时发布,使用`ConfigurableApplicationContext`接口的start()方法。 这里的“启动”意味着所有`Lifecycle` bean都会收到一个明确的启动信号。 通常,此信号用于在显式停止后重新启动bean,但也可用于启动那些没有被配置为自动启动的组件,例如,在初始化时尚未启动的组件。 | |`ContextStoppedEvent` |在`ApplicationContext`停止时发布,使用`ConfigurableApplicationContext`接口的stop()方法。 这里的“停止”意味着所有`Lifecycle` bean都会收到一个明确的停止信号。 可以通过调用start()重新启动一个已经停止的上下文。 | |`ContextClosedEvent` |在`ApplicationContext`关闭时发布,使用`ConfigurableApplicationContext`接口的close()方法。 这里所说的“关闭”意味着所有的单例bean都会被销毁。 一个关闭的上下文就到达了其生命周期的终点; 它不能再被刷新或重新启动。 | |`RequestHandledEvent` |一个特定于web的事件,告诉所有的bean某个HTTP请求已被服务。 此事件在请求完成后发布。 此事件仅适用于使用Spring的`DispatcherServlet`的Web应用程序。 | 您还可以创建和发布自己的自定义事件。 这个例子演示了一个继承Spring的ApplicationEvent基类的简单类: ~~~ public class BlackListEvent extends ApplicationEvent { private final String address; private final String test; public BlackListEvent(Object source, String address, String test) { super(source); this.address = address; this.test = test; } // 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 text) { if (blackList.contains(address)) { BlackListEvent event = new BlackListEvent(this, address, text); publisher.publishEvent(event); 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()方法将会阻塞。 这种同步的单线程的方式的一个优点是,当监听器接收到一个事件时,它将在发布者(publisher)的事务上下文中运行(如果事务上下文可用的话)。如果需要事件发布的另一个策略,请参阅Spring的`ApplicationEventMulticaster`接口的javadoc。 以下示例显示了用于注册和配置上述每个类的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`,此时它可以对notificationAddress指定的邮件地址进行适当的通知。 > Spring的事件机制是为了在同一个应用上下文中的bean之间进行简单的通信而设计的。 然而,对于更复杂的企业集成需求,单独维护的[Spring Integration](http://projects.spring.io/spring-integration/)项目为构建基于著名的Spring编程模型的轻量级的面向模式的事件驱动架构提供了完整的支持。 #### 基于注解的事件监听器 从Spring 4.2开始,可以通过在受管理的bean的任何`public`方法上标注`EventListener`注解来注册一个事件侦听器。 `BlackListNotifier`可以重写如下: ~~~ public class BlackListNotifier { private String notificationAddress; public void setNotificationAddress(String notificationAddress) { this.notificationAddress = notificationAddress; } @EventListener public void processBlackListEvent(BlackListEvent event) { // notify appropriate parties via notificationAddress... } } ~~~ 如上所述,方法签名再次声明了其侦听的事件类型,但这次使用了一个更灵活的方法名称,而不需要再实现特定的侦听器接口。 事件类型也可以通过泛型来缩小,只要实际的事件类型在其实现层次结构中解析泛型参数即可。 如果您的方法需要监听多个事件,或者如果您想将该方法定义为不带任何参数,那么也可以通过注解本身指定事件类型: ~~~ @EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class}) public void handleContextStart() { ... } ~~~ 还可以通过condition属性来添加额外的运行时过滤功能,该属性接收一个SpEL表达式,用来指定一个是否要为该事件调用这个方法的条件。 例如,如果事件的test属性的值为foo时才调用我们的通知程序,则其可以被重写为: ~~~ @EventListener(condition = "#blEvent.test == 'foo'") public void processBlackListEvent(BlackListEvent blEvent) { // notify appropriate parties via notificationAddress... } ~~~ 每个SpEL表达式都在一个专用上下文中进行解析。 下表列出了上下文中可用的项目,因此可以将它们用于条件事件处理: |名称 |位置 |说明 |示例 | | --- | --- | --- | --- | |事件 |root对象 |实际的ApplicationEvent |#root.event | |参数数组 |root对象 |用于调用目标方法的参数(数组) |#root.args[0] | |参数名称 |解析上下文 |任一一个方法参数的名称。如果因为某些原因名称不可用(如,未启用调试信息),参数名称还可以通过#a<#arg>访问,其中#arg表示参数的索引(从0开始) |#blEvent或者#a0(也可以使用#p0或者#p<#arg>这个别名) | 请注意,#root.event允许您访问底层事件,即使您的方法签名实际上指定的是任意对象作为事件对象。 如果您需要将另一个事件的处理结果发布为一个事件,只需更改方法签名,返回应发布的事件即可,如下所示: ~~~ @EventListener public ListUpdateEvent handleBlackListEvent(BlackListEvent event) { // notify appropriate parties via notificationAddress and // then publish a ListUpdateEvent... } ~~~ > 这个功能不适用于[异步监听器](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#context-functionality-events-async)。 这个新方法将为上述方法handleBlackListEvent()处理的每个BlackListEvent发布一个新的ListUpdateEvent。 如果需要发布多个事件,只需返回一个事件集合即可。 #### 异步监听器 如果你想让某个监听器以异步的方式处理事件,只需使用[常规的@Async注解](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#scheduling-annotation-support-async): ~~~ @EventListener @Async public void processBlackListEvent(BlackListEvent event) { // BlackListEvent is processed in a separate thread } ~~~ 注意异步监听器的如下限制: 1. 如果事件侦听器抛出异常,则不会将其传播给调用者,请查看`AsyncUncaughtExceptionHandler`以获取更多详细信息。 2. 此类事件侦听器无法发送响应。 如果您需要其处理结果发布为另一个事件,请注入`ApplicationEventPublisher`以手动发送事件。 #### 对监听器进行排序 如果你想让某个监听器在另一个监听器之前被调用,只需在其方法声明上标注@Order注解: ~~~ @EventListener @Order(42) public void processBlackListEvent(BlackListEvent 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,还适用于作为事件发送的任意对象。 ### 7.15.3 方便地访问低级资源 为了最佳地使用和理解应用程序上下文,用户应该熟悉Spring的`Resource`抽象,如[第8章资源](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#resources)中所述。 任何一个`ApplicationContext`都是一个`ResourceLoader`,都可用于加载`Resource`。`Resource`本质上是JDK中的`java.net.URL`的功能更丰富的版本,实际上,`Resource`的实现在适当的时候就包装了`java.net.URL`的一个实例。资源可以以一种透明的方式从几乎任何位置获得低级资源,包括从类路径、文件系统位置、任何可用标准URL描述的地方以及其他的一些变体。如果资源位置字符串是没有任何特殊前缀的简单路径,那么这些资源的来源是特定的并且适合于实际的应用程序上下文类型。 您可以将一个实现ResourceLoaderAware接口的bean部署到应用程序上下文中,以便在初始化时自动调用该接口对应的回调方法,传入该应用程序上下文本身作为ResourceLoader。您还可以公开`Resource`类型的属性,以用于访问静态资源;它们将像任何其他属性一样被注入。您可以将这些`Resource`属性指定为简单的字符串路径,并依赖上下文中自动注册的一个特殊的JavaBean `PropertyEditor`,从而将这些文本字符串转换为实际的`Resource`对象。 提供给`ApplicationContext`构造方法的位置路径实际上就是资源字符串,并且根据特定的上下文实现以简单的形式对该字符串进行了适当的处理。 `ClassPathXmlApplicationContext`将简单的位置路径视为类路径位置。您还可以使用具有特殊前缀的位置路径(资源字符串)强制从类路径或URL加载定义,而不管实际的上下文类型是什么。 ### 7.15.4 为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`作为默认值。 当参数确实存在时,侦听器将使用预定义的分隔符(逗号,分号和空格)来分隔该字符串参数,并将这些值作为将被搜索的应用程序上下文的位置。 也支持Ant风格的路径模式。 比如,/WEB-INF/*Context.xml表示所有位于/WEB-INF/下的名称以Context.xml结尾的文件,/WEB-INF/**/*Context.xml则表示在WEB-INF的任何子目录中的名称以Context.xml结尾的文件。 ### 7.15.5 将Spring ApplicationContext部署为Java EE RAR文件 TODO