·

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

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,这些都会在后续解答。

1651805989772

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

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

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

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

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

经过上面的介绍,我相信你已经对Ioc有个初步的整体认识。即这是一个容器,里面放好了可以使用的bean请牢记这个结论。那么接下来会介绍Ioc的一些知识体系,留下个整体轮廓就行,不涉及太多了源码分析。

BeanFactory 还是 ApplicationContext

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

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

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

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

下表列出了 BeanFactoryApplicationContext 接口和实现提供的功能。

特性 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 的原因,尤其是在典型企业设置中依赖 BeanFactoryPostProcessorBeanPostProcessor 实例来扩展容器功能时。

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();

}

子类结构如下:

1651819272690

从上图可以看到,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 来加载资源的。

子类结构如下:

1651820093602

  • DefaultResourceLoaderAbstractResource 相似,org.springframework.core.io.DefaultResourceLoaderResourceLoader 的默认实现。

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

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

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

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

  • Spring 提供了 ResourceResourceLoader 来统一抽象整个资源及其定位。使得资源与资源的定位有了一个更加清晰的界限,并且提供了合适的 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部分的实现去掉了。

1651822615122

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

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

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

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

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

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

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

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

ApplicationContext体系

接下来看看更高层次的容器实现ApplicationContext的体系。类结构图如下,这里只展示了常用的实现,并且去掉了大部分spring-web模块的实现类:

1651824404738

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

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

  • 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文档。子类只需要实现getConfigResourcesgetConfigLocations来提供配置文件资源。

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

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

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

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

posted @ 2022-05-06 18:08  Codegitz  阅读(802)  评论(0编辑  收藏  举报