Spring全家桶Spring

Spring详解4.容器内幕

2018-12-24  本文已影响3人  卢卡斯哔哔哔

点击进入我的博客

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的完整流程:


Bean创建流程
  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

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

1.5 InstantiationStrategy

InstantiationStrategy类继承结构

1.6 BeanWrapper

BeanWrapper类继承结构

2 属性编辑器

我们在配置文件中配置的都是字面值,如果把它们转换成对应数据类型(如double、int)的值或对象呢?

2.1 JavaBean的属性编辑器

任何实现了java.beans.PropertyEditor接口的类都是属性编辑器,其主要功能就是将外部的设置值转换成JVM内部的对应类型。

PropertyEditor

PropertyEditor是属性编辑器的接口,它规定了将外部设置值转换为内部JavaBean属性值的接口方法,是内部属性值和外部设置值的桥梁。

BeanInfo

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

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的其他属性
@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方法(方法默认是空的即不会转换),用于在属性使用之前对属性列表中的属性进行转换。

简单例子
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. 可以在属性文件中使用${}来实现属性之间的相互引用
dbName=myDatabase
url=jdbc:mysql://localhost:3306/${dbName}
  1. 如果一个属性值太长,可以在行后添加\将属性分为多行

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类表示一个本地化对象。它由语言参数和国家/地区参数构成。

本地化工具类

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接口,主要方法如下:

类结构
MessageSource类结构
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的事件体系

事件体系是观察者模式的一种具体实现方式,一共有如下几个角色:

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事件类结构

事件类
事件类
事件监听器接口
事件监听器接口
事件广播器

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


事件广播器

6.3 Spring事件体系具体实现

Spring在ApplicationContext接口的抽象实现类AbstractApplicationContext中完成了事件体系的搭建。AbstractApplicationContext拥有一个applicationEventMulticaster(应用上下文事件广播器)成员变量,它提供了容器监听器的注册表。AbstractApplicationContext在refresh()这个容器启动启动方法中通过以下3个步骤搭建了事件的基础设施:

public void refresh() throws BeansException, IllegalStateException {
    // (5)初始化应用上下文事件广播器
    initApplicationEventMulticaster();
    // (7)注册事件监听器
    registerListeners();
    // (9)完成刷新并发布容器刷新事件
    finishRefresh();
}

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;
    }
}
上一篇下一篇

猜你喜欢

热点阅读