写点什么

Spring 5 中文解析核心篇 -IoC 容器之 ApplicationContext 与 BeanFactory

用户头像
青年IT男
关注
发布于: 2020 年 09 月 04 日
Spring 5 中文解析核心篇-IoC容器之ApplicationContext与BeanFactory
1.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层

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被加载时,它会自动地在上下文中搜索bean定义为MessageSource的类。这个bean必须有个messageSource名字。如果bean没有找到,所有调用前面的方法被代理到消息源。如果没有找到消息源,ApplicationContext尝试去父容器查找相同名称的bean。如果找到,则使用它作为MessageSource。如果ApplicationContext没有找到任何消息源,一个空的DelegatingMessageSource被实例化去接受前面定义的方法调用。

Spring提供两个MessageSource实现,ResourceBundleMessageSourceStaticMessageSource。两者都实现HierarchicalMessageSource以便进行嵌套消息传递。StaticMessageSource很少被使用,但是提供编程式的方式去添加消息源。下面例子展示:

<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>

这个例子假设在你的类路径定义有三个资源包分别是formatexceptionswindows。任何解析消息的请求都以jdk标准的方式处理,即通过ResourceBundle对象解析消息。为了这个例子的目的,假设上面两个资源包文件内容分布如下:

# in format.properties
message=Alligators rock!



# in exceptions.properties
argument.required=The {0} argument is required.

下面例子展示一个程序去执行MessageSource功能。记住,所有的ApplicationContext实现也是MessageSources实现并且能够转换为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的文件中,它存在于你的类路径root下。messageSource bean定义通过它的basenames属性引用一些资源包。列表中传递给basenames属性的三个文件在类路径的根目录下以文件形式存在并且分别称为format.propertiesexceptions.propertieswindows.properties

下一个示例显示传递给消息查找的参数。这些参数被转换为String对象并且在查找消息中插入占位符。

<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.propertiesexceptions_en_GB.propertieswindows_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。任何在ApplicationContext中定义的bean,当MessageSource bean被创建且被配置时,实现MessageSourceAware接口将被注入应用上下文的MessageSource

作为ResourceBundleMessageSource的替代方法,Spring提供一个ReloadableResourceBundleMessageSource类。这个变体支持相同包文件格式,但是比基于标准JDK的ResourceBundleMessageSource实现更灵活。特别是,它允许从任何Spring资源为主读取文件并且支持包属性文件的热加载(同时在它们之间有效地缓存它们)。查看ReloadableResourceBundleMessageSourcejavadoc详细信息。

代码示例:com.liyong.ioccontainer.starter.MessageSourceIocContainer

1.15.2 标准和自定义事件

ApplicationContext中事件处理是通过ApplicationEventApplicationListener接口提供的。如果bean实现ApplicationListener接口并且部署到上下文中,则每次将ApplicationEvent发布到ApplicationContext时,都会通知该bean。实质上,这是一个标准的观察者模式。

从Spring4.2开始,事件基础设施已经被显著地改善并且提供基于注解的模式以及去发布任意事件的能力(也就是说,对象没有必须要从ApplicationEvent拓展)。当发布一个对象时,我们包装为事件。

下面表格描述Spring提供的标准事件:



你也可以创建和发布你自己的自定义事件。下面例子展示了一个简单类,它拓展了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并且注册它作为Spirng的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编程模型之上。

基于注解事件监听器

从Spring4.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() {
// ...
}

也可以通过使用定义SpEL表达式的注释的condition属性来添加其他运行时过滤器,该注释应匹配以针对特定事件实际调用该方法。

以下示例显示了仅当事件的content属性等于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...
}

这个特性对于异步监听是不支持的。

这个新方式通过上面的方法为每个BlackListEvent处理发布一个新ListUpdateEvent。如果你需要去发布一些事件,你可以返回一个事件Collection

异步事件监听器

如果你想一个特定监听器去处理异步事件,你可以重用常规的@Async支持。下面例子展示怎样使用:

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}

使用异步事件时,请注意以下限制:

  • 如果一个异步事件监听器抛出Exception,它不会被传递到调用者。查看AsyncUncaughtExceptionHandler更多详情。

  • 异步事件监听器方法不能通过返回一个值发布一个后续事件。如果你需要发布处理结果的其它事件,注入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) {
// ...
}

由于类型擦除,只有在触发的事件解析事件监听器过滤器所基于的通用参数(即,类似于类PersonCreatedEvent扩展EntityCreatedEvent<Person>{})的情况下才可以工作。

在某些情况下,如果所有事件都遵循相同的结构,这可能会变得很乏味(就像前面示例中的事件一样)。在这种情况下,你可以实现ResolvableTypeProvider来指导框架,使其超出运行时环境提供的范围(备注:通过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,而且适用于你作为事件发送的任何任意对象。

1.15.3 便捷地访问低级别资源

为了更好的使用和理解应用上下文,你应该熟悉Spring的Resource抽象,在Resources中描述。

应用上下文是ResourceLoader,它可以被使用加载Resource对象。Resource本质上是JDK java.net.URL类的功能更丰富的版本。事实上,Resource实现包装一个java.net.URL实例,Resource可以透明的方式从几乎任何位置获取低级资源,包含从类路径、文件系统路径、任何描述一个标准URL、以及其他的变体。如果资源位置字符串是没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。

你可以配置部署到应用程序上下文中的Bean,以实现特殊的回调接口ResourceLoaderAware,以便在初始化时自动回调,并将应用程序上下文本身作为ResourceLoader传入。你也可以暴露Resource类型属性,被使用获取静态资源。它们像其他属性一样注入其中。当bean被部署时,你可以指定这些Resource属性作为简单String路径并且依靠这些文本字符串自动转换真实Resource对象。

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

1.15.4 Web应用的ApplicationContext实例化

你可以通过声明式地创建ApplicationContext实例,例如: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 部署SpringApplicationContext作为Java EE rar文件

可以将Spring ApplicationContext部署为RAR文件,并将上下文及其所有必需的bean类和库JAR封装在Java EE RAR部署单元中。这等效于引导独立的ApplicationContext(仅托管在Java EE环境中)能够访问Java EE服务器功能。RAR部署是部署无头WAR文件的方案的一种更自然的选择,实际上,这种WAR文件没有任何HTTP入口点,仅用于在Java EE环境中引导Spring ApplicationContext。RAR部署非常适合于不需要HTTP入口点,而只由消息端点和调度任务组成的应用程序上下文。在上下文中的这些bean能使用应用服务资源,例如,JTA事物管理、JNDI绑定JDBC DataSource实例、JMS ConnectionFactory实例和注册平台的JMX服务-通过所有Spring的标准事物管理、JNDI、JMX支持的设施。应用组件也可以通过Spring的TaskExecutor抽象与应用server的JCA WorkManager相互交互 。

有关RAR部署中涉及的配置详细信息,请参见SpringContextResourceAdapter类的javadoc。

对于将Spring ApplicationContext作为Java EE RAR文件的简单部署:

  1. 打包应用所有类到RAR文件中(这是具有不同文件扩展名的标准JAR文件)。将所有必需的库JAR添加到RAR归档文件的根目录中。添加一个META-INF/ra.xml部署描述符(如SpringContextResourceAdapter的javadoc中所示和相应的Spring XML bean定义文件(通常为META-INF/applicationContext.xml)。

  2. 将生成的RAR文件拖放到应用程序服务器的部署目录中

此类RAR部署单位通常是独立的。它们不会将组件暴露给外界,甚至不会暴露给同一应用程序的其他模块。与基于RAR的ApplicationContext的交互通常是通过与其他模块共享的JMS目标进行的。例如,基于rar的ApplicationContext还可以调度一些作业或对文件系统中的新文件(或类似的东西)作出反应。如果它需要允许来自外部的同步访问,它可以(例如)导出RMI端点,这些端点可以由同一台机器上的其他应用程序模块使用。

1.16 BeanFactory

BeanFactory API为Spring IoC工厂提供底层基础。它的特定契约主要用于与Spring的其他部分和相关第三方框架的集成,它的DefaultListableBeanFactory实现是更高级别的GenericApplicationContext容器中的一个关键代理。

BeanFactory和相关的接口(例如:BeanFactoryAwareInitializingBeanDisposableBean)是为其他框架组件非常重要的集成点。通过不需要任何注解,甚至不需要反射,它们可以在容器及其组件之间进行非常有效的交互。应用级别bean可能使用相同回调接口,但通常更喜欢通过注释或通过编程配置进行声明式依赖注入。

请注意,核心BeanFactory API级别及其DefaultListableBeanFactory实现不对配置格式或要使用的任何组件注释进行假设。所有这些风格都通过扩展(比如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)实现,并在共享的BeanDefinition对象上进行操作,作为核心元数据表示。这是使Spring的容器如此灵活和可扩展的本质所在。

1.16.1 BeanFactoryApplicationContext

本节说明BeanFactoryApplicationContext容器级别之间的区别以及对引导的影响。

除非有充分的理由,否则应使用ApplicationContext,除非将GenericApplicationContext和其子类AnnotationConfigApplicationContext作为自定义引导的常见实现,否则应使用ApplicationContext。这些是用于所有常见目的的Spring核心容器的主要入口点:加载配置文件、触发类路径扫描、以编程方式注册Bean定义和带注解的类,以及(从5.0版本开始)注册功能性Bean定义。

因为ApplicationContext包含BeanFactory的所有功能,所以通常建议在普通BeanFactory中使用,除非需要完全控制Bean处理的方案。在ApplicationContext(例如GenericApplicationContext实现)中,按照约定(即,按bean名称或按bean类型(尤其是后处理器))检测到几种bean,而普通的DefaultListableBeanFactory则与任何特殊bean无关。

对于许多扩展的容器功能,例如注解处理和AOP代理,BeanPostProcessor扩展点是必不可少的。如果仅使用普通的DefaultListableBeanFactory,则默认情况下不会检测到此类后处理器并将其激活。这种情况可能会造成混淆,因为你的bean配置实际上并没有错。而是在这种情况下,需要通过其他设置完全引导容器。

下表列出了BeanFactoryApplicationContext接口和实现提供的功能。



要向DefaultListableBeanFactory显式注册Bean后处理器,需要以编程方式调用addBeanPostProcessor,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions
// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());
// now start using the factory

要将BeanFactoryPostProcessor应用于普通的DefaultListableBeanFactory,你需要调用其postProcessBeanFactory方法,如以下示例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));
// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
// now actually do the replacement
cfg.postProcessBeanFactory(factory);

在这两种情况下,显式的注册步骤都不方便,这就是为什么在Spring支持的应用程序中,各种ApplicationContext变量比普通的DefaultListableBeanFactory更为可取的原因,尤其是在典型企业设置中依赖BeanFactoryPostProcessorBeanPostProcessor实例来扩展容器功能时。

AnnotationConfigApplicationContext已注册了所有常见的注解后处理器,并且可以通过配置注解(例如@EnableTransactionManagement)在幕后引入其他处理器。在Spring基于注解的配置模型的抽象级别上,bean后处理器的概念仅是内部容器详细信息。

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公众号: 



发布于: 2020 年 09 月 04 日阅读数: 57
用户头像

青年IT男

关注

站在巨人肩上看得更远! 2018.04.25 加入

从事金融行业,就职过易极付、思建科技、网约车平台等一流技术团队,目前就职于银行负责支付系统建设。对金融行业有强烈的爱好。实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域

评论

发布
暂无评论
Spring 5 中文解析核心篇-IoC容器之ApplicationContext与BeanFactory