本系列文章代码基于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
个对象不香吗?要讨论这个问题,可以对比有容器和没有容器的区别,我个人认为有以下比较显著的优点:
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 |
---|---|---|
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里, 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 根据资源的不同类型提供不同的具体实现,如下:
java.io.File
类型资源的封装,只要是跟 File 打交道的,基本上与 FileSystemResource 也可以打交道。支持文件和 URL 的形式,实现 WritableResource 接口,且从 Spring Framework 5.0 开始,FileSystemResource 使用 NIO2 API进行读/写交互。java.net.URL
类型资源的封装。内部委派 URL 进行具体的资源操作。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()
方法来判断。
该方法支持以下模式的资源加载:
"file:C:/test.dat"
。"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 整个资源记载过程已经分析完毕。下面简要总结下:
Resource
和 ResourceLoader
来统一抽象整个资源及其定位。使得资源与资源的定位有了一个更加清晰的界限,并且提供了合适的 Default
类,使得自定义实现更加方便和清晰。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
体系里的类等等。
那么下面的文章就会进行真正的源码分析了,庖丁解牛。
如果有人看到这里,那在这里老话重提。与君共勉,路漫漫其修远兮,吾将上下而求索。