Spring详解4.容器内幕

50次阅读

共计 18915 个字符,预计需要花费 48 分钟才能阅读完成。

点击进入我的博客
1 Spring 容器整体流程
1.1 ApplicationContext 内部原理
AbstractApplicationContext 是 ApplicationContext 的抽象实现类,其中最重要的是 refresh()方法,它定义了容器在加载配置文件以后的各项处理过程。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// (1)初始化 BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// (2)调用工厂后处理器
invokeBeanFactoryPostProcessors(beanFactory);
// (3)注册 Bean 后处理器
registerBeanPostProcessors(beanFactory);
// (4)初始化消息源
initMessageSource();
// (5)初始化应用上下文事件广播器
initApplicationEventMulticaster();
// (6)初始化其他特殊 Bean,由具体子类实现
onRefresh();
// (7)注册事件监听器
registerListeners();
// (8)初始化所有单实例的 Bean(Lazy 加载的除外)
finishBeanFactoryInitialization(beanFactory);
// (9)完成刷新并发布容器刷新事件
finishRefresh();
}

catch (BeansException ex) {
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset ‘active’ flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring’s core, since we
// might not ever need metadata for singleton beans anymore…
resetCommonCaches();
}
}
}

初始化 BeanFactory:根据配置文件实例化 BeanFactory,在 obtainFreshBeanFactory()方法中,首先调用 refreshBeanFactory()刷新 BeanFactory,然后调用 getBeanFactory()方法获取 BeanFactory,这两个方法都是需要子类实现的抽象方法。在这一步里,Spring 将配置文件的信息装入到容器的 Bean 定义注册表(BeanDefinitionRegistry)中,但此时 Bean 还未初始化。
调用工厂后处理器:根据反射机制从 BeanDefinitionRegistry 中找出所有 BeanFactoryPostProcessor 类型的 Bean,并调用其 postProcessBeanFactory()接口方法。
注册 Bean 后处理器:根据反射机制从 BeanDefinitionRegistry 中找出所有 BeanPostProcessor 类型的 Bean,并将它们注册到容器 Bean 后处理器的注册表中。
初始化消息源:初始化容器的国际化信息资源。
初始化应用上下文事件广播器。
初始化其他特殊的 Bean:这是一个钩子方法,子类可以借助这个钩子方法执行一些特殊的操作——如 AbstractRefreshableWebApplicationContext 就使用该钩子方法执行初始化 ThemeSource 的操作。
注册事件监听器。
初始化 singleton 的 Bean:实例化所有 singleton 的 Bean(使用懒加载的吹),并将它们放入 Spring 容器的缓存中。
发布上下文刷新事件:创建上下文刷新事件,事件广播器负责将些事件广播到每个注册的事件监听器中。

1.2 Spring 创建 Bean 流程
下图描述了 Spring 容器从加载配置文件到创建一个 Bean 的完整流程:

ResourceLoader 从存储介质中加载 Spring 配置文件,并使用 Resource 表示这个配置文件的资源。
BeanDefinitionReader 读取 Resource 所指向的配置文件资源,然后解析配置文件。配置文件中每一个 <bean> 解析成一个 BeanDefinition 对象,并保存到 BeanDefinitionRegistry 中;
容器扫描 BeanDefinitionRegistry 中的 BeanDefinition,使用 Java 的反射机制自动识别出 Bean 工厂后处理器(实现 BeanFactoryPostProcessor 接口)的 Bean,然后调用这些 Bean 工厂后处理器对 BeanDefinitionRegistry 中的 BeanDefinition 进行加工处理。主要完成以下两项工作:

3.1 对使用到占位符的 <bean> 元素标签进行解析,得到最终的配置值,这意味对一些半成品式的 BeanDefinition 对象进行加工处理并得到成品的 BeanDefinition 对象。3.2 对 BeanDefinitionRegistry 中的 BeanDefinition 进行扫描,通过 Java 反射机制找出所有属性编辑器的 Bean(实现 java.beans.PropertyEditor 接口的 Bean),并自动将它们注册到 Spring 容器的属性编辑器注册表中(PropertyEditorRegistry)。

Spring 容器从 BeanDefinitionRegistry 中取出加工后的 BeanDefinition,并调用 InstantiationStrategy 着手进行 Bean 实例化的工作;
在实例化 Bean 时,Spring 容器使用 BeanWrapper 对 Bean 进行封装,BeanWrapper 提供了很多以 Java 反射机制操作 Bean 的方法,它将结合该 Bean 的 BeanDefinition 以及容器中属性编辑器,完成 Bean 属性的设置工作。
利用容器中注册的 Bean 后处理器(实现 BeanPostProcessor 接口的 Bean)对已经完成属性设置工作的 Bean 进行后续加工,直接装配出一个准备就绪的 Bean。

1.3 Spring 中的组件
Spring 中的组件按照所承担的角色可以划分为两类:

在 Bean 创建过程中被处理的元素:Resource、BeanDefinition、PropertyEditor 以及最终的 Bean。
处理上述元素的工具类:ResourceLoader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy、BeanWrapper 等。

1.4 BeanDefinition

org.springframework.beans.factory.config.BeanDefinition 是配置文件 <bean> 元素标签在容器中的内部表示,是与 <bean> 一一对应的。
一般的 <bean> 和父 <bean> 用 RootBeanDefinition 表示,而子 <bean> 用 ChildBeanDefinition 表示。
一般情况下,BeanDefinition 只在容器启动时加载并解析,除非容器重启或刷新。当然用户也可以在运行时通过编程调整 BeanDefinition 的定义。

创建 BeanDefinition 主要包括两个步骤:

利用 BeanDefinitionReader 读取承载配置信息的 Resource,通过 XML 解析器解析配置信息的 DOM 对象,简单地每个 <bean> 生成对应地 BeanDefinition 对象。但是这里生成的 BeanDefinition 可能是半成品,因为在配置文件中,可能通过占位符变量引用外部属性文件的属性,这些占位符变量在这一步里还没有被解析出来。
利用容器中注册的 BeanFactoryPostProcessor 对半成品的 BeanDefinition 进行加工处理,将以占位符表示的配置解析为最终的实际值,这样半成品的 BeanDefinition 就成为成品的 BeanDefinition。

1.5 InstantiationStrategy

org.springframework.beans.factory.support.InstantiationStrategy 负责根据 BeanDefinition 对象创建一个 Bean 实例。
InstantiationStrategy 仅负责实例化 Bean(相当于 new 的操作),不会设置的 Bean 属性,所以 InstantiationStrategy 返回的并不是最终的 Bean 实例,还需要通过 BeanWrapper 进行属性的设置。
SimpleInstantiationStrategy 是最常用的实例化策略,通过使用 Bean 的默认构造方法、带参数的构造方法或工厂方法创建 Bean 的实例。
CglibSubclassingInstantiationStrategy 利用 CGLib 类库为 Bean 动态生成子类,在子类中生成方法注入的逻辑,然后使用这个动态生成的子类创建 Bean 的实例。

1.6 BeanWrapper

BeanWrapper 相当于一个代理器,Spring 委托 BeanWrapper 完成 Bean 属性填充工作。
PropertyAccessor:属性访问接口定义了各种访问 Bean 属性的方法,如 getPropertyValue、setPropertyValue 等。
PropertyEditorRegistry:是属性编辑器的注册表,主要作用就是注册和保存属性编辑器。
BeanWrapperImpl:一个 BeanWrapperImpl 实例内部封装了两类组件——被封装的待处理的 Bean 和一套用于设置 Bean 属性的属性编辑器。BeanWrapperImpl 的三重身份——Bean 的包裹器、属性访问器和属性编辑器注册表。
Spring 首先从容器的 BeanDefinitionRegistry 中获取对应的 BeanDefinition,然后从 BeanDefinition 中获取 Bean 属性的配置信息 PropertyValue,然后使用属性编辑器对 PropertyValue 进行转换以得到 Bean 的属性值。

2 属性编辑器
我们在配置文件中配置的都是字面值,如果把它们转换成对应数据类型(如 double、int)的值或对象呢?
2.1 JavaBean 的属性编辑器
任何实现了 java.beans.PropertyEditor 接口的类都是属性编辑器,其主要功能就是将外部的设置值转换成 JVM 内部的对应类型。
PropertyEditor
PropertyEditor 是属性编辑器的接口,它规定了将外部设置值转换为内部 JavaBean 属性值的接口方法,是内部属性值和外部设置值的桥梁。

Object getValue():返回属性的当前值。基本类型被封装成对应的封装类实例。
void setValue(Object newValue):设置属性的值,基本类型以封装类传入。
String getAsText():将属性对象用一个字符串表示,以便外部的属性编辑器能以可视化的方式显示。缺省返回 null,表示该属性不能以字符串表示。
void setAsText(String text):用一个字符串去更新属性的内部值,这个字符串一般从外部属性编辑器传入。
String[] getTags():返回表示有效属性值的字符串数组(如 boolean 属性对应的有效 Tag 为 true 和 false),以便属性编辑器能以下拉框的方式显示出来。缺省返回 null,表示属性没有匹配的字符值有限集合。
String getJavaInitializationString():为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值。
我们一般不去直接实现 PropertyEditor,而是扩展 PropertyEditorSupport 来实现自己类。

BeanInfo
BeanInfo 主要描述了 JavaBean 的哪些属性可以编辑及对应的属性编辑器。BeanInfo 和 JavaBean 的对应关系通过二者命名规范确定:对应 JavaBean 的 BeanInfo 的命名规范应该是 <Bean>BeanInfo,如 Car 对应的 BeanInfo 为 CarBeanInfo。

JavaBean 的每个属性对应一个属性描述器 PropertyDescriptor。
BeanInfo 最重要的方法就是 PropertyDescriptor[] getPropertyDescriptors(),该方法返回 JavaBean 的属性描述数组。
BeanInfo 接口常用其实现类 SimpleBeanInfo,可以扩展此类实现功能。

PropertyEditorManager
JavaBean 规范提供了一个默认的属性编辑器 PropertyEditorManager,保存一些常见类型的属性编辑器。
2.2 Spring 属性编辑器
Spring 为常见的属性类型提供了默认的属性编辑器 PropertyEditorRegistrySupport,里边有多个用于保存属性编辑器的 Map 类型变量,键为属性类型,值为对应的属性编辑器实例。常见的类型如下所示。

类 别
说 明

基本数据类型
如:boolean、byte、short、int 等;

基本数据类型封装类
如:Long、Character、Integer 等;

两个基本数据类型的数组
char[]和 byte[];

大数类
BigDecimal 和 BigInteger

集合类
为 5 种类型的集合类 Collection、Set、SortedSet、List 和 SortedMap 提供了编辑器

资源类
用于访问外部资源的 8 个常见类 Class、Class[]、File、InputStream、Locale、Properties、Resource[]和 URL

2.3 自定义属性编辑器
Step1:我们可以通过扩展 java.beans.PropertyEditorSupport 类,并覆盖其中的 setAsText()方法即可自定义属性编辑器。
class KFCWaitress {
private KFCCombo kfcCombo;
// getters & setters
}

class KFCCombo {
private String burger;
private String drink;
// getters & setters
}

/**
* KFCCombo 的 Editor
*/
class KFCComboEditor extends PropertyEditorSupport {
@Override
public void setAsText(String text) throws IllegalArgumentException {
// 将字面值转换为属性类型对象
String[] textArr = text.split(“,”);
KFCCombo kfcCombo = new KFCCombo();
kfcCombo.setBurger(textArr[0]);
kfcCombo.setDrink(textArr[1]);

// 调用父类的 setValue()方法设置转换后的属性对象
setValue(kfcCombo);
}
}
Step2:如果使用 BeanFactory 需要手动调用 registerCustomEditor(class requiredType, PropertyEditor propertyEditor)方法注册自定义的属性编辑器;如果使用 ApplicationContext,只需要在配置文件中通过 CustomEditorConfigurer 注册即可。
<!– (1)配置自动注册属性编辑器的 CustomEditorConfigurer –>
<bean class=”org.springframework.beans.factory.config.CustomEditorConfigurer”>
<property name=”customEditors”>
<map>
<!– (2)属性编辑器对应的属性类型 –>
<entry key=”com.ankeetc.spring.KFCCombo” value=”com.ankeetc.spring.KFCComboEditor”/>
</map>
</property>
</bean>

<bean id=”myWaitress” class=”com.ankeetc.spring.KFCWaitress”>
<!– (3)该属性将使用 (2) 处的属性编辑器完成属性填充操作 –>
<property name=”kfcCombo” value=”Zinger Burger,Mirinda”/>
</bean>
在 (3) 处,直接通过一个字符串配置一个 Bean。BeanWrapper 在设置 KFCCombo 类型的属性时,将会检索自定义属性编辑器的注册表,如果发现有 KFCCombo 属性类型有对应的属性编辑器时,就会使用该方法的 setAsText()转换该对象。
3 使用外部属性文件
Spring 提供了一个 PropertyPlaceholderConfigurer 来引用外部属性文件,它实现了 BeanFactoryPostProcessor 接口,因此也是一个 Bean 工厂后处理器。
3.1 使用 PropertyPlaceholderConfigurer
简单的例子
通过 PropertyPlaceholderConfigurer 并引入属性文件,实现使用属性名来引用属性值。
<!– 创建 PropertyPlaceholderConfigurer 的 Bean 并引入属性文件 –>
<bean class=”org.springframework.beans.factory.config.PropertyPlaceholderConfigurer”>
<property name=”location” value=”classpath:application.properties”/>
<property name=”fileEncoding” value=”utf-8″/>
</bean>

<!– 也可以使用这种方式引用属性文件 –>
<context:property-placeholder location=”classpath:application.properties” file-encoding=”utf-8″/>

<!– 通过属性名引用属性值 –>
<bean id=”myCat” class=”com.ankeetc.spring.Cat”>
<property name=”name” value=”${name}”/>
</bean>
PropertyPlaceholderConfigurer 的其他属性

location:如果只有一个属性文件,则直接使用 location 属性指定就可以了;如果有多个属性文件,则可以通过 locations 属性进行设置。可以像配置 List 一样配置 locations 属性。
fileEncoding:属性文件的编码格式。Spring 使用操作系统默认编码读取属性文件。如果属性文件采用了特殊编码,则需要通过该属性显示指定。
order:如果配置文件中定义了多个 PropertyPlaceholderConfigurer,则通过该属性指定优先顺序。
placeholderPrefix:在上面的例子中,通过 ${属性名}引用属性文件中的属性项,其中 ${为默认的占位符前缀,可以根据需要改为其他的前缀符。
placeholderSuffix:占位符后缀,默认为}。

@Value 引用属性
在使用基于注解配置 Bean 时,可以通过 @Value 注解为 Bean 的成员变量或方法入参自动注入容器中已有的属性,也可以使用 @Value 注入字面值。
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(“com.ankeetc.spring);

System.out.println(applicationContext.getBean(Cat.class).getName());
}
}

@Configuration
class Config {
@Bean
public PropertyPlaceholderConfigurer configurer() {
PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
configurer.setLocation(new PathMatchingResourcePatternResolver().getResource(“classpath:application.properties”));
configurer.setFileEncoding(“utf-8”);
return configurer;
}
}

@Component
class Cat {
@Value(“${name}”)
private String name;

public String getName() {
return name;
}
}
3.2 使用加密的属性文件
如果属性是敏感的,一般不允许使用明文形式保存,此时需要对属性进行加密.PropertyPlaceHolderConfigurer 继承自 PropertyResourceConfigurer 类,后者有几个有用的 protected 方法(方法默认是空的即不会转换),用于在属性使用之前对属性列表中的属性进行转换。

void convertProperties(Properties props):属性文件的所有属性值都封装在 props 中,覆盖该方法,可以对所有的属性值进行转换处理。

String convertProperty(String propertyName, String propertyValue):在加载属性文件并读取文件中的每个属性时,都会调用此方法进行转换处理。

String convertPropertyValue(String originalValue):和上一个方法类似,只不过没有传入属性名。

简单例子
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(“com.ankeetc.spring”);
// userName 没有被改变
System.out.println(applicationContext.getBean(DataSource.class).getUserName());
// password 值被改变
System.out.println(applicationContext.getBean(DataSource.class).getPassword());
}
}

@Component
class DataSource {
@Value(“${userName}”)
private String userName;
@Value(“${password}”)
private String password;

public String getUserName() {
return userName;
}

public String getPassword() {
return password;
}
}

@Configuration
class Config {
@Bean
public EncryptPropertyPlaceholderConfigurer encryptPropertyPlaceholderConfigurer() {
EncryptPropertyPlaceholderConfigurer configurer = new EncryptPropertyPlaceholderConfigurer();
configurer.setLocation(new PathMatchingResourcePatternResolver().getResource(“classpath:application.properties”));
configurer.setFileEncoding(“utf-8”);
return configurer;
}
}

class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
@Override
protected String convertProperty(String propertyName, String propertyValue) {
if (“password”.equals(propertyName)) {
// 在此过滤并实现相关的揭秘逻辑
return “Decrypt” + propertyValue;
} else {
return propertyValue;
}
}
}
3.3 属性文件
可以在属性文件中使用 ${}来实现属性之间的相互引用
dbName=myDatabase
url=jdbc:mysql://localhost:3306/${dbName}
如果一个属性值太长,可以在行后添加 \ 将属性分为多行
4 应用 Bean 的属性值
基于 XML 的配置
在 XML 配置文件中,可以使用 #{beanName.propName}的方式引用其他 Bean 的属性值。
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath:beans.xml”);
System.out.println(applicationContext.getBean(Application.class).getDatabaseName());
System.out.println(applicationContext.getBean(Application.class).getDatabasePassword());
}
}

class DatabaseConfig {
private String userName;
private String password;
// getters & setters
}

class Application {
private String databaseName;
private String databasePassword;
// getters & setters
}
<bean id=”databaseConfig” class=”com.ankeetc.spring.DatabaseConfig”>
<property name=”userName” value=”lucas”/>
<property name=”password” value=”123456″/>
</bean>

<!– 通过 #{databaseConfig.propName}的方式引用 databaseConfig 的属性值 –>
<bean id=”applicationConfig” class=”com.ankeetc.spring.Application”>
<property name=”databaseName” value=”#{databaseConfig.userName}”/>
<property name=”databasePassword” value=”#{databaseConfig.password}”/>
</bean>
基于注解和 Java 类的配置
使用 @Value(“#{beanName.propName}”)的形式也可以引用其他类的属性值。
@Component
class Application {
@Value(“#{databaseConfig.userName}”)
private String databaseName;
@Value(“#{databaseConfig.password}”)
private String databasePassword;
}
5 国际化信息
国际化信息的含义是根据不同的地区语言类型返回不同的信息,简单来说就是为每种语言配置一套对应的资源文件。
5.1 基础知识
本地化类 java.util.Locale
国际化信息也称为本地化信息,由 java.util.Locale 类表示一个本地化对象。它由语言参数和国家 / 地区参数构成。

语言参数:每种语言由两位小写字母表示,如 zh、en

国家 / 地区参数:用两个大写字母表示,如 CN、TW、HK、EN、US

本地化工具类
java.util 包下的 NumberFormat、DateFormat、MessageFormat 都支持本地化的格式化操作,而且 MessageFormat 还支持占位符的格式化操作。
ResourceBundle
使用 ResourceBundle 可以访问不同本地化资源文件,文件名必须按照如下格式来命名:资源名_语言代码_国家地区代码.properties,其中语言代码和国家 / 地区代码是可选的。假如默认资源文件的文件名为 application.properties,则中文中国大陆的资源文件名为 application_zh_CN.properties。
public class Main {
public static void main(String[] args) {
// 如果找不到对应的资源文件,将会使用默认的资源文件
// getBundle 是类路径的文件名,而且不带.properties 后缀
ResourceBundle zhCN = ResourceBundle.getBundle(“application”, Locale.SIMPLIFIED_CHINESE);
ResourceBundle enUs = ResourceBundle.getBundle(“application”, Locale.US);
System.out.println(zhCN.getString(“name”));
System.out.println(enUs.getString(“name”));
}
}
5.2 MessageSource
Spring 定义了访问国际化信息的 MessageSource 接口,主要方法如下:

String getMessage(String code, Object[] args, String defaultMessage, Locale locale):code 表示国际化资源中的属性名;args 用于传递格式化串占位符所用的运行期参数;当在资源找不到对应属性名时,返回 defaultMessage 参数所指定的默认信息;locale 表示本地化对象;

String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException:与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出 NoSuchMessageException 异常;

String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException:MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个接口方法相同。

类结构

HierarchicalMessageSource 接口的作用是建立父子层级的 MessageSource 结构。

StaticMessageSource 主要用于程序测试,它允许通过编程的方式提供国际化信息。

ResourceBundleMessageSource 实现类允许通过 beanName 指定一个资源名(包括类路径的全限定资源名),或通过 beanNames 指定一组资源名。

ReloadableResourceBundleMessageSource 提供了定时刷新功能,允许在不重启系统的情况下,更新资源的信息。

public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“beans.xml”);
System.out.println(applicationContext.getBean(“myMessageSource1”, MessageSource.class).getMessage(“name”, null, Locale.US));
System.out.println(applicationContext.getBean(“myMessageSource1”, MessageSource.class).getMessage(“name”, null, Locale.SIMPLIFIED_CHINESE));
}
}
<!– 可以使用 basename 指定资源文件的路径 –>
<bean id=”myMessageSource1″ class=”org.springframework.context.support.ResourceBundleMessageSource”>
<property name=”basename” value=”application”/>
</bean>

<!– 可以使用 basenames 指定多组资源文件的路径 –>
<bean id=”myMessageSource2″ class=”org.springframework.context.support.ResourceBundleMessageSource”>
<property name=”basenames”>
<list>
<value>application</value>
</list>
</property>
</bean>

<!– 当使用 ReloadableResourceBundleMessageSource 可以使用 cacheSeconds 指定刷新周期 –>
<!– 刷新周期默认为 - 1 即不刷新,单位是秒 –>
<bean id=”myMessageSource3″ class=”org.springframework.context.support.ReloadableResourceBundleMessageSource”>
<property name=”basename” value=”application”/>
<property name=”cacheSeconds” value=”100″/>
</bean>
5.3 容器级的国际化信息
由于 ApplicationContext 本身也继承了 MessageSource 接口,所以 ApplicationContext 的所有实现类本身也是一个 MessageSource 对象,国际化信息是整个容器的公共设施。在本章(1.1 ApplicationContext 内部原理)我们提到,在 ApplicationContext 会在 initMessageSource()方法中,Spring 通过反射机制找出 bean 名为 messageSource(bean 名必须是 messageSource)且类型为 MessageSource 子类的 Bean,将这个 Bean 定义的信息资源加载为容器级的国际化信息资源。
public class Main {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“beans.xml”);
System.out.println(applicationContext.getMessage(“name”, null, Locale.US));
}
}
<bean id=”messageSource” class=”org.springframework.context.support.ResourceBundleMessageSource”>
<property name=”basename” value=”application”/>
</bean>
6 容器事件
6.1 Java 的事件体系
事件体系是观察者模式的一种具体实现方式,一共有如下几个角色:

事件:java.util.EventObject 是 Java 中的事件。
监听器:java.util.EventListener 是用于描述事件的接口,是一个没有任何方法的标记接口。
事件源:事件的生产者,任何一个 EventObject 都有一个事件源。
事件监听器注册表:框架必须有一个地方来保存事件监听器,当事件源产生事件时,就会通知这些位于注册表中的监听器。
事件广播器:是事件和事件监听器之间的桥梁,负责把事件通知给事件监听器。

public class Main {
public static void main(String[] args) {
Waitress waitress = new Waitress(“ 田二妞 ”);
waitress.addEventListener(new Chef(“ 王二狗 ”));
waitress.order(“ 宫保鸡丁 ”);
// 厨师 [王二狗] 收到服务员 [田二妞] 的订单,开始做[宫保鸡丁]
}
}

// 一个餐厅的点单事件,继承了 EventObject
class OrderEventObject extends EventObject {
private String order;

public String getOrder() {
return order;
}

public OrderEventObject(Object source, String order) {
super(source);
this.order = order;
}
}

// 服务员是事件源,由她产生点单事件
class Waitress {
private String name;
// 服务员维护了所有在餐厅的厨师,即监听器注册表
private List<Chef> eventListenerList = new ArrayList<>();

public Waitress(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void addEventListener(Chef chef) {
eventListenerList.add(chef);
}

// 该方法是广播器,即把点单事件通知给注册表中的全部厨师
public void order(String order) {
OrderEventObject orderEventObject = new OrderEventObject(this, order);
eventListenerList.forEach(var -> var.cook(orderEventObject));
}
}

// 厨师是事件监听器
class Chef implements EventListener {
private String name;

public Chef(String name) {
this.name = name;
}

// 监听到点单事件并作出相关反应
public void cook(EventObject o) {
System.out.println(String.format(“ 厨师 [%s] 收到服务员 [%s] 的订单,开始做[%s]”, name, ((Waitress)o.getSource()).getName(), ((OrderEventObject)o).getOrder()));
}
}
6.2 Spring 事件类结构
事件类

ApplicationEvent:Spring 的事件类的基类,其类结构如下所示。
ApplicationContextEvent:容器事件,它拥有 4 个子类分别表示容器的启动、刷新、停止、关闭事件。
RequestHandleEvent:与 Web 应用有关的事件,当一个 HTTP 请求被处理后产生该事件。只有在 web.xml 中定义了 DispatcherServlet 时才会产生该事件。它有两个子类,分别代表 Servlet 和 Portlet 的请求事件。

事件监听器接口

ApplicationListener:该接口只定义了一个方法 onApplicationEvent(E event),该方法接受 ApplicationEvent 事件对象,在该方法中编写事件的响应处理逻辑。
SmartApplicationListener:定义了两个方法 boolean supportsEventType(Class<? extends ApplicationEvent> eventType):指定监听器支持哪种类型的容器事件,即它只会对该类型的事件做出响应;boolean supportsSourceType(Class<?> sourceType):指定监听器仅对何种事件源对象做出响应。
GenericApplicationListener:Spring 4.2 新增的类,使用可解析类型 ResolvableType 增强了对范型的支持。

事件广播器
当发生容器事件时,容器主控程序将调用事件广播器将事件通知给事件监听器注册表中的事件监听器。Spring 为事件广播器提供了接口和实现类。
6.3 Spring 事件体系具体实现
Spring 在 ApplicationContext 接口的抽象实现类 AbstractApplicationContext 中完成了事件体系的搭建。AbstractApplicationContext 拥有一个 applicationEventMulticaster(应用上下文事件广播器)成员变量,它提供了容器监听器的注册表。AbstractApplicationContext 在 refresh()这个容器启动启动方法中通过以下 3 个步骤搭建了事件的基础设施:
public void refresh() throws BeansException, IllegalStateException {
// (5)初始化应用上下文事件广播器
initApplicationEventMulticaster();
// (7)注册事件监听器
registerListeners();
// (9)完成刷新并发布容器刷新事件
finishRefresh();
}

在 (5) 处,Spring 初始化事件的广播器,可以在配置文件中为容器定义一个自定义的事件广播器,只要实现 ApplicationEventMulticaster 即可,Spring 会通过反射机制将其注册容器的事件广播器。如果没有找到配置的外部事件广播器,则 Spring 自动使用 SimpleApplicationEventMulticaster 作为事件广播器。
在 (7) 处,Spring 根据反射机制,从 BeanDefinitionRegistry 中找出所有实现 ApplicationListener 的 Bean,将它们注册为容器的事件监听器,即将其添加到事件广播器所提供的事件监听器注册表中
在 (9) 处,容器启动完成,调用事件发布接口向容器中所有的监听器发布事件

6.4 一个例子
假如我们希望容器刷新时打印一行文字,可以继承 GenericApplicationListener 并实现相关方法。
public class Main {
public static void main(String[] args) {
// new AnnotationConfigApplicationContext()会调用 refresh 方法,MyListener 会监听到并处理
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(“com.ankeetc.spring”);
// stop 事件不会被监听到
((AnnotationConfigApplicationContext) applicationContext).stop();
}
}

@Component
class MyListener implements GenericApplicationListener {
// 判断是否是刷新事件
@Override
public boolean supportsEventType(ResolvableType eventType) {
return ResolvableType.forClass(ContextRefreshedEvent.class).equals(eventType);
}

@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}

// 在此实现监听到相关事件的处理逻辑
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println(“Hello world”);
}

@Override
public int getOrder() {
return 0;
}
}

正文完
 0