Ioc的概念本系列文章代码基于Spring Framework 5.2.x
在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
容器级别之间的差异以及对使用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
要使用 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里, 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 接口的大部分的公共实现 。
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 实例,根据实现不同而不同。
- URL位置资源,如
-
该方法的主要实现是在其子类
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)
方法。
下面来介绍一下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
的体系。类结构图如下,这里只展示了常用的实现,并且去掉了大部分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
体系里的类等等。
那么下面的文章就会进行真正的源码分析了,庖丁解牛。
如果有人看到这里,那在这里老话重提。与君共勉,路漫漫其修远兮,吾将上下而求索。