您的位置:首页 > 编程语言 > Java开发

Spring Ioc源码分析系列--Ioc的基础知识准备

2022-05-06 18:08 495 查看

Spring Ioc源码分析系列--Ioc的基础知识准备

本系列文章代码基于Spring Framework 5.2.x

Ioc的概念

在Spring里,Ioc的定义为The IoC Container,翻译过来也就是Ioc容器。为什么会被叫做容器呢?我们来比对一下日常生活中的容器,也就是那些瓶瓶罐罐。假设我们有个大米缸,里面提前放好了米,等我们需要米的时候,我们就可以到大米缸里面取。那么Ioc也是一样的道理,里面有一个容器

singletonObjects
(提前透露这里容器的类型是ConcurrentHashMap),里面放好了各种初始化好的
bean
,当我们代码需要使用的时候,就到里面去取。

借助一张图来看一下Spring Ioc的工作流程。整个过程就如同上面描述类似,把业务类

pojo
和一些元数据配置信息
Configuration Metadata
提供到Ioc,Ioc会根据你给的信息生成可以使用的
Bean
,这里生成的
bean
是可以直接使用的,Ioc是不是替我们省去了一大堆
new
的工作。当然这里面涉及非常多的细节,例如怎么获取元数据,怎么根据元数据生成想要的
bean
,这些都会在后续解答。

那么问题来了,为什么需要一个容器,我随手

new
个对象不香吗?要讨论这个问题,可以对比有容器和没有容器的区别,我个人认为有以下比较显著的优点

  • 方便管理。容器提供了一个集中化的管理,方便进行其他的操作,例如Aop相关的功能实现。无容器的无法集中管理
    bean
    ,所有
    bean
    散落到项目的各个角落,如果要进行一些额外的调整需要改动的点非常多。
  • 性能节省。容器只需初始化一次
    bean
    ,后续使用只需要直接获取。而无容器需要每次
    new
    对象,开销相比较而言肯定会更大。
  • 代码美观。容器屏蔽了复杂对象的构造过程,对于使用而言只需要直接去获取,无容器需要每次构造复杂对象,代码重复率非常高,想想你的项目里充满了各种
    new
    对象的代码,是不是就已经让你很头疼。

那么一个东西不可能只有优点而没有缺点,任何事物都需要辩证地去看待,那么提供容器后的缺点是什么?个人认为有如下比较显著的缺点

  • 并发安全。提供了一个集中式的容器管理,不可避免得在多线程情况下出现并发访问的情况,那么在保证线程安全的时候需要付出额外的性能开销。
  • 启动缓慢。同理,提供了一个集中式的容器管理,那么就需要在启动之初就把需要的各种
    bean
    初始化好,放入容器中,尽管这些
    bean
    不一定会被用到。如果没有指定初始化时机,那么这部分没有使用的
    bean
    也会在启动之初就进行初始化,这相比使用时再创建当然会消耗了额外的性能。
  • 内存隐患。由于对象都放在容器里,那么在有许多大对象或者对象的生命周期都非常的长的时候,需要考虑对象太多造成的内存开销。

这里简单分析了一下优缺点,当然这只是一家之言,有错漏补充欢迎指出。目前来看,Spring的优点远远大于其缺点,这也是Spring经久不衰的原因。

经过上面的介绍,我相信你已经对Ioc有个初步的整体认识。即这是一个容器,里面放好了可以使用的

bean
请牢记这个结论。那么接下来会介绍Ioc的一些知识体系,留下个整体轮廓就行,不涉及太多了源码分析。

BeanFactory 还是 ApplicationContext

本节说明

BeanFactory
ApplicationContext
容器级别之间的差异以及对使用Ioc的影响。 相信尝试看过Ioc源码的人都会被这两个迷惑过,
BeanFactory
ApplicationContext
提供的功能看起来似乎是类似的,那么这两个玩意有啥联系和区别呢?

我们通常推荐使用

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

因为

ApplicationContext
包含
BeanFactory
的所有功能,所以通常建议使用
ApplicationContext
,除非需要完全控制
bean
处理的场景。在
ApplicationContext
(例如
GenericApplicationContext
实现)中,按照约定(即按
bean
名称或按
bean
类型 —特别是后处理器)检测几种
bean
,而普通的
DefaultListableBeanFactory
不知道任何特殊的
bean

对于许多扩展容器特性,例如注解处理和 AOP 代理,

BeanPostProcessor
扩展点是必不可少的。如果你仅使用普通的
DefaultListableBeanFactory
,则默认情况下不会检测和激活此类后处理器。这种情况可能会令人困惑,因为您的
bean
配置实际上没有任何问题。相反,在这种情况下,需要通过额外的设置来完全引导容器。

下表列出了

BeanFactory
ApplicationContext
接口和实现提供的功能。

特性
BeanFactory
ApplicationContext
Bean实例化/注入 Yes Yes
集成的生命周期管理 No Yes
自动 BeanPostProcessor 注册 No Yes
自动 BeanFactoryPostProcessor 注册 No Yes
方便的 MessageSource 访问(用于国际化) No Yes
内置ApplicationEvent发布机制 No Yes

要使用

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

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 用 bean 定义填充工厂

// 现在注册任何需要的 BeanPostProcessor 实例
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// 现在开始使用工厂

要将

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

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// 从属性文件中引入一些属性值
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// 现在实际进行替换
cfg.postProcessBeanFactory(factory);

在这两种情况下,显式注册步骤都不方便,这就是为什么在 Spring 支持的应用程序中各种

ApplicationContext
变体优于普通
DefaultListableBeanFactory
的原因,尤其是在典型企业设置中依赖
BeanFactoryPostProcessor
BeanPostProcessor
实例来扩展容器功能时。

AnnotationConfigApplicationContext
注册了所有常见的注释后处理器,并且可以通过配置注释(例如
@EnableTransactionManagement
)在幕后引入额外的处理器。在 Spring 的基于注解的配置模型的抽象级别上,bean 后置处理器的概念变成了纯粹的内部容器细节。

Spring的统一资源加载策略

资源抽象Resource

在Spring里,

org.springframework.core.io.Resource
为 Spring 框架所有资源的抽象和访问接口,它继承
org.springframework.core.io.InputStreamSource
接口。作为所有资源的统一抽象,Resource 定义了一些通用的方法,由子类
AbstractResource
提供统一的默认实现。定义如下:

public interface Resource extends InputStreamSource {

/**
* 资源是否存在
*/
boolean exists();

/**
* 资源是否可读
*/
default boolean isReadable() {
return true;
}

/**
* 资源所代表的句柄是否被一个 stream 打开了
*/
default boolean isOpen() {
return false;
}

/**
* 是否为 File
*/
default boolean isFile() {
return false;
}

/**
* 返回资源的 URL 的句柄
*/
URL getURL() throws IOException;

/**
* 返回资源的 URI 的句柄
*/
URI getURI() throws IOException;

/**
* 返回资源的 File 的句柄
*/
File getFile() throws IOException;

/**
* 返回 ReadableByteChannel
*/
default ReadableByteChannel readableChannel() throws IOException {
return java.nio.channels.Channels.newChannel(getInputStream());
}

/**
* 资源内容的长度
*/
long contentLength() throws IOException;

/**
* 资源最后的修改时间
*/
long lastModified() throws IOException;

/**
* 根据资源的相对路径创建新资源
*/
Resource createRelative(String relativePath) throws IOException;

/**
* 资源的文件名
*/
@Nullable
String getFilename();

/**
* 资源的描述
*/
String getDescription();

}

子类结构如下:

从上图可以看到,Resource 根据资源的不同类型提供不同的具体实现,如下:

  • FileSystemResource :对
    java.io.File
    类型资源的封装,只要是跟 File 打交道的,基本上与 FileSystemResource 也可以打交道。支持文件和 URL 的形式,实现 WritableResource 接口,且从 Spring Framework 5.0 开始,FileSystemResource 使用 NIO2 API进行读/写交互。
  • ByteArrayResource :对字节数组提供的数据的封装。如果通过 InputStream 形式访问该类型的资源,该实现会根据字节数组的数据构造一个相应的 ByteArrayInputStream。
  • UrlResource :对
    java.net.URL
    类型资源的封装。内部委派 URL 进行具体的资源操作。
  • ClassPathResource :class path 类型资源的实现。使用给定的 ClassLoader 或者给定的 Class 来加载资源。
  • InputStreamResource :将给定的 InputStream 作为一种资源的 Resource 的实现类。

org.springframework.core.io.AbstractResource
,为 Resource 接口的默认抽象实现。它实现了 Resource 接口的大部分的公共实现

资源定位ResourceLoader

Spring 将资源的定义和资源的加载区分开了,Resource 定义了统一的资源,那资源的加载则由 ResourceLoader 来统一定义

org.springframework.core.io.ResourceLoader
为 Spring 资源加载的统一抽象,具体的资源加载则由相应的实现类来完成,所以我们可以将 ResourceLoader 称作为统一资源定位器。其定义如下:

/**
* 用于加载资源(例如类路径或文件系统资源)的策略接口。
* 需要 {@link org.springframework.context.ApplicationContext} 来提供此功能,
* 以及扩展的 {@link org.springframework.core.io.support.ResourcePatternResolver} 支持。
* <p>{@link DefaultResourceLoader} 是一个独立的实现,可以在 ApplicationContext 之外使用,也被 {@link ResourceEditor} 使用。
* <p>在 ApplicationContext 中运行时,可以使用特定上下文的资源加载策略从字符串中填充类型为 Resource 和 Resource 数组的 Bean 属性。
*
*/
public interface ResourceLoader {

/** Pseudo URL prefix for loading from the class path: "classpath:". */
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;

/**
* Return a Resource handle for the specified resource location.
* <p>The handle should always be a reusable resource descriptor,
* allowing for multiple {@link Resource#getInputStream()} calls.
* <p><ul>
* <li>Must support fully qualified URLs, e.g. "file:C:/test.dat".
* <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
* <li>Should support relative file paths, e.g. "WEB-INF/test.dat".
* (This will be implementation-specific, typically provided by an
* ApplicationContext implementation.)
* </ul>
* <p>Note that a Resource handle does not imply an existing resource;
* you need to invoke {@link Resource#exists} to check for existence.
* @param location the resource location
* @return a corresponding Resource handle (never {@code null})
* @see #CLASSPATH_URL_PREFIX
* @see Resource#exists()
* @see Resource#getInputStream()
*/
Resource getResource(String location);

/**
* Expose the ClassLoader used by this ResourceLoader.
* <p>Clients which need to access the ClassLoader directly can do so
* in a uniform manner with the ResourceLoader, rather than relying
* on the thread context ClassLoader.
* @return the ClassLoader
* (only {@code null} if even the system ClassLoader isn't accessible)
* @see org.springframework.util.ClassUtils#getDefaultClassLoader()
* @see org.springframework.util.ClassUtils#forName(String, ClassLoader)
*/
@Nullable
ClassLoader getClassLoader();

}
  • #getResource(String location)
    方法,根据所提供资源的路径 location 返回 Resource 实例,但是它不确保该 Resource 一定存在,需要调用
    Resource#exist()
    方法来判断。

  • 该方法支持以下模式的资源加载:

    URL位置资源,如
    "file:C:/test.dat"
  • ClassPath位置资源,如
    "classpath:test.dat
  • 相对路径资源,如
    "WEB-INF/test.dat"
    ,此时返回的Resource 实例,根据实现不同而不同。
  • 该方法的主要实现是在其子类

    DefaultResourceLoader
    中实现,具体过程我们在分析
    DefaultResourceLoader
    时做详细说明。

  • #getClassLoader()
    方法,返回
    ClassLoader
    实例,对于想要获取
    ResourceLoader
    使用的
    ClassLoader
    用户来说,可以直接调用该方法来获取。在分析
    Resource
    时,提到了一个类
    ClassPathResource
    ,这个类是可以根据指定的
    ClassLoader
    来加载资源的。

  • 子类结构如下:

    • DefaultResourceLoader
      AbstractResource
      相似,
      org.springframework.core.io.DefaultResourceLoader
      ResourceLoader
      的默认实现。

    • FileSystemResourceLoader
      继承
      DefaultResourceLoader
      ,且覆写了
      #getResourceByPath(String)
      方法,使之从文件系统加载资源并以
      FileSystemResource
      类型返回,这样我们就可以得到想要的资源类型。

    • ClassRelativeResourceLoader
      DefaultResourceLoader
      的另一个子类的实现。和
      FileSystemResourceLoader
      类似,在实现代码的结构上类似,也是覆写
      #getResourceByPath(String path)
      方法,并返回其对应的
      ClassRelativeContextResource
      的资源类型。

    • PathMatchingResourcePatternResolver
      ResourcePatternResolver
      最常用的子类,它除了支持
      ResourceLoader
      ResourcePatternResolver
      新增的
      classpath*:
      前缀外,还支持 Ant 风格的路径匹配模式(类似于
      "**/*.xml"
      )。

    至此 Spring 整个资源记载过程已经分析完毕。下面简要总结下:

    • Spring 提供了
      Resource
      ResourceLoader
      来统一抽象整个资源及其定位。使得资源与资源的定位有了一个更加清晰的界限,并且提供了合适的
      Default
      类,使得自定义实现更加方便和清晰。
    • AbstractResource 为 Resource 的默认抽象实现,它对
      Resource
      接口做了一个统一的实现,子类继承该类后只需要覆盖相应的方法即可,同时对于自定义的
      Resource
      我们也是继承该类。
    • DefaultResourceLoader
      同样也是
      ResourceLoader
      的默认实现,在自定
      ResourceLoader
      的时候我们除了可以继承该类外还可以实现
      ProtocolResolver
      接口来实现自定资源加载协议。
    • DefaultResourceLoader
      每次只能返回单一的资源,所以 Spring 针对这个提供了另外一个接口
      ResourcePatternResolver
      ,该接口提供了根据指定的
      locationPattern
      返回多个资源的策略。其子类
      PathMatchingResourcePatternResolver
      是一个集大成者的
      ResourceLoader
      ,因为它即实现了
      Resource getResource(String location)
      方法,也实现了
      Resource[] getResources(String locationPattern)
      方法。

    BeanFactory与ApplicationContext体系

    BeanFactory体系

    下面来介绍一下Ioc的核心实现有哪些重要的类,先看

    BeanFactory
    的体系,类结构如下,这里把
    spring-context
    部分的实现去掉了。

    可以看到里面的类还是比较多的,但是各司其职,每个类都有自己对应的职责,下面来介绍几个比较重点的类。

    • AutowireCapableBeanFactory
      接口提供了对现有
      bean
      进行自动装配的能力,设计目的不是为了用于一般的应用代码中,对于一般的应用代码应该使用
      BeanFactory
      ListableBeanFactory
      。其他框架的代码集成可以利用这个接口去装配和填充现有的bean的实例,但是Spring不会控制这些现有bean的生命周期。

    • ConfigurableBeanFactory
      提供了bean工厂的配置机制(除了BeanFactory接口中的bean的工厂的客户端方法)。该
      BeanFactory
      接口不适应一般的应用代码中,应该使用
      BeanFactory
      ListableBeanFactory
      。该扩展接口仅仅用于内部框架的使用,并且是对
      bean
      工厂配置方法的特殊访问。

    • ConfigurableListableBeanFactory
      接口继承自
      ListableBeanFactory
      ,
      AutowireCapableBeanFactory
      ,
      ConfigurableBeanFactory
      。大多数具有列出能力的bean工厂都应该实现此接口。此了这些接口的能力之外,该接口还提供了分析、修改bean的定义和单例的预先实例化的机制。这个接口不应该用于一般的客户端代码中,应该仅仅提供给内部框架使用。

    • AbstractBeanFactory
      继承自
      FactoryBeanRegistrySupport
      ,实现了
      ConfigurableBeanFactory
      接口。
      AbstractBeanFactory
      BeanFactory
      的抽象基础类实现,提供了完整的
      ConfigurableBeanFactory
      的能力。

      单例缓存
    • 别名的管理
    • FactoryBean
      的处理
    • 用于子
      bean
      定义的
      bean
      的合并
    • bean
      的摧毁接口
    • 自定义的摧毁方法
    • BeanFactory
      的继承管理
  • AbstractAutowireCapableBeanFactory
    继承自
    AbstractBeanFactory
    ,实现了
    AutowireCapableBeanFactory
    接口。该抽象了实现了默认的bean的创建。

      提供了
      bean
      的创建、属性填充、装配和初始化
    • 处理运行时
      bean
      的引用,解析管理的集合、调用初始化方法等
    • 支持构造器自动装配,根据类型来对属性进行装配,根据名字来对属性进行装配
  • DefaultListableBeanFactory
    继承自
    AbstractAutowireCapableBeanFactory
    ,实现了
    ConfigurableListableBeanFactory
    ,
    BeanDefinitionRegistry
    ,
    Serializable
    接口。这个类是一个非常完全的
    BeanFactory
    ,基于
    bean
    的定义元数据,通过后置处理器来提供可扩展性。

  • XmlBeanFactory
    继承自
    DefaultListableBeanFactory
    ,用来从
    xml
    文档中读取bean的定义的一个非常方便的类。最底层是委派给
    XmlBeanDefinitionReader
    ,实际上等价于带有
    XmlBeanDefinitionReader
    DefaultListableBeanFactory
    。 该类已经废弃,推荐使用的是
    DefaultListableBeanFactory

  • ApplicationContext体系

    接下来看看更高层次的容器实现

    ApplicationContext
    的体系。类结构图如下,这里只展示了常用的实现,并且去掉了大部分
    spring-web
    模块的实现类:

    • ConfigurableApplicationContext
      从上面的类的继承层次图能看到,
      ConfigurableApplicationContext
      是比较上层的一个接口,该接口也是比较重要的一个接口,几乎所有的应用上下文都实现了该接口。该接口在
      ApplicationContext
      的基础上提供了配置应用上下文的能力,此外提供了生命周期的控制能力。

    • AbstractApplicationContext
      ApplicationContext
      接口的抽象实现,这个抽象类仅仅是实现了公共的上下文特性。这个抽象类使用了模板方法设计模式,需要具体的实现类去实现这些抽象的方法。

    • GenericApplicationContext
      继承自
      AbstractApplicationContext
      ,是为通用目的设计的,它能加载各种配置文件,例如xml,properties等等。它的内部持有一个
      DefaultListableBeanFactory
      的实例,实现了
      BeanDefinitionRegistry
      接口,以便允许向其应用任何bean的定义的读取器。为了能够注册bean的定义,
      refresh()
      只允许调用一次。

    • AnnotationConfigApplicationContext
      继承自
      GenericApplicationContext
      ,提供了注解配置(例如:Configuration、Component、inject等)和类路径扫描(scan方法)的支持,可以使用
      register(Class... annotatedClasses)
      来注册一个一个的进行注册。实现了
      AnnotationConfigRegistry
      接口,来完成对注册配置的支持,只有两个方法:
      register()
      scan()
      。内部使用
      AnnotatedBeanDefinitionReader
      来完成注解配置的解析,使用
      ClassPathBeanDefinitionScanner
      来完成类路径下的bean定义的扫描。

    • AbstractXmlApplicationContext
      继承自
      AbstractRefreshableConfigApplicationContext
      ,用于描绘包含能被
      XmlBeanDefinitionReader
      所理解的bean定义的XML文档。子类只需要实现
      getConfigResources
      getConfigLocations
      来提供配置文件资源。

    • ClassPathXmlApplicationContext
      继承自
      AbstractXmlApplicationContext
      ,和
      FileSystemXmlApplicationContext
      类似,只不过
      ClassPathXmlApplicationContext
      是用于处理类路径下的
      xml
      配置文件。文件的路径可以是具体的文件路径,例如:
      xxx/application.xml
      ,也可以是ant风格的配置,例如:
      xxx/*-context.xml

    • AnnotationConfigWebApplicationContext
      继承自
      AbstractRefreshableWebApplicationContext
      ,接受注解的类作为输入(特殊的
      @Configuration
      注解类,一般的
      @Component
      注解类,与JSR-330兼容的
      javax.inject
      注解)。允许一个一个的注入,同样也能使用类路径扫描。对于web环境,基本上是和
      AnnotationConfigApplicationContext
      等价的。使用
      AnnotatedBeanDefinitionReader
      来对注解的bean进行处理,使用
      ClassPathBeanDefinitionScanner
      来对类路径下的bean进行扫描。

    小结

    这篇主要做了一些基础知识的准备,简单介绍了一些Ioc的概念,这里并没有举代码例子,只是通过生活中的容器去类比了一下Spring的容器。接下来对比分析了

    BeanFactory
    ApplicationContext
    区别与联系,然后介绍了Spring的资源加载,Spring的许多元数据加载通过统一资源加载的方式去获取的,特别是
    classpath
    路径下文件的获取。最后我们简单看了一下
    BeanFactory
    ApplicationContext
    的体系结构,展示常见的类图,并且有简单的描述,但是没有涉及太多的代码分析,主要也是混个眼熟。

    那么有了这些准备,下一篇,我们就会通过一个

    xml
    配置文件去加载配置,通过Spring容器获取我们需要的
    bean
    ,那么这就会用到这篇文章介绍过的资源加载,
    BeanFactory
    以及
    ApplicationContext
    体系里的类等等。

    那么下面的文章就会进行真正的源码分析了,庖丁解牛。

    如果有人看到这里,那在这里老话重提。与君共勉,路漫漫其修远兮,吾将上下而求索。

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: