三万字盘点Spring 9大核心基础功能(下篇)

史前动物
发布于 2023-9-18 14:24
浏览
0收藏

数据绑定

上一节我们讲了类型转换,而既然提到了类型转换,那么就不得不提到数据绑定了,他们是密不可分的,因为在数据绑定时,往往都会伴随着类型转换,

数据绑定的意思就是将一些配置属性跟我们的Bean对象的属性进行绑定。

不知你是否记得,在远古的ssm时代,我们一般通过xml方式声明Bean的时候,可以通过​​<property/>​​来设置Bean的属性

<bean class="com.sanyou.spring.core.basic.User">
    <property name="username" value="三友的java日记"/>
</bean>
@Data
public class User {

    private String username;

}

然后Spring在创建User的过程中,就会给​​username​​​属性设置为​​三友的java日记​​。

这就是数据绑定,将​​三友的java日记​​绑定到username这个属性上。

数据绑定的核心api主要包括以下几个:

  • PropertyValues
  • BeanWrapper
  • DataBinder

1、PropertyValues

这里我们先来讲一下PropertyValue(注意没有s)

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

顾明思议,PropertyValue就是就是封装了属性名和对应的属性值,它就是数据绑定时属性值的来源。

以前面的提到的xml创建Bean为例,Spring在启动的时候会去解析xml中的​​<property/>​​​标签,然后将​​name​​​和​​value​​封装成PropertyValue

当创建User这个Bean的时候,到了属性绑定的阶段的时候,就会取出PropertyValue,设置到User的username属性上。

而PropertyValues,比PropertyValue多了一个s,也就是复数的意思,所以其实PropertyValues本质上就是PropertyValue的一个集合

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

因为一个Bean可能有多个属性配置,所以就用PropertyValues来保存。

2、BeanWrapper

BeanWrapper其实就数据绑定的核心api了,因为在Spring中涉及到数据绑定都是通过BeanWrapper来完成的,比如前面提到的Bean的属性的绑定,就是通过BeanWrapper来的

BeanWrapper是一个接口,他有一个唯一的实现BeanWrapperImpl。

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

先来个demo

public class BeanWrapperDemo {

    public static void main(String[] args){
        //创建user对象
        User user = new User();

        //创建BeanWrapper对象,把需要进行属性绑定的user对象放进去
        BeanWrapper beanWrapper = new BeanWrapperImpl(user);

        //进行数据绑定,将三友的java日记这个属性值赋值到username这个属性上
        beanWrapper.setPropertyValue(new PropertyValue("username", "三友的java日记"));

        System.out.println("username = " + user.getUsername());
    }

}

结果

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

成功获取到,说明设置成功

BeanWrapperImpl也间接实现了TypeConverter接口

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

当然底层还是通过前面提到的ConversionService和PropertyEditor实现的

所以当配置的类型跟属性的类型不同时,就可以对配置的类型进行转换,然后再绑定到属性上

这里简单说一下数据绑定和@Value的异同,因为这两者看起来好像是一样的,但实际还是有点区别的

相同点:

  • 两者都会涉及到类型转换,@Value和数据绑定都会将值转换成目标属性对应的类型,并且都是通过TypeConverter来转换的

不同点:

  • 1、发生时机不同,@Value比数据绑定更早,当@Value都注入完成之后才会发生数据绑定(属性赋值)
  • 2、属性赋值方式不同,@Value是通过反射来的,而是数据绑定是通过setter方法来的,如果没有setter方法,属性是没办法绑定的

3、DataBinder

DataBinder也是用来进行数据绑定的,它的底层也是间接通过BeanWrapper来实现的数据绑定的

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

但是他相比于BeanWrapper多了一些功能,比如在数据绑定之后,可以对数据校验,比如可以校验字段的长度等等

说到数据校验,是不是想到了SpringMVC中的参数校验,通过@Valid配合一些诸如@NotBlank、@NotNull等注解,实现优雅的参数校验。

其实SpringMVC的参数校验就是通过DataBinder来的,所以DataBinder其实在SpringMVC中用的比较多,但是在Spring中确用的很少。

如果你有兴趣,可以翻一下SpringMVC中关于请求参数处理的HandlerMethodArgumentResolver的实现,里面有的实现会用到DataBinder(WebDataBinder)来进行数据请求参数跟实体类的数据绑定、类型转换、数据校验等等。

不知道你有没有注意过,平时写接口的时候,前端传来的参数String类型的时间字符串无法通过Spring框架本身转换成Date类型,有部分原因就是前面提到的Spring没有相关的Converter实现

总的来说,数据绑定在xml配置和SpringMVC中用的比较多的,并且数据绑定也是Spring Bean生命周期中一个很重要的环节。

泛型处理

Spring为了方便操作和处理泛型类型,提供了一个强大的工具类——ResolvableType。

泛型处理其实是一块相对独立的东西,因为它就只是一个工具类,只还不过这个工具类在Spring中却是无处不在!

ResolvableType提供了有一套灵活的API,可以在运行时获取和处理泛型类型等信息。

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

ResolvableType

接下来就通过一个案例,来看一看如何通过ResolvableType快速简单的获取到泛型的

首先我声明了一个MyMap类,继承HashMap,第一个泛型参数是Integer类型,第二个泛型参数是List类型,List的泛型参数又是String

public class MyMap extends HashMap<Integer, List<String>> {

}

接下来就来演示一下如何获取到HashMap的泛型参数以及List的泛型参数

第一步,先来通过​​ResolvableType#forClass​​方法创建一个MyMap类型对应的ResolvableType

//创建MyMap对应的ResolvableType
ResolvableType myMapType = ResolvableType.forClass(MyMap.class);

因为泛型参数是在父类HashMap中,所以我们得获取到父类HashMap对应的ResolvableType,通过​​ResolvableType#getSuperType()​​方法获取

//获取父类HashMap对应的ResolvableType
ResolvableType hashMapType = myMapType.getSuperType();

接下来需要获取HashMap的泛型参数对应的ResolvableType类型,可以通过​​ResolvableType#getGeneric(int... indexes)​​就可以获取指定位置的泛型参数ResolvableType,方法参数就是指第几个位置的泛型参数,从0开始

比如获取第一个位置的对应的ResolvableType类型

//获取第一个泛型参数对应的ResolvableType
ResolvableType firstGenericType = hashMapType.getGeneric(0);

现在有了第一个泛型参数的ResolvableType类型,只需要通过​​ResolvableType#resolve()​​方法就可以获取到ResolvableType类型对应的class类型,这样就可以获取到一个泛型参数的class类型

//获取第一个泛型参数对应的ResolvableType对应的class类型,也就是Integer的class类型
Class<?> firstGenericClass = firstGenericType.resolve();

如果你想获取到HashMap第二个泛型参数的泛型类型,也就是List泛型类型就可以这么写

//HashMap第二个泛型参数的对应的ResolvableType,也就是List<String>
ResolvableType secondGenericType = hashMapType.getGeneric(1);
//HashMap第二个泛型参数List<String>的第一个泛型类型String对应的ResolvableType
ResolvableType secondFirstGenericType = secondGenericType.getGeneric(0);
//这样就获取到了List<String>的泛型类型String
Class<?> secondFirstGenericClass = secondFirstGenericType.resolve();

从上面的演示下来可以发现,其实每变化一步,其实就是获取对应泛型或者是父类等等对应的ResolvableType,父类或者是泛型参数又可能有泛型之类的,只需要一步一步获取就可以了,当需要获取到具体的class类型的时候,通过​​ResolvableType#resolve()​​方法就行了。

除了上面提到的通过​​ResolvableType#forClass​​方法创建ResolvableType之外,还可以通过一下几个方法创建:

  • ​forField(Field field)​​:获取字段类型对应的ResolvableType
  • ​forMethodReturnType(Method method)​​:获取方法返回值类型对应的ResolvableType
  • ​forMethodParameter(Method method, int parameterIndex)​​:获取方法某个位置方法参数对应的ResolvableType
  • ​forConstructorParameter(Constructor<?> constructor, int parameterIndex)​​:获取构造方法某个构造参数对应的ResolvableType

通过上面解释可以看出,对于一个类方法参数,方法返回值,字段等等都可以获取到对应的ResolvableType

国际化

国际化(Internationalization,简称i18n)也是Spring提供的一个核心功能,它其实也是一块相对独立的功能。

所谓的国际化,其实理解简单点就是对于不同的地区国家,输出的文本内容语言不同。

Spring的国际化其实主要是依赖Java中的国际化和文本处理方式。

1、Java中的国际化

Locale

Locale是Java提供的一个类,它可以用来标识不同的语言和地区,如en_US表示美国英语,zh_CN表示中国大陆中文等。

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

目前Java已经穷举了很多国家的地区Locale。

我们可以使用Locale类获取系统默认的Locale,也可以手动设置Locale,以适应不同的语言环境。

ResourceBundle

ResourceBundle是一个加载本地资源的一个类,他可以根据传入的Locale不同,加载不同的资源。

来个demo

首先准备资源文件,资源文件通常是.properties文件,文件名命名规则如下:

basename_lang_country.properties

basename无所谓,叫什么都可以,而lang和country是从Locale中获取的。

举个例子,我们看看英语地区的Locale

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

从上图可以看出,英语Locale的lang为en,country为空字符串,那么此时英语地区对应资源文件就可以命名为:basename_en.properties,由于country为空字符串,可以省略

中国大陆Locale如下图

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

此时文件就可以命为:basename_zh_CN.properties

好了,现在既然知道了命名规则,我们就创建两个文件,basename就叫message,一个英语,一个中文,放在classpath路径下

中文资源文件:message_zh_CN.properties,内容为:

name=三友的java日记

英文资源文件:message_en.properties,内容为:

name=sanyou's java diary

有了文件之后,就可以通过​​ResourceBundle#getBundle(String baseName,Locale locale)​​方法来获取获取ResourceBundle

  • 第一个参数baseName就是我们的文件名中的basename,对于我们的demo来说,就是message
  • 第二个参数就是地区,根据地区的不同加载不同地区的文件

测试一下

public class ResourceBundleDemo {

    public static void main(String[] args){

        //获取ResourceBundle,第一个参数baseName就是我们的文件名称,第二个参数就是地区
        ResourceBundle chineseResourceBundle = ResourceBundle.getBundle("message", Locale.SIMPLIFIED_CHINESE);
        //根据name键取值
        String chineseName = chineseResourceBundle.getString("name");
        System.out.println("chineseName = " + chineseName);

        ResourceBundle englishResourceBundle = ResourceBundle.getBundle("message", Locale.ENGLISH);
        String englishName = englishResourceBundle.getString("name");
        System.out.println("englishName = " + englishName);

    }

}

运行结果

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

其实运行结果可以看出,其实是成功获取了,只不过中文乱码了,这主要是因为ResourceBundle底层其实编码是​​ISO-8859-1​​,所以会导致乱码。

解决办法最简单就是把中文用Java Unicode序列来表示,之后就可以读出中文了了,比如​​三友的java日记​​​用Java Unicode序列表示为​​\u4e09\u53cb\u7684java\u65e5\u8bb0​

除了这种方式之外,其实还可以继承ResourceBundle内部一个Control类

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

Control

重写newBundle方法

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

newBundle

newBundle是创建ResourceBundle对应核心方法,重写的时候你就可以随心所欲让它支持其它编码方式。

有了新的Control之后,获取ResourceBundle时只需要通过​​ResourceBundle#getBundle(String baseName, Locale targetLocale,Control control)​​方法指定Control就可以了。

Spring实际上就是通过这种方式扩展,支持不同编码的,后面也有提到。

MessageFormat

MessageFormat顾明思议就是把消息格式化。它可以接收一条包含占位符的消息模板,并根据提供的参数替换占位符,生成最终的消息。

MessageFormat对于将动态值插入到消息中非常有用,如欢迎消息、错误消息等。

先来个Demo

public class MessageFormatDemo {

    public static void main(String[] args){
        String message = MessageFormat.format("你好:{0}", "张三");
        System.out.println("message = " + message);
    }

}

解释一下上面这段代码:

  • ​你好:{0}​​​其实就是前面提到的消息的模板,​​{0}​​就是占位符,中间的0代表消息格式化的时候将提供的参数第一个参数替换占位符的值
  • ​张三​​​就是提供的参数,你可以写很多个,但是我们的demo只会取第一个参数,因为是​​{0}​

所以输出结果为:

message = 你好:张三

成功格式化消息。

2、Spring国际化

Spring提供了一个国际化接口MessageSource

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

MessageSource

他有一个基于ResourceBundle + MessageFormat的实现ResourceBundleMessageSource

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

ResourceBundleMessageSource

他的本质可以在资源文件存储消息的模板,然后通过MessageFormat来替换占位符,MessageSource的getMessage方法就可以传递具体的参数

来个demo

现在模拟登录欢迎语句,对于不同的人肯定要有不同的名字,所以资源文件需要存模板,需要在不同的资源文件加不同的模板

中文资源文件:message_zh_CN.properties

welcome=您好:{0}

英文资源文件:message_en.properties

welcome=hello:{0}

占位符,就是不同人不同名字

测试代码

public class MessageSourceDemo {

    public static void main(String[] args){
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();

        //Spring已经扩展了ResourceBundle的Control,支持资源文件的不同编码方式,但是需要设置一下
        messageSource.setDefaultEncoding("UTF-8");

        //添加 baseName,就是前面提到的文件中的basename
        messageSource.addBasenames("message");

        //中文,传个中文名字
        String chineseWelcome = messageSource.getMessage("welcome", new Object[]{"张三"}, Locale.SIMPLIFIED_CHINESE);
        System.out.println("chineseWelcome = " + chineseWelcome);

        //英文,英语国家肯定是英文名
        String englishWelcome = messageSource.getMessage("welcome", new Object[]{"Bob"}, Locale.ENGLISH);
        System.out.println("englishWelcome = " + englishWelcome);
    }

}

运行结果

chineseWelcome = 您好:张三
englishWelcome = hello:Bob

成功根据完成不同国家资源的加载和模板消息的格式化。

小结

这里来简单总结一下这一小节说的内容

  • Locale:不同国家和地区的信息封装
  • ResourceBundle:根据不同国家的Locale,加载对应的资源文件,这个资源文件的命名需要遵守​​basename_lang_country.properties​​命名规范
  • MessageFormat:其实就是一个文本处理的方式,他可以解析模板,根据参数替换模板的占位符
  • MessageSource:Spring提供的国际化接口,其实他底层主要是依赖Java的ResourceBundle和MessageFormat,资源文件存储模板信息,MessageFormat根据MessageSource方法的传参替换模板中的占位符

BeanFactory

我们知道Spring的核心就是IOC和AOP,而BeanFactory就是大名鼎鼎的IOC容器,他可以帮我们生产对象。

1、BeanFactory接口体系

BeanFactory本身是一个接口

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

BeanFactory

从上面的接口定义可以看出从可以从BeanFactory获取到Bean。

他也有很多子接口,不同的子接口有着不同的功能

  • ListableBeanFactory
  • HierarchicalBeanFactory
  • ConfigurableBeanFactory
  • AutowireCapableBeanFactory
ListableBeanFactory

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

ListableBeanFactory

从提供的方法可以看出,提供了一些获取集合的功能,比如有的接口可能有多个实现,通过这些方法就可以获取这些实现对象的集合。

HierarchicalBeanFactory

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

HierarchicalBeanFactory

从接口定义可以看出,可以获取到父容器,说明BeanFactory有子父容器的概念。

ConfigurableBeanFactory

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

ConfigurableBeanFactory

从命名可以看出,可配置BeanFactory,所以可以对BeanFactory进行配置,比如截图中的方法,可以设置我们前面提到的类型转换的东西,这样在生成Bean的时候就可以类型属性的类型转换了。

AutowireCapableBeanFactory

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

提供了自动装配Bean的实现、属性填充、初始化、处理获取依赖注入对象的功能。

比如@Autowired最终就会调用​​AutowireCapableBeanFactory#resolveDependency​​处理注入的依赖。

其实从这里也可以看出,Spring在BeanFactory的接口设计上面还是基于不同的职责进行接口的划分,其实不仅仅是在BeanFactory,前面提到的那些接口也基本符合这个原则。

2、BeanDefinition及其相关组件

BeanDefinition

BeanDefinition是Spring Bean创建环节中很重要的一个东西,它封装了Bean创建过程中所需要的元信息。

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
    //设置Bean className
    void setBeanClassName(@Nullable String beanClassName);

    //获取Bean className
    @Nullable
    String getBeanClassName();

    //设置是否是懒加载
    void setLazyInit(boolean lazyInit);

    //判断是否是懒加载
    boolean isLazyInit();

    //判断是否是单例
    boolean isSingleton();

}

如上代码是BeanDefinition接口的部分方法,从这方法的定义名称可以看出,一个Bean所创建过程中所需要的一些信息都可以从BeanDefinition中获取,比如这个Bean的class类型,这个Bean是否是懒加载,这个Bean是否是单例的等等,因为有了这些信息,Spring才知道要创建一个什么样的Bean。

读取BeanDefinition

读取BeanDefinition大致分为以下几类

  • BeanDefinitionReader
  • ClassPathBeanDefinitionScanner
BeanDefinitionReader

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

BeanDefinitionReader

BeanDefinitionReader可以通过​​loadBeanDefinitions(Resource resource)​​方法来加载BeanDefinition,方法参数就是我们前面说的资源,比如可以将Bean定义在xml文件中,这个xml文件就是一个资源

BeanDefinitionReader的相关实现:

  • XmlBeanDefinitionReader:读取xml配置的Bean
  • PropertiesBeanDefinitionReader:读取properties文件配置的Bean,是的,你没看错,Bean可以定义在properties文件配置中
  • AnnotatedBeanDefinitionReader:读取通过注解定义的Bean,比如@Lazy注解等等,AnnotatedBeanDefinitionReader不是BeanDefinitionReader的实现,但是作用是一样的
ClassPathBeanDefinitionScanner

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

这个作用就是扫描指定包下通过@Component及其派生注解(@Service等等)注解定义的Bean,其实就是@ComponentScan注解的底层实现

ClassPathBeanDefinitionScanner这个类其实在很多其它框架中都有使用到,因为这个类可以扫描指定包下,生成BeanDefinition,对于那些需要扫描包来生成BeanDefinition来说,用的很多

比如说常见的MyBatis框架,他的注解@MapperScan可以扫描指定包下的Mapper接口,其实他也是通过继承ClassPathBeanDefinitionScanner来扫描Mapper接口的

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

BeanDefinitionRegistry

这个从命名就可以看出,是BeanDefinition的注册中心,也就是用来保存BeanDefinition的。

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

提供了BeanDefinition的增删查的功能。

讲到这里,就可以用一张图来把前面提到东西关联起来

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

  • 通过BeanDefinitionReader或者是ClassPathBeanDefinitionScanner为每一个Bean生成一个BeanDefinition
  • BeanDefinition生成之后,添加到BeanDefinitionRegistry中
  • 当从BeanFactory中获取Bean时,会从BeanDefinitionRegistry中拿出需要创建的Bean对应的BeanDefinition,根据BeanDefinition的信息来生成Bean
  • 当生成的Bean是单例的时候,Spring会将Bean保存到SingletonBeanRegistry中,也就是平时说的三级缓存中的第一级缓存中,以免重复创建,需要使用的时候直接从SingletonBeanRegistry中查找

3、BeanFactory核心实现

前面提到的BeanFactory体系都是一个接口,那么BeanFactory的实现类是哪个类呢?

BeanFactory真正底层的实现类,其实就只有一个,那就是DefaultListableBeanFactory这个类,这个类以及父类真正实现了BeanFactory及其子接口的所有的功能。

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

并且接口的实现上可以看出,他也实现了BeanDefinitionRegistry,也就是说,在底层的实现上,其实BeanFactory跟BeanDefinitionRegistry的实现是同一个实现类。

上面说了这么多,来个demo

public class BeanFactoryDemo {

    public static void main(String[] args){
        //创建一个BeanFactory
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        //创建一个BeanDefinitionReader,构造参数是一个BeanDefinitionRegistry
        //因为DefaultListableBeanFactory实现了BeanDefinitionRegistry,所以直接把beanFactory当做构造参数传过去
        AnnotatedBeanDefinitionReader beanDefinitionReader = new AnnotatedBeanDefinitionReader(beanFactory);

        //读取当前类 BeanFactoryDemo 为一个Bean,让Spring帮我们生成这个Bean
        beanDefinitionReader.register(BeanFactoryDemo.class);

        //从容器中获取注册的BeanFactoryDemo的Bean
        BeanFactoryDemo beanFactoryDemo = beanFactory.getBean(BeanFactoryDemo.class);

        System.out.println("beanFactoryDemo = " + beanFactoryDemo);
    }

}

简单说一下上面代码的意思

  • 创建一个BeanFactory,就是DefaultListableBeanFactory
  • 创建一个AnnotatedBeanDefinitionReader,构造参数是一个BeanDefinitionRegistry,因为BeanDefinitionReader需要把读出来的BeanDefinition存到BeanDefinitionRegistry中,同时因为DefaultListableBeanFactory实现了BeanDefinitionRegistry,所以直接把beanFactory当做构造参数传过去
  • 读取当前类 BeanFactoryDemo 为一个Bean,让Spring帮我们生成这个Bean
  • 后面就是获取打印

运行结果

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

成功获取到我们注册的Bean

总结

本节主要讲了实现IOC的几个核心的组件

BeanFactory及其接口体系:

  • ListableBeanFactory
  • HierarchicalBeanFactory
  • ConfigurableBeanFactory
  • AutowireCapableBeanFactory

BeanDefinition及其相关组件:

  • BeanDefinition
  • BeanDefinitionReader和ClassPathBeanDefinitionScanner:读取资源,生成BeanDefinition
  • BeanDefinitionRegistry:存储BeanDefinition

BeanFactory核心实现:

  • DefaultListableBeanFactory:IOC容器,同时实现了BeanDefinitionRegistry接口

ApplicationContext

终于讲到了ApplicationContext,因为前面说的那么多其实就是为ApplicationContext做铺垫的

先来看看ApplicationContext的接口

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

你会惊讶地发现,ApplicationContext继承的几个接口,除了EnvironmentCapable和ApplicationEventPublisher之外,其余都是前面说的。

EnvironmentCapable这个接口比较简单,提供了获取Environment的功能

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

EnvironmentCapable

说明了可以从ApplicationContext中获取到Environment,所以EnvironmentCapable也算是前面说过了

至于ApplicationEventPublisher我们留到下一节说。

ApplicationContext也继承了ListableBeanFactory和HierarchicalBeanFactory,也就说明ApplicationContext其实他也是一个BeanFactory,所以说ApplicationContext是IOC容器的说法也没什么毛病,但是由于他还继承了其它接口,功能比BeanFactory多多了。

所以,ApplicationContext是一个集万千功能为一身的接口,一旦你获取到了ApplicationContext(可以@Autowired注入),你就可以用来获取Bean、加载资源、获取环境,还可以国际化一下,属实是个王炸。

虽然ApplicationContext继承了这些接口,但是ApplicationContext对于接口的实现是通过一种委派的方式,而真正的实现都是我们前面说的那些实现

什么叫委派呢,咱写一个例子你就知道了

public class MyApplicationContext implements ApplicationContext {

    private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

    @Override
    public Resource[] getResources(String locationPattern) throws IOException {
        return resourcePatternResolver.getResources(locationPattern);
    }

}

如上,其实是一段伪代码

因为ApplicationContext继承了ResourcePatternResolver接口,所以我实现了getResources方法,但是真正的实现其实是交给变量中的PathMatchingResourcePatternResolver来实现的,这其实就是委派,不直接实现,而是交给其它真正实现了这个接口的类来处理

同理,ApplicationContext对于BeanFactory接口的实现其实最终也是交由DefaultListableBeanFactory来委派处理的。

委派这种方式在Spring内部还是用的非常多的,前面提到的某些接口在的实现上也是通过委派的方式来的

ApplicationContext有一个子接口,ConfigurableApplicationContext

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

从提供的方法看出,就是可以对ApplicationContext进行配置,比如设置Environment,同时也能设置parent,说明了ApplicationContext也有子父的概念

我们已经看到了很多以Configurable开头的接口,这就是命名习惯,表示了可配置的意思,提供的都是set、add之类的方法

ApplicationContext的实现很多,但是他有一个非常重要的抽象实现AbstractApplicationContext,因为其它的实现都是继承这个抽象实现

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

AbstractApplicationContext

这个类主要是实现了一些继承的接口方法,通过委派的方式,比如对于BeanFactory接口的实现

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

并且AbstractApplicationContext这个类也实现了一个非常核心的refresh方法

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

所有的ApplicationContext在创建之后必须调用这个refresh方法之后才能使用,至于这个方法干了哪些事,后面有机会再写一篇文章来着重扒一扒。

事件

上一小节在说ApplicationContext继承的接口的时候,我们留下了一个悬念,那就是ApplicationEventPublisher的作用,而ApplicationEventPublisher就跟本节要说的事件有关。

Spring事件是一种观察者模式的实现,他的作用主要是用来解耦合的。

当发生了某件事,只要发布一个事件,对这个事件的监听者(观察者)就可以对事件进行响应或者处理。

举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于事件的模型来实现,只需要打119、救人监听火灾的发生就行了,当发生了火灾,通知这些打119、救人去触发相应的逻辑操作。

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

1、什么是Spring Event 事件

Spring Event 事件就是Spring实现了这种事件模型,你只需要基于Spring提供的API进行扩展,就可以轻易地完成事件的发布与订阅

Spring事件相关api主要有以下几个:

  • ApplicationEvent
  • ApplicationListener
  • ApplicationEventPublisher
ApplicationEvent

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

ApplicationEvent

事件的父类,所有具体的事件都得继承这个类,构造方法的参数是这个事件携带的参数,监听器就可以通过这个参数来进行一些业务操作。

ApplicationListener

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

ApplicationListener

事件监听的接口,泛型是需要监听的事件类型,子类需要实现onApplicationEvent,参数就是监听的事件类型,onApplicationEvent方法的实现就代表了对事件的处理,当事件发生时,Spring会回调onApplicationEvent方法的实现,传入发布的事件。

ApplicationEventPublisher

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

ApplicationEventPublisher

上一小节留下来的接口,事件发布器,通过publishEvent方法就可以发布一个事件,然后就可以触发监听这个事件的监听器的回调。

ApplicationContext继承了ApplicationEventPublisher,说明只要有ApplicationContext就可以来发布事件了。

话不多说,上代码

就以上面的火灾为例

创建一个火灾事件类

火灾事件类继承ApplicationEvent

// 火灾事件
public class FireEvent extends ApplicationEvent {

    public FireEvent(String source){
        super(source);
    }

}

创建火灾事件的监听器

打119的火灾事件的监听器:

public class Call119FireEventListener implements ApplicationListener<FireEvent> {

    @Override
    public void onApplicationEvent(FireEvent event){
        System.out.println("打119");
    }

}

救人的火灾事件的监听器:

public class SavePersonFireEventListener implements ApplicationListener<FireEvent> {

    @Override
    public void onApplicationEvent(FireEvent event){
        System.out.println("救人");
    }

}

事件和对应的监听都有了,接下来进行测试:

public class Application {

    public static void main(String[] args){
        //创建一个Spring容器
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        //将 事件监听器 注册到容器中
        applicationContext.register(Call119FireEventListener.class);
        applicationContext.register(SavePersonFireEventListener.class);
        applicationContext.refresh();

        // 发布着火的事件,触发监听
        applicationContext.publishEvent(new FireEvent("着火了"));
    }

}

将两个事件注册到Spring容器中,然后发布FireEvent事件

运行结果:

打119
救人

控制台打印出了结果,触发了监听。

如果现在需要对火灾进行救火,那么只需要去监听FireEvent,实现救火的逻辑,注入到Spring容器中,就可以了,其余的代码根本不用动。

2、Spring内置的事件

Spring内置的事件很多,这里我罗列几个

事件类型

触发时机

ContextRefreshedEvent

在调用ConfigurableApplicationContext 接口中的refresh()方法时触发

ContextStartedEvent

在调用ConfigurableApplicationContext的start()方法时触发

ContextStoppedEvent

在调用ConfigurableApplicationContext的stop()方法时触发

ContextClosedEvent

当ApplicationContext被关闭时触发该事件,也就是调用close()方法触发

在ApplicationContext(Spring容器)启动的过程中,Spring会发布这些事件,如果你需要这Spring容器启动的某个时刻进行什么操作,只需要监听对应的事件即可。

3、Spring事件的传播特性

Spring事件的传播是什么意思呢?

前面提到,ApplicationContext有子父容器的概念,而Spring事件的传播就是指当通过子容器发布一个事件之后,不仅可以触发在这个子容器的事件监听器,还可以触发在父容器的这个事件的监听器。

上代码
public class EventPropagateApplication {

    public static void main(String[] args){

        // 创建一个父容器
        AnnotationConfigApplicationContext parentApplicationContext = new AnnotationConfigApplicationContext();
        //将 打119监听器 注册到父容器中
        parentApplicationContext.register(Call119FireEventListener.class);
        parentApplicationContext.refresh();

        // 创建一个子容器
        AnnotationConfigApplicationContext childApplicationContext = new AnnotationConfigApplicationContext();
        //将 救人监听器 注册到子容器中
        childApplicationContext.register(SavePersonFireEventListener.class);
        childApplicationContext.refresh();

        // 设置一下父容器
        childApplicationContext.setParent(parentApplicationContext);

        // 通过子容器发布着火的事件,触发监听
        childApplicationContext.publishEvent(new FireEvent("着火了"));

    }

}

创建了两个容器,父容器注册了打119的监听器,子容器注册了救人的监听器,然后将子父容器通过setParent关联起来,最后通过子容器,发布了着火的事件。

运行结果:

救人
打119

从打印的日志,的确可以看出,虽然是子容器发布了着火的事件,但是父容器的监听器也成功监听了着火事件。

而这种传播特性,从源码中也可以看出来

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

事件传播源码

如果父容器不为空,就会通过父容器再发布一次事件。

传播特性的一个小坑

前面说过,在Spring容器启动的过程,会发布很多事件,如果你需要有相应的扩展,可以监听这些事件。

但是,不知道你有没有遇到过这么一个坑,就是在SpringCloud环境下,你监听这些Spring事件的监听器会执行很多次,这其实就是跟传播特性有关。

在SpringCloud环境下,为了使像FeignClient和RibbonClient这些不同服务的配置相互隔离,会为每个FeignClient或者是RibbonClient创建一个Spring容器,而这些容器都有一个公共的父容器,那就是SpringBoot项目启动时创建的容器

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

假设你监听了容器刷新的ContextRefreshedEvent事件,那么你自己写的监听器就在SpringBoot项目启动时创建的容器中

每个服务的配置容器他也是Spring容器,启动时也会发布ContextRefreshedEvent,那么由于传播特性的关系,你的事件监听器就会触发执行多次

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

如何解决这个坑呢?

你可以进行判断这些监听器有没有执行过,比如加一个判断的标志;或者是监听类似的事件,比如ApplicationStartedEvent事件,这种事件是在SpringBoot启动中发布的事件,而子容器不是SpringBoot,所以不会多次发这种事件,也就会只执行一次。

总结

到这到这整篇文章终于写完了,这里再来简单地回顾一下本文说的几个核心功能:

  • 资源管理:对资源进行统一的封装,方便资源读取和管理
  • 环境:对容器或者是项目的配置进行管理
  • 类型转换:将一种类型转换成另一种类型
  • 数据绑定:将数据跟对象的属性进行绑定,绑定之前涉及到类型转换
  • 泛型处理:一个操作泛型的工具类,Spring中到处可见
  • 国际化:对Java的国际化进行了统一的封装
  • BeanFactory:IOC容器
  • ApplicationContext:一个集万千功能于一身的王炸接口,也可以说是IOC容器
  • 事件:Spring提供的基于观察者模式实现的解耦合利器

当然除了上面,Spring还有很多其它核心功能,就比如AOP、SpEL表达式等等,由于AOP涉及到Bean生命周期,本篇文章也没有涉及到Bean生命周期的讲解,所以这里就不讲了,后面有机会再讲;至于SpEL他是Spring提供的表达式语言,主要是语法,解析语法的一些东西,这里也不讲了。

最后,我怕你文章看得过于入迷,所以再来重复一遍,如果本篇文章对你有所帮助,还请多多点赞、转发、在看,非常感谢!!

三万字盘点Spring 9大核心基础功能(下篇)-鸿蒙开发者社区

哦,真差点就忘了,本文所有demo代码都在这了

​https://github.com/sanyou3/spring-core-basic.git​

参考资料:

[1].《极客时间--小马哥讲Spring核心编程思想》

文章转载自公众号:三友的java日记

标签
已于2023-9-18 14:24:50修改
收藏
回复
举报
回复
    相关推荐