Spring | 3.15 ApplicationContext的附加功能

3.15 ApplicationContext的附加功能

正如在介绍一章中所讨论的,org.springframework.beans.factory包提供了管理和操作bean的基本功能,包括编程方式。org.springframework.context包添加了ApplicationContext接口,它扩展了BeanFactory接口,此外还扩展了其他接口,以以更面向应用程序框架的风格提供附加功能。许多人以完全声明式的方式使用ApplicationContext,甚至不以编程方式创建它,而是依赖于支持类(如ContextLoader)自动实例化ApplicationContext,将其作为Java EE web应用程序正常启动过程的一部分。
为了以更面向框架的方式增强BeanFactory功能,上下文包还提供了以下功能:

  • 通过MessageSource接口访问i18n样式的消息。
  • 通过ResourceLoader接口访问资源,例如url和文件。
  • 事件发布,即通过使用ApplicationEventPublisher接口实现ApplicationListener接口的bean。
  • 加载多个(层次结构)上下文,允许每个上下文通过HierarchicalBeanFactory接口集中于一个特定的层,例如应用程序的web层。
3.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。如果是,则使用该bean作为消息源。如果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>

在本例中,假设你的类路径中定义了三个资源包,分别是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相同的语言环境解析和回退规则。简而言之,继续前面定义的示例messageSource,如果你想解析针对英国(en-GB)地区的消息,你将分别创建名为format_en_GB.properties,exceptions_en_GB.properties和windows_en_GBproperties。
通常,区域设置解析由应用程序的周围环境管理。在本例中,将手动指定解析(英国)消息的区域设置。

# 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注入。

注意:作为ResourceBundleMessageSource的替代品,Spring提供了ReloadableResourceBundleMessageSource类。此变体支持相同的包文件格式,但比基于JDK的标准ResourceBundleMessageSource实现更加灵活。特别是,它允许从任何Spring资源位置读取文件(不只是从类路径),并支持热重载bundle属性文件(同时有效地在两者之间缓存它们)。有关详细信息,请查看ReloadableResourceBundleMessageSource javadocs。

3.15.2 标准和自定义事件

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。如果将实现ApplicationListener接口的bean部署到上下文中,每当将ApplicationEvent发布到ApplicationContext时,就会通知该bean。本质上,这是标准的观察者设计模式。

注意:从Spring 4.2开始,事件基础设施得到了显著的改进,并提供了基于注解的模型,以及发布任意事件的能力,即不一定要扩展自ApplicationEvent的对象。当这样一个对象发布时,我们将它封装在一个事件中。

Spring提供了以下标准事件:

表3.7 内置的事件

事件解释
ContextRefreshedEvent在初始化或刷新ApplicationContext时发布,例如,使用ConfigurableApplicationContext接口上的refresh()方法。这里的“初始化”意味着加载所有bean,检测并激活后处理器bean,预实例化单例,并准备使用ApplicationContext对象。只要上下文没有关闭,就可以多次触发刷新,前提是所选的ApplicationContext实际上支持这种“热”刷新。例如,XmlWebApplicationContext支持热刷新,但是GenericApplicationContext不支持。
ContextStartedEvent在启动ApplicationContext时,使用ConfigurableApplicationContext接口上的start()方法发布。这里的“Started”意味着所有生命周期bean都接收一个显式的start信号。通常,此信号用于在显式停止之后重新启动bean,但也可以用于启动未为autostart配置的组件,例如,在初始化时尚未启动的组件。
ContextStoppedEvent在停止ApplicationContext时,使用ConfigurableApplicationContext接口上的stop()方法发布。这里的“停止”意味着所有生命周期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()方法将阻塞,直到所有侦听器都完成对事件的处理。这种同步单线程方法的一个优点是,当侦听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文中进行操作。如果需要另一种事件发布策略,请参考JavaDoc获取Spring的ApplicationEventMulticaster接口。
下面的示例显示了用于注册和配置上述每个类的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;
	}

	@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表达式再次计算一个专用上下文。下表列出了对上下文可用的项,以便可以将它们用于条件事件处理:

表3.8 事件SpEL可用元数据

名字位置描述实例
Eventroot object实际ApplicationEvent#root.event
Arguments arrayroot object用于调用目标的参数(作为数组)#root.args[0]
Argument nameevaluation context任何方法参数的名称。如果由于某种原因名称不可用(例如没有调试信息),参数名称也可以在#a<#arg>下使用,其中#arg代表参数索引(从0开始)。#blEvent or #a0(one can also use#p0 or #p<#arg>notation as an alias).

请注意,#root.event允许你访问底层事件,即使你的方法签名实际上引用已发布的任意对象。
如果你需要发布处理另一个事件的结果,只需更改方法签名以返回应该发布的事件,如:

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
	// notify appropriate parties via notificationAddress and
	// then publish a ListUpdateEvent...
}

注意:异步侦听器不支持此功能。

这个新方法将为上面方法处理的每个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>,其中T是创建的实际实体的类型。你可以创建以下侦听器定义,以仅接收一个人的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,还适用于作为事件发送的任意对象。

3.15.3 方便地访问底层资源

为了最佳地使用和理解应用程序上下文,用户通常应该熟悉Spring的Resource 抽象,如第4章“资源”中所述。
应用程序上下文是ResourceLoader,可用于加载资源。资源本质上是JDK类java.net.URL的功能更丰富的版本,实际上,Resource的实现在适当的地方封装了java.net.URL的实例。Resource可以以透明的方式从几乎任何位置获得底层资源,包括类路径、文件系统位置、任何可以用标准URL描述的位置,以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。
你可以配置部署到应用程序上下文中的bean,以实现特殊的回调接口ResourceLoaderAware,以便在初始化时自动回调,并将应用程序上下文本身作为ResourceLoader传递进来。还可以公开Resource类型的属性,用于访问静态资源;它们会像其他属性一样被注入其中。你可以将这些资源属性指定为简单的字符串路径,并依赖于由上下文自动注册的特殊JavaBean PropertyEditor,以便在部署bean时将这些文本字符串转换为实际的资源对象。

提供给ApplicationContext构造函数的位置路径实际上是资源字符串,在简单的形式中,它们被适当地处理为特定上下文实现。ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。你还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不管实际的上下文类型如何。

3.15.4 方便的web应用程序的ApplicationContext实例化

你可以通过使用ContextLoader(例如)声明式地创建ApplicationContext实例。当然,你也可以通过使用ApplicationContext实现之一以编程方式创建ApplicationContext实例。
你可以使用ContextLoaderListener注册一个应用程序上下文,如下所示:

<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”结尾的文件。,位于“WEB-INF”目录和/WEB-INF/**/*Context.xml,用于“WEB-INF”任何子目录中的所有此类文件。

3.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实例,还可以通过Spring的标准事务管理以及JNDI和JMX支持设施注册到平台的JMX服务器。应用程序组件还可以通过Spring的TaskExecutor抽象与应用服务器的JCA WorkManager交互。
查看SpringContextResourceAdapter类的JavaDoc,了解RAR部署中涉及的配置细节。

要将Spring ApplicationContext简单地部署为Java EE RAR文件:将所有应用程序类打包到一个RAR文件中,这是一个标准JAR文件,具有不同的文件扩展名。将所有需要的库jar添加到RAR存档的根目录中。添加一个“meta - inf / ra.xml“部署描述符(如springcontextresourceadapter JavaDoc中所示)和相应的Spring xml bean定义文件(通常是“META-INF/applicationContext.xml”),并将生成的RAR文件放入应用服务器的部署目录中。

注意:这种RAR部署单元通常是自包含的;它们不向外部公开组件,甚至不向同一应用程序的其他模块公开组件。与基于rar的ApplicationContext的交互通常通过与其他模块共享的JMS目的地进行。例如,基于rar的ApplicationContext还可以调度一些作业,以响应文件系统中的新文件(或类似的东西)。如果需要允许从外部进行同步访问,则可以导出RMI端点,当然,同一台机器上的其他应用程序模块也可以使用这些端点。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值