Spring源码解读(1)-IOC容器BeanDefinition的加载
一、IOC容器BeanDefinition的加载
1、概述
spring的两大核心:IOC(依赖注入)和AOP(面向切面),IOC本质上就是一个线程安全的hashMap,put和get方法就对应IOC容器的bean的注册和获取,spring通过读取xml或者使用注解配置的类生成一个BeanDefinition
放入到容器中,获取的时候通过BeanDefinition
的配置通过asm、反射等技术完成属性的注入最终获取一个bean,获取bean的方法就getBean()
,我们无需关心实现细节,直接按照spring提供的注解或者xml配置方式使用即可。
2、IOC容器
虽然IOC本质上是一个线程安全的hashMap,使用时直接通过getBean()
获取(@Autowired本质也是通过getBean()
获取),这样在使用bean实例的时候,就不用关心bean的创建,只管用就行了,IOC会在程序启动时,自动将依赖的对象注入到目标对象中,非常简单,省心。但是如果不了解IOC中bean的注册和获取原理,当使用Spring无法获取一个bean的时候,针对抛出的异常可能一头雾水。
IOC容器的实现包含了两个非常重要的过程:
- xml的读取生成
BeanDefinition
注册到IOC中 - 通过
BeanDefinition
实例化bean,并从IOC中通过getBean()
获取
//spring源码中将一个bean的BeanDefinition放入到IOC中
this.beanDefinitionMap.put(beanName, beanDefinition);
复制代码
//Spring源码中通过beanName获取一个bean
public Object getBean(String name) throws BeansException {assertBeanFactoryActive();return getBeanFactory().getBean(name);}
复制代码
这两个过程还对应了两个非常核心的接口:
BeanDefinitionRegistry和BeanFactory,一个向IOC中注册BeanDefinition
,一个从IOC获取Bean实例对象。
读IOC源码必须从了解BeanDefinition
开始,BeanDefinition
是一个接口,无论是通过xml声明还是通过注解定义一个bean实例,在IOC容器中第一步总是为其对应生成一个BeanDefinition
,里面包含了类的所有基本信息。
其中AbstractBeanDefinition
实现BeanDefinition
,这个接口有两个子接口GenericBeanDefinition
和RootBeanDefinition
,再来看看BeanDefinition中的一些方法
-getBeanClassName()
获取bean的全限定名,
getScope()
获取该类的作用域,isLazyInit()
该类是否为懒加载,getPropertyValues()
获取配置的属性值列表(用于setter注入),getConstructorArgumentValues()获取配置的构造函数值(用于构造器注入)等bean的重要信息,如果通过注解的方式,还会包含一些注解的属性信息,
总而言之 ,BeanDefinition
包含了我们定义的一个类的所有信息,然后通过 BeanDefinitionRegistry
接口的registerBeanDefinition
注册到IOC容器中,最后通过BeanDefinition
结合asm,反射等相关技术,通过BeanFactory
接口的getBean()获取一个实例对象好像也不是什么困难的事情了。当然这只是表层的大致原理,实际上spring在实现IOC的时候,用了大量的设计模式,比如:单例模式、模板方法、工厂模式、代理模式(AOP基本上全是)等,此外面向对象的基本原则中的单一职责、开放封闭原则等随处可见,具体的源码解读还是在之后的笔记里介绍。
3、读取xml配置文件
读取源码需要通过调试去看,Spring启动时首先会读取xml配置文件,xml文件可以从当前类路径下读,也可以从文件系统下读取,以下是用于调试的简单案例:
@Testpublic void testSpringLoad() {ClassPathXmlApplicationContext application = new ClassPathXmlApplicationContext("spring/spring-context.xml");BankPayService bankPayService = (BankPayService) application.getBean("bankPayService");Assert.assertNotNull(bankPayService);}
复制代码
xml配置:
<?xml version="1.0" encoding="UTF-8"?>
<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"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"default-lazy-init="true"><bean id="bankPayService" class="com.yms.manager.serviceImpl.BankPayServiceImpl"/><context:property-placeholder location="classpath*:app-env.properties"/><context:component-scan base-package="com.yms.market"/>
</beans>
复制代码
执行测试案例,肯定是成功的,打断点开始调试,首先会进入ClassPathXmlApplicationContext
的构造函数中:
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent){super(parent);//获取当前环境 装饰传入的路径setConfigLocations(configLocations);if (refresh) {//程序入口refresh();}}
复制代码
构造函数中最关键的部分是refresh()方法,该方法用于刷新IOC容器数据,该方法由AbstractApplicationContext
实现。
@Overridepublic void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// Prepare this context for refreshing.prepareRefresh();// 让子类去刷新beanFactory 进入这里查看ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.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) {// Destroy already created singletons to avoid dangling resources.destroyBeans();// Reset 'active' flag.cancelRefresh(ex);// Propagate exception to caller.throw ex;}}}
复制代码
在refresh
方法中主要完成了加载xml文件的环境配置、xml文件读取,注册BeanFactoryPostProcessor
处理器、注册监听器等工作,其中比较核心的是第二行代码ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory()
;
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {//加载xml配置文件,生成BeanDefinition并注册到IOC容器中 refreshBeanFactory();//获取加载完xml文件之后的beanFactory对象ConfigurableListableBeanFactory beanFactory = getBeanFactory();if (logger.isDebugEnabled()) {logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);}return beanFactory;}
复制代码
refreshBeanFactory和getBeanFactory都是由AbstractApplicationContext
的子类AbstractRefreshableApplicationContext
实现的,
@Overrideprotected final void refreshBeanFactory() throws BeansException {//如果beanFactory不为空 ,清除老的beanFactoryif (hasBeanFactory()) {destroyBeans();closeBeanFactory();}try {//创建一个beanFactoryDefaultListableBeanFactory beanFactory = createBeanFactory();beanFactory.setSerializationId(getId());//设置bean是否允许覆盖 是否允许循环依赖customizeBeanFactory(beanFactory);//加载beans声明,即读取xml或者扫描包 生成BeanDefinition注册到IOCloadBeanDefinitions(beanFactory);synchronized (this.beanFactoryMonitor) {this.beanFactory = beanFactory;}}catch (IOException ex) {throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);}}
复制代码
在Spring中DefaultListableBeanFactory
是一个非常重要的类,它实现了BeanDefinitionRegistry
和BeanFactory
接口,并且完成了这两个接口的具体实现,DefaultListableBeanFactory
的类图如下:
我们已经知道BeanDefinitionRegistry
完成了BeanDefinition
的注册,BeanFactory
完成了getBean()
中bean的创建,其中xml读取和bean的 注册的入口就是loadBeanDefinitions(beanFactory)
这个方法,loadBeanDefinitions
是一个抽象方法,由类AbstractXmlApplicationContext
实现。loadBeanDefinitions
在AbstractXmlApplicationContext
有很多个重载方法,在不通阶段方法使用的参数值不同,接下来看看各个loadBeanDefinitions
的调用顺序:
创建XmlBeanDefinitionReader
对象
@Overrideprotected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {//Spring将读取xml操作委托给了XmlBeanDefinitionReader对象//并且传入DefaultListableBeanFactory将生成的beandefinition注册到IOC中XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);//设置Spring中bean的环境beanDefinitionReader.setEnvironment(this.getEnvironment());beanDefinitionReader.setResourceLoader(this);beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));// 设置beanDefinitionReader验证属性,子类可以重写该方法使用自定义reader对象initBeanDefinitionReader(beanDefinitionReader);//通过beanDefinitionReader读取xmlloadBeanDefinitions(beanDefinitionReader);}
复制代码
Spring对于读取xml文件。并不是由DefaultListableBeanFactory
亲力亲为,而是委托给了XmlBeanDefinitionReader
,在该类内部会将xml配置文件转换成Resource,Spring封装了xml文件获取方式,我们使用ClassPathXmlApplicationContext
读取xml,因此Spring会通过ClassLoader获取当前项目工作目录,并在该目录下查找spring-context.xml文件,当然我们还可以使用FileSystemXmlApplicationContext
从文件系统上以绝对路径的方式读取文件
继续查看第二个loadBeanDefinitions(beanDefinitionReader)
:
获取配置文件路径集合
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {//默认返回为空 子类可以实现该方法 读取指定文件Resource[] configResources = getConfigResources();if (configResources != null) {reader.loadBeanDefinitions(configResources);}//获取我们配置的xml文件路径集合String[] configLocations = getConfigLocations();if (configLocations != null) {reader.loadBeanDefinitions(configLocations);}}
复制代码
在这个方法中,最终获取到了我们通过ClassPathXmlApplicationContext
对象传进来的xml配置文件路径,然后由进入委托对象XmlBeanDefinitionReader
的loadBeanDefinitions
方法中:
@Overridepublic int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {Assert.notNull(locations, "Location array must not be null");int counter = 0;for (String location : locations) {//循环读取配置的所有配置文件路径counter += loadBeanDefinitions(location);}//返回此次加载的BeanDefinition个数return counter;}
复制代码
在XmlBeanDefinitionReader
中,会循环读取配置的所有配置文件路径,并将读取到的bean的声明创建成BeanDefinition
,并将此次生成的数量返回,继续查看loadBeanDefinitions
:
public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {ResourceLoader resourceLoader = getResourceLoader();if (resourceLoader == null) {throw new BeanDefinitionStoreException("Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");}if (resourceLoader instanceof ResourcePatternResolver) {// Resource pattern matching available.try {Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);//实际执行到这里int loadCount = loadBeanDefinitions(resources);if (actualResources != null) {for (Resource resource : resources) {actualResources.add(resource);}}if (logger.isDebugEnabled()) {logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");}return loadCount;}catch (IOException ex) {throw new BeanDefinitionStoreException("Could not resolve bean definition resource pattern [" + location + "]", ex);}}else {// Can only load single resources by absolute URL.Resource resource = resourceLoader.getResource(location);int loadCount = loadBeanDefinitions(resource);if (actualResources != null) {actualResources.add(resource);}if (logger.isDebugEnabled()) {logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");}return loadCount;}}
复制代码
这个方法就是上面所说的Spring将xml配置文件封装成Resourse,最终获取到Resourse的过程,这部分代码没什么好看的,就是找到ClassPathResource
将xml路径放进去,然后调用loadBeanDefinitions(resources)
,再来看这个方法:
@Overridepublic int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {return loadBeanDefinitions(new EncodedResource(resource));}
复制代码
这个方法将Resource封装城了EncodedResource
对象,这个对象有一个属性encoding,如果设置了xml文件的编码,在这里读取xml文件的时候会根据该编码进行读取,继续往下看:
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {Assert.notNull(encodedResource, "EncodedResource must not be null");if (logger.isInfoEnabled()) {logger.info("Loading XML bean definitions from " + encodedResource.getResource());}//获取前面加载到的xml配置资源文件Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();if (currentResources == null) {currentResources = new HashSet<EncodedResource>(4);this.resourcesCurrentlyBeingLoaded.set(currentResources);}if (!currentResources.add(encodedResource)) {throw new BeanDefinitionStoreException("Detected cyclic loading of " + encodedResource + " - check your import definitions!");}try {//之所以将路径都封装到Resource里面,就是使其提供一个统一的getInputStream方法//获取文件流对象,XmlBeanDefinitionReader无需关心xml文件怎么来的InputStream inputStream = encodedResource.getResource().getInputStream();try {InputSource inputSource = new InputSource(inputStream);if (encodedResource.getEncoding() != null) {inputSource.setEncoding(encodedResource.getEncoding());}//终于来到了最最核心的方法 解析文件流return doLoadBeanDefinitions(inputSource, encodedResource.getResource());}finally {inputStream.close();}}catch (IOException ex) {throw new BeanDefinitionStoreException("IOException parsing XML document from " + encodedResource.getResource(), ex);}finally {currentResources.remove(encodedResource);if (currentResources.isEmpty()) {this.resourcesCurrentlyBeingLoaded.remove();}}}
复制代码
spring的代码风格好像有一个特点,凡是真正开始做事的方法入口都会以do为前缀,经过前面一系列对xml配置文件的设置,终于来到了doLoadBeanDefinitions(inputSource, encodedResource.getResource())
,在这个方法里Spring会读取每一个Element标签,并根据命名空间找到对应的NameSpaceHandler
去读取解析Node生成BeanDefinition
对象。经过一系列操作Resouse最终会被转换成InputSource
对象,这个类也没什么特别的,只是除了文件流之外多了一些参数而已,比如XSD,DTD的publicId,systemId约束,文件流的编码等,最重要的还是InputStream
,然后来看看这个方法:
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) {//主要是校验文件 通过查找DTD文件约束 校验文件格式//读取xml文件生成DOCDocument doc = doLoadDocument(inputSource, resource);return registerBeanDefinitions(doc, resource);}
复制代码
这个方法完成两件事情:
通过
inputSource
生成Document
对象解析
Document
并将生成BeanDefinition
注册到IOC中
4、解析DOC生成BeanDefinition
文件校验和生成DOC文档都是一些校验操作,如果想自定义DTD文档让Spring加载,后面还会细说这部分内容,暂且放下,现在主要是看看IOC的BeanDefinition
的生成过程,接下来进入registerBeanDefinitions
:
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();documentReader.setEnvironment(this.getEnvironment());//获取加载之前IOC容器中的BeanDefinition数量int countBefore = getRegistry().getBeanDefinitionCount();//具体解析 注册 documentReader.registerBeanDefinitions(doc, createReaderContext(resource));//返回本次加载的BeanDefinition数量return getRegistry().getBeanDefinitionCount() - countBefore;}
复制代码
在这个方法里,首先创建BeanDefinitionDocumentReader
,这是个接口用于完成BeanDefinition
向IOC容器注册的功能,Spring只提供了唯一的实现DefaultBeanDefinitionDocumentReader
,查看registerBeanDefinitions
:
@Overridepublic void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {this.readerContext = readerContext;logger.debug("Loading bean definitions");//root 在这个测试里就是<beans></beans>Element root = doc.getDocumentElement();doRegisterBeanDefinitions(root);}
复制代码
该方法第一步首先回去xml的根节点,在这个测试xml里就是标签了,然后将根节点作为参数传入到下面的方法中解析doRegisterBeanDefinitions
:
protected void doRegisterBeanDefinitions(Element root) {//获取profile环境变量String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);if (StringUtils.hasText(profileSpec)) {Assert.state(this.environment != null, "Environment must be set for evaluating profiles");String[] specifiedProfiles = StringUtils.tokenizeToStringArray(profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);//判断该root下的bean是否是前面通过web.xml或者前面设置的bean的环境值//如果不是 不需要解析当前root标签if (!this.environment.acceptsProfiles(specifiedProfiles)) {return;}}BeanDefinitionParserDelegate parent = this.delegate;this.delegate = createDelegate(this.readerContext, root, parent);//解析bean所要执行的方法,该方法实际是空的 允许子类扩展 去读取自定义的node//属于模板方法preProcessXml(root);//真正解析beans的方法parseBeanDefinitions(root, this.delegate);//beans解析完之后需要执行的方法,实际也是通过子类扩展 是模板方法postProcessXml(root);this.delegate = parent;}
复制代码
这个方法看起来很多,其实真正核心的只有两部分:
读取beans的profile属性,判断是否属于被激活的组,如果不是则不解析
创建
BeanDefinitionParserDelegate
,委托该类执行beans解析工作。
最后通过parseBeanDefinitions(root, this.delegate)
方法将beans的解析交给BeanDefinitionParserDelegate
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {//判断是否是默认的命名空间 也就是beansif (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)) {//判断是否是xml配置的bean,如果是则调用该方法解析parseDefaultElement(ele, delegate);}else {//否则按照自定义方式解析delegate.parseCustomElement(ele);}}}}else {//否则按照自定义方式解析delegate.parseCustomElement(root);}}
复制代码
这个方法很重要,这里已经开始解析<beans>
了,首先会判断,要解析的root是否是beans标签,如果是再判断子元素是否是<bean>
元素,正常来讲,我们使用spring的时候都会再<beans>
标签下配置,所以不出意外都会走到for循环里,然后在for循环里判断是否是默认命名空间的时候就会发生变化:
如果是则走parseDefaultElement(ele, delegate);
如果是
<mvc:annotation-driven>、 <context:component-scan base-package="***"/>
等则会走到自定义元素解析delegate.parseCustomElement(ele)里
自定义解析加载到最后还是会跟加载默认命名空间的bean一样,所以在这里只分析自定义命名空间的解析,不过值得提一下的是自定义解析方法里会首先根据Element的命名空间找到NamespaceHandler
,然后由该NamespaceHanler
去解析
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {//获取元素的命名空间String namespaceUri = getNamespaceURI(ele);//获取命名空间解析器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));}
复制代码
由于后面会自己实现一个NamespaceHandler
解析自定义的标签,会专门说明Spring如何查找NamespaceHandler
以及如何解析自定义元素,这里只是了解下NamespaceHandler
的概念即可,接着看Spring解析<Beans>
查看parseDefaultElement:
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {//解析 importimportBeanDefinitionResource(ele);}else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {//解析aliasprocessAliasRegistration(ele);}else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {//解析beanprocessBeanDefinition(ele, delegate);}else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {// 如果还是beans 递归调用doRegisterBeanDefinitions(ele);}}
复制代码
这个方法里面就是几个if判断,用于解析对应的标签,其中import alias相当于是去读取另一个xml文件,最后还是会调用解析bean,所以在这里只看解析bean的方法processBeanDefinition(ele, delegate)
:
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {//将bean的属性都读取到到BeanDefinitionHolder上BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);if (bdHolder != null) {//如果bean里面有自定义标签 来决定是否再次解析bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);try {// 将生成的BeanDefinitionHolder注册到IOC中BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());}catch (BeanDefinitionStoreException ex) {getReaderContext().error("Failed to register bean definition with name '" +bdHolder.getBeanName() + "'", ele, ex);}// 发送注册事件getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));}}
复制代码
在这个方法里 ,之前分析的逻辑才逐渐清晰起来,代码的条例也很清晰
BeanDefinitionParserDelegate
将bean标签的属性读取到BeanDefinitionHolder
对象中如果beans下还有其他自定义标签决定是否有必要再次解析
将
BeanDefinition
注册到IOC中发送注册事件
首先来看第一步,读取node属性到BeanDefinitionParserDelegate
中
public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, BeanDefinition containingBean) {this.parseState.push(new BeanEntry(beanName));String className = null;if (ele.hasAttribute(CLASS_ATTRIBUTE)) {//获取class全限定名className = ele.getAttribute(CLASS_ATTRIBUTE).trim();}try {String parent = null;if (ele.hasAttribute(PARENT_ATTRIBUTE)) {parent = ele.getAttribute(PARENT_ATTRIBUTE);}//设置beanClass或者beanClassNameAbstractBeanDefinition bd = createBeanDefinition(className, parent);//读取node属性 将配置的属性 塞入合适的字段中parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));parseMetaElements(ele, bd);//记录lookup-method配置parseLookupOverrideSubElements(ele, bd.getMethodOverrides());//记录replaced-method配置parseReplacedMethodSubElements(ele, bd.getMethodOverrides());//解析构造函数(构造器注入)parseConstructorArgElements(ele, bd);//解析属性(setter注入)parsePropertyElements(ele, bd);parseQualifierElements(ele, bd);bd.setResource(this.readerContext.getResource());bd.setSource(extractSource(ele));return bd;}finally {this.parseState.pop();}return null;}复制代码
这个方法完成了读取一个bean包括将node属性读入到BeanDefinition
,读取bean的构造函数配置(是构造器注入的前提),读取bean的属性配置(是setter注入的前提),其实将node属性读取到BeanDefinition
很简单,仅仅是一一对应而已,真正的复杂点在于读取构造函数参数、读取属性值参数。
5、构造器和属性参数解析
来看下面一段配置:
<bean id="userDao" class="spring.road.beans.models.UserDao"/><!--setter注入--><bean id="beanService" class="spring.road.beans.models.BeanService"><property name="mapper" ref="userDao"/><property name="name" value="lijinpeng"/><property name="sex" value="false"/></bean><!--构造器注入--><bean id="person" class="spring.road.beans.models.Person"><constructor-arg name="age" value="26"/><constructor-arg name="name" value="dangwendi"/><constructor-arg name="userDao" ref="userDao"/><constructor-arg name="sex" value="true"/></bean>
复制代码
这段配置使用了两种注入方式:
property属性解析
setter注入就是我们通过为属性赋值,如果属性值都是string类型的还很好解决,如果pojo类的属性值不是String,而是比如像Boolean、int、Date等这些数据的时候,必须要进行数据转换操作才可以在getBean()
的时候将property配置的属性通过反射注入到对应的字段里,这好像也不是什么困难的事情,但是如果是ref引用类型呢,这个问题该如何解决呢?Spring很巧妙的解决了这个问题,用RuntimeBeanReference
来表示ref引用的数据,用TypedStringValue
表示普通String字符串。既然一个pojo类的所有配置都会读取到BeanDefinition
,所以在xml中配置的属性必然也会存储到BeanDefinition
中,继续看源码会发现BeanDefinition
中用MutablePropertyValues
类表示属性集合,该类中propertyValueList
就是property集合数据,Spring用PropertyValue存储了property的name value信息。
//在BeanDefinition类中
MutablePropertyValues getPropertyValues();
//在MutablePropertyValues类中的属性
private final List<PropertyValue> propertyValueList;
//在PropertyValue中的属性
private final String name;
private final Object value;
复制代码
根据上面xml配置可以得知value可能需要类型转换,也可能是引用ref,鉴于getBean阶段无法直接赋值,所以需要一个中间类保存数据,在getBean()
反射阶段根据类型去转换成对象,再次查看parsePropertyElements
方法:
public void parsePropertyElements(Element beanEle, BeanDefinition bd) {NodeList nl = beanEle.getChildNodes();for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);//解析bean下的property属性节点if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {parsePropertyElement((Element) node, bd);}}}
复制代码
parsePropertyElement
解析bean下的property属性节点
public void parsePropertyElement(Element ele, BeanDefinition bd) {//获取property的name 这个很简单String propertyName = ele.getAttribute(NAME_ATTRIBUTE);if (!StringUtils.hasLength(propertyName)) {error("Tag 'property' must have a 'name' attribute", ele);return;}this.parseState.push(new PropertyEntry(propertyName));try {if (bd.getPropertyValues().contains(propertyName)) {error("Multiple 'property' definitions for property '" + propertyName + "'", ele);return;}//获取获取property的value 这个需要用中间类表示Object val = parsePropertyValue(ele, bd, propertyName);PropertyValue pv = new PropertyValue(propertyName, val);parseMetaElements(ele, pv);pv.setSource(extractSource(ele));bd.getPropertyValues().addPropertyValue(pv);}finally {this.parseState.pop();}}
复制代码
山重水复疑无路,柳暗花明又一村,下面方法即是实现过程:
public Object parsePropertyValue(Element ele, BeanDefinition bd, String propertyName) {String elementName = (propertyName != null) ?"<property> element for property '" + propertyName + "'" :"<constructor-arg> element";// Should only have one child element: ref, value, list, etc.NodeList nl = ele.getChildNodes();Element subElement = null;for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) &&!nodeNameEquals(node, META_ELEMENT)) {// Child element is what we're looking for.if (subElement != null) {error(elementName + " must not contain more than one sub-element", ele);}else {subElement = (Element) node;}}}//是否有ref属性boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE);//是否有value属性boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE);//ref和value只能存在一个if ((hasRefAttribute && hasValueAttribute) ||((hasRefAttribute || hasValueAttribute) && subElement != null)) {error(elementName +" is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele);}if (hasRefAttribute) {String refName = ele.getAttribute(REF_ATTRIBUTE);if (!StringUtils.hasText(refName)) {error(elementName + " contains empty 'ref' attribute", ele);}//如果是ref 则转换成RuntimeBeanReferenceRuntimeBeanReference ref = new RuntimeBeanReference(refName);ref.setSource(extractSource(ele));return ref;}else if (hasValueAttribute) {//如果是String则转换成TypedStringValueTypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE));valueHolder.setSource(extractSource(ele));return valueHolder;}else if (subElement != null) {return parsePropertySubElement(subElement, bd);}else {// Neither child element nor "ref" or "value" attribute found.error(elementName + " must specify a ref or value", ele);return null;}}
复制代码
看有注释行的代码,结合上面的分析,大概能了解setter注入的第一阶段BeanDefinition
保存属性数据的方式了,在调用BeanFactory
的getBean()
方法时,在反射阶段获取到值对象时可以根据类型去获取值,如果是TypedStringValue
则只需校验值是否应该转换,如果需要转换即可,至于如何转换,如果是RuntimeBeanReference
更简单了,直接通过getBean()
获取就好了,请记住这两个类型,在分析getBean()
阶段属性值解析的时候就会用到他们。
构造函数constructor-arg解析
构造器注入其实比setter注入要稍微麻烦一点,之所以说麻烦其实就是要借助相关技术去实现,因为构造器可能会有很多重载,在xml配置中如果参数顺序不同可能会调用不同的构造函数,导致注入失败,所以如果要保存构造参数值,必须匹配到唯一合适的构造函数,并且在xml配置的constructor-arg
必须按照一定规则与匹配的构造函数一一对应,才可以在getBean()
阶段注入成功。
当我自己尝试去写的时候,以为只需要通过反射获取构造函数的参数名即可,但是很不幸,通过反射拿到的参数名是诸如arg1 arg2 这样的name,所以只能通过读取类的字节码文件了, 以前看过《深入了解java虚拟机》这本书,知道可以通过读取字节码文件的方式获取参数名,但是里面的各种索引,字段表集合啊什么的想记住真的好难,而且我的水平还远远达不到那个高度,所以就用现成的吧, 我当时是用javassite实现的,看了spring的源码,发现spring是用asm实现的,当然这个阶段是在getBean()
阶段实现的,之所以介绍是因为必须要先了解为什么Spring要这么保存构造参数,后面的getBean在分析这块源码,还是先来看看构造函数参数在BeanDefinition的保存吧。
spring允许在xml中通过index、name、type来指定一个参数,在BeanDefinition
中使用ConstructorArgumentValues
存储构造函数参数集合,在ConstructorArgumentValues包含了两个集合一个配置了索引的indexedArgumentValues
参数集合,另一个没有配置索引的enericArgumentValues
构造函数参数集合,然后构造函数参数值用内部类ValueHolder
表示,这个类里包含了参数的value,类型,参数名等。
//在BeanDefinition类中
ConstructorArgumentValues getConstructorArgumentValues();
//在ConstructorArgumentValues类中
//使用了索引的构造函数参数值集合
private final Map<Integer, ValueHolder> indexedArgumentValues = new LinkedHashMap<Integer, ValueHolder>(0);
//未使用索引的构造函数参数值集合
private final List<ValueHolder> genericArgumentValues = new LinkedList<ValueHolder>();
//在ValueHolder中的属性
private Object value;
private String type;
private String name;
private Object source;
复制代码
构造函数参数的存储结构分析完了,接下来看看代码吧,其实存储和属性值的存储是一样的 ,这里只看关键的代码:
public void parseConstructorArgElements(Element beanEle, BeanDefinition bd) {NodeList nl = beanEle.getChildNodes();for (int i = 0; i < nl.getLength(); i++) {Node node = nl.item(i);if (isCandidateElement(node) && nodeNameEquals(node, CONSTRUCTOR_ARG_ELEMENT)) {parseConstructorArgElement((Element) node, bd);}}}
复制代码
解析constructor-arg
标签parseConstructorArgElement
public void parseConstructorArgElement(Element ele, BeanDefinition bd) {//获取indexString indexAttr = ele.getAttribute(INDEX_ATTRIBUTE);//获取typeString typeAttr = ele.getAttribute(TYPE_ATTRIBUTE);//获取nameString nameAttr = ele.getAttribute(NAME_ATTRIBUTE);//如果index不为空 保存到indexedArgumentValues集合中if (StringUtils.hasLength(indexAttr)) {try {int index = Integer.parseInt(indexAttr);if (index < 0) {error("'index' cannot be lower than 0", ele);}else {try {this.parseState.push(new ConstructorArgumentEntry(index));//将value转换成RuntimeBeanReference或者TypedStringValueObject value = parsePropertyValue(ele, bd, null);ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);//保存typeif (StringUtils.hasLength(typeAttr)) {valueHolder.setType(typeAttr);}//保存nameif (StringUtils.hasLength(nameAttr)) {valueHolder.setName(nameAttr);}valueHolder.setSource(extractSource(ele));if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) {error("Ambiguous constructor-arg entries for index " + index, ele);}else {//保存构造函数参数值bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder);}}finally {this.parseState.pop();}}}catch (NumberFormatException ex) {error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele);}}//index未配置 保存到普通集合中genericArgumentValueselse {try {this.parseState.push(new ConstructorArgumentEntry());Object value = parsePropertyValue(ele, bd, null);ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);if (StringUtils.hasLength(typeAttr)) {valueHolder.setType(typeAttr);}if (StringUtils.hasLength(nameAttr)) {valueHolder.setName(nameAttr);}valueHolder.setSource(extractSource(ele));//保存构造函数参数值bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder);}finally {this.parseState.pop();}}}复制代码
至此,一个xml配置的bean被完全存放到了BeanDefinition
中,其实基于扫描注解配置也是一样的,只不过在做很多清理工作,针对下面配置简要说明下基于注解的处理:
<context:component-scan base-package="com.yms.market"/>
复制代码
首先spring读取到这个node,会查找该node的
NameSpaceHandler
,然后调用parse方法解析然后读取到属性
base-package
,转换成对应路径后查找该路径下所有的class文件读取class文件的注解,查看是否实现了特定注解,如果实现了注解则处里方式与xml配置的处理相同,否则不处理。
真实的处理过程比较复杂,也是用了很多设计模式,用了很多类来处理,但是我想说的是,无论是用过注解还是通过xml,最终的处理方式都是一样的,都是先生成一个BeanDefinition
注册到IOC中,然后通过getBean()
获取。
6、BeanDefinitionRegistry注册
BeanDefinition
创建完成了,还差最后一步,将生成的BeanDefinition
注册到IOC中,这就必须往回看了。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {//将bean的属性都读取到到BeanDefinitionHolder上BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);if (bdHolder != null) {//如果bean里面有自定义标签 来决定是否再次解析bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);try {// 将生成的BeanDefinitionHolder注册到IOC中BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());}catch (BeanDefinitionStoreException ex) {getReaderContext().error("Failed to register bean definition with name '" +bdHolder.getBeanName() + "'", ele, ex);}// 发送注册事件getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));}}
复制代码
这段代码应该还很熟悉,这次看最后一步registerBeanDefinition
public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)throws BeanDefinitionStoreException {// 获取beanNameString beanName = definitionHolder.getBeanName();//注册BeanDefinition,key为beanName,value是BeanDefinitionregistry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());// 如果配置别名的话获取别名String[] aliases = definitionHolder.getAliases();if (aliases != null) {for (String aliase : aliases) {//注册别名registry.registerAlias(beanName, aliase);}}}
复制代码
该方法完成了以下事情
- 将
BeanDefinition
用beanName作为key注册到IOC容器中 - 如果配置了别名,将beanName与别名映射起来。
再来看具体的注册过程registry.registerBeanDefinition
,注册是调用BeanDefinitionRegistry
的registerBeanDefinition
方法,在刚开始的分析说过DefaultListableBeanFactory
实现了BeanDefinitionRegistry
和BeanFactory
,而且实现了具体逻辑,下面的内容就是Spring注册的过程,为了看的清晰我省去了很多异常和无用的代码:
@Overridepublic void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
{if (beanDefinition instanceof AbstractBeanDefinition) {try {//验证((AbstractBeanDefinition) beanDefinition).validate();}catch (BeanDefinitionValidationException ex) {}}//这里保证了线程安全synchronized (this.beanDefinitionMap) {BeanDefinition oldBeanDefinition = this.beanDefinitionMap.get(beanName);if (oldBeanDefinition != null) {if (!this.allowBeanDefinitionOverriding) {//不允许覆盖抛出异常}else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) {if (this.logger.isWarnEnabled()) {}else {if (this.logger.isInfoEnabled()) {}}}else {this.beanDefinitionNames.add(beanName);this.frozenBeanDefinitionNames = null;}//注册进去喽this.beanDefinitionMap.put(beanName, beanDefinition);}resetBeanDefinition(beanName);}
复制代码
到此为止,IOC容器的第一步为bean生成BeanDefinition
并注册到IOC容器中完成,接下来就是第二步,通过BeanFactory实现依赖注入了。
转载于:https://juejin.im/post/5ccff60d6fb9a032447f0c8e
Spring源码解读(1)-IOC容器BeanDefinition的加载相关推荐
- 【spring源码分析】IOC容器初始化(二)
前言:在[spring源码分析]IOC容器初始化(一)文末中已经提出loadBeanDefinitions(DefaultListableBeanFactory)的重要性,本文将以此为切入点继续分析. ...
- 转 Spring源码剖析——核心IOC容器原理
Spring源码剖析--核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring 源码 ioc 编程 bean 更多 个人分类: Java https:// ...
- spring源码解析之IOC容器(二)------加载和注册
上一篇跟踪了IOC容器对配置文件的定位,现在我们继续跟踪代码,看看IOC容器是怎么加载和注册配置文件中的信息的.开始之前,首先我们先来了解一下IOC容器所使用的数据结构-------BeanDefin ...
- spring源码 — 一、IoC容器初始化
IoC容器初始化 注意:本次的spring源码是基于3.1.1.release版本 容器:具有获取Bean功能--这是最基本功能,也是BeanFactory接口定义的主要行为,在添加了对于资源的支持之 ...
- CesiumJS 2022^ 源码解读[7] - 3DTiles 的请求、加载处理流程解析
3DTiles 与 I3S 是竞争关系,可是比起生态开放性.数据定义的灵活性与易读性来说,3DTiles 比 I3S 好太多了.由于数据生产工具的开发者水平参差不齐,且数据并不存在极致的.万能的优化方 ...
- spring beans源码解读之 ioc容器之始祖--DefaultListableBeanFactory
spring Ioc容器的实现,从根源上是beanfactory,但真正可以作为一个可以独立使用的ioc容器还是DefaultListableBeanFactory,因此可以这么说, DefaultL ...
- 【spring源码分析】IOC容器初始化(六)
前言:经过前几篇文章的讲解,我们已经得到了BeanDefinition,接下来将分析Bean的加载. 获取Bean的入口:AbstractApplicationContext#getBean 1 pu ...
- Spring 源码分析(七)--bean的加载详细分析
一:缓存中获取单例bean 前面已经提到过,单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例缓存中获取,当然这里也只是尝试加载,首先尝试从缓存中加载,然后再次尝试从sing ...
- Spring源码分析【3】-SpingWebInitializer的加载
SpingWebInitializer的加载 Spring基于注解的配置代码: public class SpingWebInitializer extends AbstractAnnotationC ...
- Spring 源码解读第七弹!bean 标签的解析
Spring 源码解读继续. 本文是 Spring 系列第八篇,如果小伙伴们还没阅读过本系列前面的文章,建议先看看,这有助于更好的理解本文. Spring 源码解读计划 Spring 源码第一篇开整! ...
最新文章
- 图解C/C++中函数参数的值传递、指针传递与引用传递
- 2020年1月份学习总结,死线(Deadline)杀死团队拖延症
- 2017年9月2日普级组T1 正方形
- spring-boot-maven-plugin插件找不到含有main的主类
- php重定向和伪静态,Apache301重定向和伪静态设置教程(wp程序为例)
- jzoj4282-[NOIP2015模拟10.29B组]平方数游戏【构造】
- 罚款200元的交通违法行为
- python: 使用socket实现局域网不同主机通信。解决ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个现有的连接。
- 开放华为30年研发能力与实践 助力DevOps真正落地
- apache shiro版本查看_深入学习SpringBoot(四):springboot整合shiro
- 开源网格划分软件_网格划分:PointWise 18.3R1
- php header是什么,学习猿地-php header什么意思
- 为何python不好找工作-Python培训班出来找工作容易吗?老男孩教育
- 手机c语言编译器ide文件位置,C语言编译器IDE
- 对Ajax回调函数中返回错误信息的处理及常见情况整理
- Times New Roman vs Arial,期刊论文如何选择字体?
- 如何分析留存率?(案例:留存率堆积直方图+留存率下降分析)
- iov_iter结构体
- HBuilderX 开发工具
- vue中奖名单,新闻列表跑马灯,无缝上下滚动
热门文章
- 转:ElasticSearch 插件安装
- matlab练习程序(倾斜校正,透视变换)
- 【LeetCode】【字符串】题号:*49. 字母异位词分组
- tensorflow.python.framework.errors_impl.NotFoundError: Could not find valid device for node.
- excel 输入自动更新的时间和日期
- 计算机应用基础本科答案排序,计算机应用基础本科操作题
- ios笔试题算法_【2018年最新】iOS面试题之常见算法
- 函数 fork 和函数 vfork区别
- matlab图像融合代码,图像融合+源代码+matlab
- 阿里云:已有10000家企业在云上构建数据湖