Spring详解4.容器内幕


点击进入我的博客

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

1.2 Spring创建Bean流程

下图描述了Spring容器从加载配置文件到创建一个Bean的完整流程:
Spring详解4.容器内幕

  1. ResourceLoader从存储介质中加载Spring配置文件,并使用Resource表示这个配置文件的资源。
  2. BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个<bean>解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistry中;
  3. 容器扫描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)。
  4. Spring容器从BeanDefinitionRegistry中取出加工后的BeanDefinition,并调用InstantiationStrategy着手进行Bean实例化的工作;
  5. 在实例化Bean时,Spring容器使用BeanWrapper对Bean进行封装,BeanWrapper提供了很多以Java反射机制操作Bean的方法,它将结合该Bean的BeanDefinition以及容器中属性编辑器,完成Bean属性的设置工作。
  6. 利用容器中注册的Bean后处理器(实现BeanPostProcessor接口的Bean)对已经完成属性设置工作的Bean进行后续加工,直接装配出一个准备就绪的Bean。

1.3 Spring中的组件

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

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

1.4 BeanDefinition

Spring详解4.容器内幕

  • org.springframework.beans.factory.config.BeanDefinition是配置文件<bean>元素标签在容器中的内部表示,是与<bean>一一对应的。
  • 一般的<bean>和父<bean>用RootBeanDefinition表示,而子<bean>用ChildBeanDefinition表示。
  • 一般情况下,BeanDefinition只在容器启动时加载并解析,除非容器重启或刷新。当然用户也可以在运行时通过编程调整BeanDefinition的定义。
创建BeanDefinition主要包括两个步骤:
  1. 利用BeanDefinitionReader读取承载配置信息的Resource,通过XML解析器解析配置信息的DOM对象,简单地每个<bean>生成对应地BeanDefinition对象。但是这里生成的BeanDefinition可能是半成品,因为在配置文件中,可能通过占位符变量引用外部属性文件的属性,这些占位符变量在这一步里还没有被解析出来。
  2. 利用容器中注册的BeanFactoryPostProcessor对半成品的BeanDefinition进行加工处理,将以占位符表示的配置解析为最终的实际值,这样半成品的BeanDefinition就成为成品的BeanDefinition。

1.5 InstantiationStrategy

Spring详解4.容器内幕

  • 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

Spring详解4.容器内幕

  • 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 属性文件

  1. 可以在属性文件中使用${}来实现属性之间的相互引用
  2. 如果一个属性值太长,可以在行后添加\将属性分为多行
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类表示一个本地化对象。它由语言参数和国家/地区参数构成。

  • 语言参数:每种语言由两位小写字母表示,如zhen
  • 国家/地区参数:用两个大写字母表示,如CNTWHKenUS
本地化工具类

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将属性名、参数数组以及默认信息封装起来,它的功能和第一个接口方法相同。
类结构

Spring详解4.容器内幕

  • 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都有一个事件源。
  • 事件监听器注册表:框架必须有一个地方来保存事件监听器,当事件源产生事件时,就会通知这些位于注册表中的监听器。
  • 事件广播器:是事件和事件监听器之间的桥梁,负责把事件通知给事件监听器。

Spring详解4.容器内幕

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的请求事件。

Spring详解4.容器内幕

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

Spring详解4.容器内幕

事件广播器

当发生容器事件时,容器主控程序将调用事件广播器将事件通知给事件监听器注册表中的事件监听器。Spring为事件广播器提供了接口和实现类。
Spring详解4.容器内幕

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

相关推荐