前言
在我们学习 spring 的时候,尤其是使用注解的方式实现自动装配,总会感到很神奇,也产生了很多的疑问。
- 注解是什么时候被解析的?
- 注解的生效逻辑是什么?
以@Autowired 为例,为什么加了这个直接就可以直接得到对应的属性实例,并调用它的方法呢?
我们越学感觉到疑问越多,而光看源码又容易被带偏,总是一个问题深入研究下去就找不到回来的路了。
本文试着从问题的角度出发,边看源码边解答这些疑问,这样思路清晰一些。
阅读本文时,我假定阅读者已经粗略的看过 springioc 和 springaop 相关的源码,有一定的基础,所以可能非关键步骤会略过。
解答
下面跟着一起来看看吧,大家打开自己的源码工程跟着一起做。
我们知道 spring 的启动配置有 3 中方式:xml 的方式、注解的方式、java 配置方式,其实主要分析 xml 和注解方式就可以了,他们在解析注解有一定的差异,我们分开分析。
我们主要分析 xml 方式和注解方式启动,解答之前我们先构建一下项目代码:
XML 配置文件的方式启动服务
注解是什么时候被解析的
测试启动类
public class MainStart {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("beans/beans2.xml");
}
}
定义几个 bean
// 使用注解方式注入
@Service(value = "ioc2")
public class IOCService2Impl implements IOCService {
@Autowired
@Qualifier(value = "iocService")
private IOCService iocService;
@Override
public String hollo() {
return "Hello,IOC";
}
}
// 使用配置文件的方式注入
public class IOCServiceImpl implements IOCService {
@Override
public String hollo() {
return "Hello,IOC";
}
}
// 接口
public interface IOCService {
public String hollo();
}
配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/tool"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tool http://www.springframework.org/schema/tool/spring-tool.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"
default-autowire="byName">
<!-- 手动注入bean -->
<bean class="top.ybq.ioc.beanlifecycle.xml2.IOCServiceImpl" id="iocService"/>
<!-- 开启扫包自动注入 -->
<context:component-scan base-package="top.ybq.ioc.beanlifecycle.xml2"/>
</beans>
我们在 org.springframework.context.support.AbstractApplicationContext#refresh
这个方法打个断点然后启动
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
我们看到第一张图 BeanFactory 是 null ,然后我们跳到下一步看第二张图,这里又有值了,说明 spring 在 xml 配置方式启动的时候,会在这个方法里去解析所有的注解。
obtainFreshBeanFactory()做了什么?
我们跟踪到了 AbstractRefreshableApplicationContext#refreshBeanFactory
@Override
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
// 关闭工厂
closeBeanFactory();
}
try {
/**
* 创建 DefaultListableBeanFactory 类型的 BeanFactory,目前还没有任何属性
* 这个类超级强大,所有关于容器的接口、抽象类它都继承了
*/
DefaultListableBeanFactory beanFactory = createBeanFactory();
/**
* 为容器设置一个序列化 id
*/
beanFactory.setSerializationId(getId());
/**
* 设置 BeanFactory的2个配置属性, 是否允许bean覆盖,是否允许循环引用
*/
customizeBeanFactory(beanFactory);
/**
* 加载我们的 bean 定义(最最最主要的作用就是保存我们的传递进去的配置类)
* 此方法是一个抽象方法,需要交给子类去实现,用于加载自定义的 BeanDefinition
* 其中 4 个子类:
* FileSystemXmlApplicationContext: 从文件系统加载配置类
* ClassPathXmlApplicationContext: 从 classpath 加载配置文件,子类 AbstractXmlApplicationContext 实现了此方法,一般用 xml 配置启动的,看这个类的实现
* AnnotationConfigWebApplicationContext: 以注解的方式加载配置的bean
* XmlWebApplictaionContext:
*/
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
看我的注释其他的方法大致做什么就知道了,我们去看AbstractXmlApplicationContext#loadBeanDefinitions
方法,跟进到XmlBeanDefinitionReader#loadBeanDefinitions
,然后XmlBeanDefinitionReader#doLoadBeanDefinitions
spring 框架中真正干活的方法都是 do、parse 等开头的,所以看到这几个前缀就知道它要干活了。
// 摘取部分代码
try {
// 使用 document 的方式读取配置文件信息
Document doc = doLoadDocument(inputSource, resource);
/**
* 依据 document 信息解析 bean,注册 BeanDefinition,关键位置
*/
int count = registerBeanDefinitions(doc, resource);
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " bean definitions from " + resource);
}
return count;
}
跟进之后
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 构建读取 document 的工具类
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 获取已经注册的 bean 的数量
int countBefore = getRegistry().getBeanDefinitionCount();
/**
* 注册BeanDefinition, 看 registerBeanDefinitions() 方法
*/
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 总注册 bean 数量减去之前注册的数量,就是本次注册的 bean 数量
return getRegistry().getBeanDefinitionCount() - countBefore;
}
再次看到了 do 方法DefaultBeanDefinitionDocumentReader#doRegisterBeanDefinitions
这个方法的第一部分的作用是:获取 <beans … profile="***" /> 中的 profile参数与当前环境是否匹配,如果不匹配则不再进行解析。
这个和环境变量相关,我们先不讨论,看后面的DefaultBeanDefinitionDocumentReader#parseBeanDefinitions
方法。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
/**
* 判定是否是默认节点,spring 默认的标签有且只有 4 个:<import/>、<alias>、<bean>、<beans>
* spring 只有 4 个默认标签,其他的标签都属于自定义标签,
* 比如:<tx:annotation-driven/>、注解扫描标签<context:component-scan/>,对应的是 注解 @Component @trans...
* 这些都是自定义节点
*/
if (delegate.isDefaultNamespace(root)) {
// 默认标签解析
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
/**
* 默认标签处理,我们不展开,留给读者自己去跟踪,如果按照demo 中的代码,这里就是解析 <bean> 节点
*/
parseDefaultElement(ele, delegate);
}
else {
/**
* 自定义标签的解析, 这是我们重点分析的
*/
delegate.parseCustomElement(ele);
}
}
}
}
else {
// 自定义标签解析
delegate.parseCustomElement(root);
}
}
老铁们,好像跟到这里有点苗头了,我们看到了和标签相关的说明。
跟进到这里,parse 开头的方法就是真正干活的苦力。
默认标签不是我们分析的重点,大家参考自定义标签的分析再去看看就好了,我们这里分析的是注解什么时候解析的,所以我们只关注自定义标签。
来,继续看自定义标签的解析
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
// 获取命名空间,
String namespaceUri = getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
}
// 通过 namespace 获取到 NamespaceHandler,重点看 resolve() 方法
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 进行解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
我们调试到这里断点发现
这个就是来处理我们自定义标签<context:component-scan base-package="top.ybq.ioc.beanlifecycle.xml2"/>
,同时我们得到了一个叫做命名空间的串:"http://www.springframework.org/schema/context”
和我们在 xml 文件中配置的xmlns:context="http://www.springframework.org/schema/context”
关联起来的。
那么这个 namespaceUri 有什么作用呢,我们跟着注释去看看 reslove 方法DefaultNamespaceHandlerResolver#resolve
public NamespaceHandler resolve(String namespaceUri) {
/**
* 获取所有一级配置的 handler ,getHandlerMappings():方法获取到所有命名空间对应的 uri,
* 比如 beans.xml 文件中配置包扫描的时候用到的自定义标签<context:component-scan xx>
* 它的对应的 uri = http://www.springframework.org/schema/context,
*