什么是Spring容器
Spring 容器是 Spring 框架的核心,是用来管理对象的。容器将创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁。
2. Spring容器加载方式
Spring 容器的加载主要有 2 种(不包括SpringBoot),一种是xml的方式
ClassPathXmlApplicationContext ,一种是基于注解的方式
AnnotationConfigApplicationContext ,实际上他们的顶级接口都是 ApplicationContext 。
下面的用例主要就是基于XML的方式来启动Spring容器并查看Bean的创建和管理。
3. 创建项目
创建一个基本的 Maven 项目,结构如下:
3.1 pom.xml
我们导入 spring-context 即可,它依赖了其他组件,如下:
xml配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.javatv</groupId>
<artifactId>spring-xml</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<spring.version>5.2.8.RELEASE</spring.version>
</properties>
<dependencies>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
</dependency>
<!-- spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- lombok插件 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<!-- 日志相关依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
</project>
3.2 spring.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd"
default-lazy-init="false">
<bean id="SpringXml" class="cn.javatv.bean.SpringXml"/>
</beans>
3.3 实例Bean
import lombok.Data;
@Data
public class SpringXml {
private String info = "Hello Sprin Xml";
}
3.4 测试类
import cn.javatv.bean.SpringXml;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
@Test
public void test1() {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
SpringXml bean = applicationContext.getBean(SpringXml.class);
System.out.println(bean.getInfo());
}
}
测试输出:
4. 谈谈BeanDefinition
一般情况下,在一个 Spring 项目中,获取对象的方式有两种,一种是手动直接 new,另一种是交给 Spring 管理,Spring 将管理的对象称之为 Bean,容器会先实例化 Bean,然后自动注入,实例化的过程就需要依赖 BeanDefinition。
BeanDefinition 用于保存 Bean 的相关信息,包括属性、构造方法参数、依赖的 Bean 名称及是否单例、延迟加载等,它是实例化 Bean 的原材料,Spring 就是根据 BeanDefinition 中的信息实例化 Bean。
如果你从未了解过 Spring 源码,上面一段描述应该是无法理解的,但是我们大致可以知道它是用来描述 Bean 信息的,并且 Spring 可以通过它来创建实例 Bean。
另外,我不认为一上来详细的讲 BeanDefinition 的继承关系等等,你能有收获,大部分情况下还是一脸懵逼,我觉得最好的方式就是通过源码的方式一步一步的去了解它的组成关系或许更好。或者你可以看看这篇文章:BeanDefinition 解析
5. 源码分析
对于源码分析的入口主要通过方法的调用来分析,其中可能涉及到一些不理解的类或者对象,可先不必纠结,我们先过一遍整体流程,由大到小。(源码中标注红框的方法为主要方法)
5.1 加载配置文件入口
在代码中很明显可以看到通过
ClassPathXmlApplicationContext 去加载了spring.xml文件从而初始化容器,看看它的构造方法:
public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
this(new String[] {configLocation}, true, null);
}
其中的 this 方法:
public ClassPathXmlApplicationContext(
String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
throws BeansException {
super(parent);
//设置
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}
5.2 获取配置文件
setConfigLocations() 该方法的主要作用是获取配置文件位置信息。
public void setConfigLocations(@Nullable String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}
5.3 refresh()
该方法是 spring 容器初始化的核心方法。是 spring 容器初始化的核心流程,它读取 spring xml 配置文件,并创建和初始化 beans,所以该方法 非常重要 。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
/*
* 1、初始化做准备
* 设置ApplicationContext中的一些标志位,如closed设为false,active设为true。
* 校验添加了required标志的属性,如果他们为空,则抛出MissingRequiredPropertiesException异常。
*/
prepareRefresh();
// 2、创建 BeanFactory,解析xml文件
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3、给beanFactory设置一些属性值,如beanClassLoader
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization -
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
5.4 加载配置文件
在 refresh() 中通过 obtainFreshBeanFactory() 去解析 xml 文件,并且创建 BeanFactory。
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
refreshBeanFactory();
return getBeanFactory();
}
这一步主要看 refreshBeanFactory() 中的 loadBeanDefinitions() 。
protected final void refreshBeanFactory() throws BeansException {
//如果BeanFactory不为空,则清除BeanFactory和里面的实例
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
//BeanFactory 实例工厂
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
//设置是否可以循环依赖 allowCircularReferences
//是否允许使用相同名称重新注册不同的bean实现.
customizeBeanFactory(beanFactory);
//解析xml,并把xml中的标签封装成BeanDefinition对象
loadBeanDefinitions(beanFactory);
this.beanFactory = beanFactory;
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
5.5 创建 Xml 解析器
5.6 通过 Xml 解析器加载配置文件
需要主要的是,在方法的跳转的时候有很多重载的 loadBeanDefinitions() ,可能在跳转过程中就晕了,但实际上我们只需要关注 XmlBeanDefinitionReader 中的即可,因为它的父类是
AbstractBeanDefinitionReader ,通过 模板方法模式 调用子类的方法,
5.7 配置文件封装 Document 对象
Document 接口表示整个 HTML 或 XML 文档。从概念上讲,它是文档树的根,并提供对文档数据的基本访问。也就是说我们的 xml 配置文件转为了一个 Java 中的对象。
5.8 解析 Document 对象
把 Document 对象解析为具体的 Element,并解析具体的标签。
5.9 默认标签解析
Spring 默认标签有四种:import、alias、bean和beans,如在 xml 文件中定义的 bean,这里我们主要看 bean 标签的解析。
bean 标签的解析方法如下:
正如上图所标记的,第一次出现了 BeanDefinition 的概念,从名字可以看出来它是用来描述或者说是定义 Bean 的(当然,这并不是一句话就能简单描述的,后续会主要来讲解)。而 BeanDefinitionHolder 用来保存 BeanDefinition ,从源码中也可以看到:
然后,在去看对于 Bean 标签是如何解析的,我们在 xml 中一个标签是有很多属性的,如:
再去看看
parseBeanDefinitionElement() :
该方法并没有截取全部代码,我们要清楚一点,看源码并不是说每一行代码都需要弄清楚是干嘛的,比如上面方法我们知道它的返回对象为 BeanDefinitionHolder ,而 BeanDefinitionHolder 里面主要是包含了 BeanDefinition 等属性,所以我们要去看看这几个属性是创建的。
beanName 已经确定就是 Bean 标签 id 的值, aliases 为 name 的值,再去看看 BeanDefinition 是如何定义的。
进入
parseBeanDefinitionElement() :
可以看到 createBeanDefinition(className, parent) ,该方法实际就是用来创建 BeanDefinition 的,只不过是其子类 GenericBeanDefinition :
创建的 GenericBeanDefinition 对于标签的解析主要是以下标签并设置,由此我们可以得出一个结论:
Spring中每个 bean 实例,都有一个对应的 BeanDefinition 实例。这个 BeanDefinition 实例记录了该 bean 的各种属性信息,如 id、name 等等。
对于上述中具体标签的使用和源码分析可参考: bean标签的解析之简单子元素的解析
解析后封装的 BeanDefinitionHolder ,里 面 放 置 beanName 和 BeanDefinition 对象,然后对 BeanDefinition 对象进行缓存注册:
5.10 自定义标签解析
除了 Spring 默认的标签,还可以自定义标签,如最常使用的 context:component-scan ,并且通过改标签的命名空间来解析。
那么什么是命名空间?
在 XML 中,元素名称是由开发者定义的,当两个不同的文档使用相同的元素名时,就会发生命名冲突。
如 Spring 的 XML 配置文件,配置项都在 <beans> 标签中,但这时开发者如果自定义一个自己的标签,同样命名为 <beans> ,便造成了冲突,XML 解析器无法分辨这些冲突的命名。因此,对于 Spring 来说在最基本的配置中,含有 xmlns,xmlns:xsi,xsi:schemaLocation 三项,这三项是 Spring 最基本的命名空间,其含义如下:
- xmlns 表示是该 XML 文件的默认命名空间;
- xmlns:xsi 表示该 XML 文件遵守 xml 规范;
- xsi:schemaLocation 表示具体用到的 schema 资源。
如下配置文件:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd"
default-lazy-init="false">
<bean id="SpringXml" class="net.javatv.bean.SpringXml"/>
<context:component-scan base-package="net.javatv.custom"/>
</beans>
自定义类
import lombok.Data;
import org.springframework.stereotype.Component;
@Data
@Component
public class CustomBean {
private String msg = "11";
}
然后再去看自定义标签的解析,在
DefaultBeanDefinitionDocumentReader#parseBeanDefinitions() 中的 parseCustomElement() :
这个方法中,会看到有一个 NamespaceHandler,这就是用来解析xml中具体标签的一个 handler 类,是一个接口,具体的解析操作在自定义个性化标签时需要自己实现,如下:
这个handler对象的创建会去调用名称空间解析器的 resolve 方法得到,而这个 resolver 在前面创建 ReaderContext的时候创建的,默认类型为:
DefaultNamespaceHandlerResolver,点开 resolve 方法如下:
这个方法中,会先去实例化 NamespaceHandler,实例化的过程是通过反射,反射所需要的class是通过配置文件配置的,NamespaceHandler 对象通过 SPI 机制获取 spring 中所有 jar 包里面的 META-INF/spring.handlers 文件,并且建立映射关系 类似于如下图所示:
也就是说,如果我们自定义一个标签的话也就是通过上面的方式,在自己的项目 META-INF/spring.handlers 的结构中添加配置,当然,这在 Spring Boot 中一个注解就能解决。
在实例化 NamespaceHandler 之后,再去加载具体的用于解析自定义标签的 Parser 类,也就是代码中的 init() 方法:
到此,都是对一些类进行了初始化,还没有进行真正的解析操作,在回头看
BeanDefinitionParserDelegate#parseCustomElement() ,当 NamespaceHandler 对象通过标签 component-scan 拿到对应的解析器:
在拿到
ComponentScanBeanDefinitionParser#parse() 解析器后开始解析:
这个方法中就是最最后真正解析标签中的属性,并注册bean定义的方法。此处有一个细节:
问:上述方法中的第二行中,调用了一个resolvePlacehodler方法,第一步已经得到了base-package的属性了,此处为啥还要再解析一次呢?
答:因为 base-package 的值支持占位符,即:<context:component-scan base-package="${javatv.path}"/>,所以需要解析出真正的包路径。
在 configureScanner() 方法中,这个方法同自定义标签中的方法,就是解析每个节点的值,然后组装成一个scanner 对象,并返回。然后调用 doScan() 方法,这个方法就是根据解析到的 xml 中的各种属性以及内部 bean 定义,执行具体的 BeanDefinition 的定义,即:把真正的 BeanDefinition 注册到 bean 定义注册中心,代码如下:
通过层层递归扫描 base-package 下的包,先扫描出 classpath:/base-package 以 .class 结尾的所有文件,然后再根据过滤器扫描出具有 @Service 和 @Component 注解的类添加到对应的集合 Set<BeanDefinition> 完成 BeanDefinition 的注册。
先看看 findCandidateComponents() 中的 scanCandidateComponents() 即可看到 Bean 的定义过程 :
值得注意的是,在定义之前,在方法 isCandidateComponent() 中会判断扫描出来的类是否满足条件:
该方法涉及到 component-scan 的属性:
- useDefaultFilters :默认为 true,此时Spring扫描类时发现如果其被标注为@Component、@Repository、@Service、@Controller则自动实例化为bean并将其添加到上下文中,如果设置为false,即使将其标注为@Component或者其他,Spring都会忽略。
- includeFilters :指定扫描时需要实例化的类型,我们可以从名字看到这是一个Filter,你可以自己定义该Filter,Spring为我们提供了一套方便的实现,我们可以根据标注、类、包等相关信息决定当扫描到该类时是否需要实例化该类,需要注意的是如果你仅仅想扫描如@Controller不仅要加includeFilters,还需要将useDefaultFilters 设置为false。
- excludeFilter ,指定扫描到某个类时需要忽略它,实现和上一个Filter一样,区别只是如果Filter匹配,Spring会忽略该类
因此,Spring每扫描一个类,都会经过 includeFilters 以及 excludeFilters,如果某个Filter匹配,就执行相应的操作(实例化或者忽略)。而在源码中也有体现,即 configureScanner() 创建的
ClassPathBeanDefinitionScanner ,其构造方法如下:
进入 registerDefaultFilters() 方法:
可以看到源码中是把有 @Component 注解的类添加到 includeFilters ,那么和其他的注解有什么关系呢?比如 @Controller 并没有添加进去为什么也能加载?
当然,这个问题很好回答,我们到 @Component 的包下可以看到:
即 @Controller 继承自 @Component ,其他几个也相同,这里出现了一个在开发中基本没使用过的标签 @AliasFor ,其中之一的作用就是可以表示继承关系。
6. 总结
通过对源码的分析可以知道,定义的标签要生效,大的过程就是解析标签,然后生成对应的 BeanDefinition,然后放入到 BeanDefinition注册中心,这里只讲到了xml标签的解析,而对于Bean的真正实例化还是在 refresh() 中,后续文章中在去细讲。