一、概述
1. 为什么需要理解XML配置解析?
我是一个刚交了一年社保的一年工作经验的小老弟,从大学刚接触软件开发到毕业正式入职所接触到JavaSE或JavaEE项目中,基本都会使用Spring作为项目的对象管理容器。尤其在大学期间,WEB项目使用Spring的时候基本都是通过配置applicationContext.xml全局配置文件,然后使用web.xml配置ContextLoaderListener加载这个全局配置文件去初始化容器。
早期版本的Spring,只能通过加载XML去启动配置文件,演变到现在也可以通过扫描类和注解去启动Spring容器,但Spring的本质和核心是不会变的。通过学习XML配置解析可以起到窥一斑而知全貌的效果,当你在研究扫描类和注解的时候会突然发现,大致的流程基本是很相似的。
2. BeanDefinition的注册有必要了解吗?
当你成功启动了一个Spring容器,有一个Bean配置的是懒加载,那么此时这个Bean在容器中是否存在?或者是以什么方式存在?
答案就是在一个需要被实例化的类还没有创建的时候,它在Spring中就是一个BeanDefinition,也就是这个Bean的定义,BeanDefinition包含了这个对象实例化所需要用到的所有信息。(详情可见之前的文章有描述过)
由此可见BeanDefinition注册是Spring容器的启动中非常重要的一环,BeanDefinition的注册不难,简单来讲就是Spring中有一个装BeanDefiniton的Map,当你扫描解析完XML的Bean标签之后就会注册到这个Map中。
二、ClassPathXmlApplicationContext解析XML
1. ClassPathXmlApplicationContext的解析XML入口方法
// 方法在AbstractApplicationContext的709行左右
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
/**
* 主要内容:配置信息到beanDefinition的转换过程
* 模版设计模式
* Spring中运用的最多的设计模式
* obtainFreshBeanFactory方法是一个模版方法
* refreshBeanFactory方法是一个钩子方法,需要子类去实现
*/
refreshBeanFactory();
return getBeanFactory();
}
这个方法在容器的refresh()流程中被调用,ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); 实际调用的是ClassPathXmlApplicationContext的父类AbstractApplicationContext的方法。(模版方法模式)
obtainFreshBeanFactory()方法也是一个模版方法,其中定义了一个钩子方法refreshBeanFactory(),refreshBeanFactory()方法在AbstractApplicationContext中是一个抽象方法,需要子类去实现。
protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;
1.1 XML解析前创建工厂
经过查询容器类的继承结构,发现最终调用的是AbstractRefreshableApplicationContext的refreshBeanFactory()方法。该方法中不重要的掐头去尾已经省略了,主要有记个关键点和概念需要理解一下,就是BeanFactory是一个实例工厂,主要是用来管理Bean的,容器Context包含了BeanFactory。
protected final void refreshBeanFactory() throws BeansException {
// ...
try {
/**
* 创建beanFactory
* beanFactory:实例工厂,无论什么实例只要被spring管理,都在这个工厂里面,主要是bean的相关操作。
* Context和beanFactory的关系:从属关系。 不重要理解即可
*/
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());
/*
* 容器重要属性设置:
* 1.是否允许同名bean
* 2.是否允许循环依赖
*/
customizeBeanFactory(beanFactory);
/**
* 解析XML,注册beanDefinition 重要:* * * * *
*/
loadBeanDefinitions(beanFactory);
// ...
}
// ...
}
其中我们创建的工厂就是Spring默认的工厂,DefaultListableBeanFactory。
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}
1.2 创建工厂后设置一些参数
在上述方法流程中会执行customizeBeanFactory(),这个方法会设置一些工厂的属性,是否允许同名bean存在和是否允许循环依赖。(看代码注释即可)
protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
/*
* 是否允许同名的bean存在
* 默认是不允许的
*/
if (this.allowBeanDefinitionOverriding != null) {
beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
/*
* 是否允许循环依赖
* 默认是允许的
*/
if (this.allowCircularReferences != null) {
beanFactory.setAllowCircularReferences(this.allowCircularReferences);
}
}
1.3 执行加载XML注册BeanDefinition的方法。
同样是在1.1的方法流程中,执行了loadBeanDefinitions(beanFactory);方法并传入当前创建的BeanFactory。
loadBeanDefinition(DefaultListableBeanFactory beanFactory)这个方法在AbstractRefreshableApplicationContext是一个抽象方法,也需要子类去实现它。经过查询其继承结构发现是AbstractXmlApplicationContext实现的。
protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
throws BeansException, IOException;
在AbstractXmlApplicationContext抽象类,loadBeanDefinitions(DefaultListableBeanFactory beanFactory)方法主要是采用委托设计模式去实现的。 也就是说容器将XML的解析委托给了XmlBeanDefinitionReader类的实例,自身并没有详细的解析流程,将自身作为参数传入构造器,使得XmlBeanDefinitionReader类的实例可以再解析XML的过程中在工厂中注册BeanDefinition。
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
/**
* 创建XML解析器,这里涉及到了委托设计模式
* 委托设计模式:【专人专事】
* 主类要持有委托类的引用,在实现服务时把具体实现的逻辑委托给委托类去实现。
* 创建XML解析器
* Create a new XmlBeanDefinitionReader for the given BeanFactory.
*/
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
beanDefinitionReader.setEnvironment(this.getEnvironment());
/**
* 使得XmlBeanDefinitionReader持有当前Spring容器的对象,这个很重要需要记住。
* 把当前的Spring容器当作ResourceLoader注入到XmlBeanDefinitionReader
* ClasspathXmlApplicationContext 类的继承链最顶端是一个 ResourceLoader
*/
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
initBeanDefinitionReader(beanDefinitionReader);
/**
* 执行解析,解析XML为BeanDefinition过程很重要
*/
loadBeanDefinitions(beanDefinitionReader);
}
// XmlBeanDefinitionReader 重载的 loadBeanDefinitions 方法,拿URL,直接委托。
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
// ...
//没有多余的代码简单的条件直接委托给其他对象去执行,就是专人专事。
String[] configLocations = getConfigLocations();
if (configLocations != null) {
// 走这个方法 重要 : * * * * *
reader.loadBeanDefinitions(configLocations);
}
}
2. 解析XML从AbstractBeanDefinitionReader开始
2.1 解析URL的心路历程
紧接上文执行reader.loadBeanDefinitions(configLocations);,实际调用的是AbstractBeanDefinitionReader类的loadBeanDefinitions(String... locations) 方法。loadBeanDefinitions(String... locations)方法又会循环遍历这个locations去挨个解析location。
public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
Assert.notNull(locations, "Location array must not be null");
int count = 0;
/**
* 循环遍历解析各个路径下的xml文件
*/
for (String location : locations) {
count += loadBeanDefinitions(location);
}
return count;
}
loadBeanDefinitions(String location)方法会调用另一个重载的方法,loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources)方法,只不过actualResources参数传的是null。
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
/**
* 根据地址解析XML文件
*/
return loadBeanDefinitions(location, null);
}
2.2 从解析URL到解析Resource的心路历程
下面代码先执行了 getResourceLoader() ,这个ResourceLoader就是容器本身,在创建XmlBeanDefinitionReader时候持有的容器对象。因为容器本身顶层实现了ResourcePatternResolver接口,所以可以通过getResources方法得到资源数组(Resource[])。
然后再次调用重载的遍历解析资源的loadBeanDefinitions(Resource... resources)方法。
public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
/**
* resourceLoader在loadBeanDefinitions前被注入到reader对象中
* 这个resourceLoader其实就是容器本身对象
*/
ResourceLoader resourceLoader = getResourceLoader();
// .......
if (resourceLoader instanceof ResourcePatternResolver) {
// Resource pattern matching available.
try {
/**
* Spring.xml对象封装成一个Resource对象,Resource对象中有文件对象,文件流。
*/
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
/**
* 解析这些Resource对象
*/
int count = loadBeanDefinitions(resources);
}
// .......
}
转来转去,还没有结束,批量解析资源的loadBeanDefinitions遍历调用解析单个资源的loadBeanDefinitions方法。
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
Assert.notNull(resources, "Resource array must not be null");
int count = 0;
for (Resource resource : resources) {
/**
* AbstractBeanDefinitionReader父类的方法
* 模版方法
*/
count += loadBeanDefinitions(resource);
}
return count;
}
2.3 从解析Resource到解析InputStream的心路历程
上面遍历调用解析Resource的是,调用的是BeanDefinitionReader接口的这个方法
int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
实际执行的是XmlBeanDefinitionReader实现类的方法,然后又会将这个resource编码后再次调用重载的方法。
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException
{
/**
* 实现类实现的钩子方法
* resource -> Encoded 将流变成有编码的流
*/
return loadBeanDefinitions(new EncodedResource(resource));
}
我们来查看下加载有编码的流的方法是什么样子的。就是获取编码流中的InputStream然后继续去解析这个流。
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
//......
try {
/**
* 拿到文件流
*/
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
/*
* InputSource是XML文档解析的类 , JDK的类。专门做XML文档解析的一个类
*/
InputSource inputSource = new InputSource(inputStream);
//......
/**
* 加载Bean的定义信息
*/
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
//......
}
2.4 Java 的 SAX 解析XML
承接2.3继续到了doLoadBeanDefinitions(InputSource inputSource, Resource resource)看见了曙光,终于是将inputSource和resource解析成了Document对象。提到Document对象我们都不会陌生,就是XML解析出的文档信息,里面包含了Node标签,也就是我们配置的Bean。
然后就是注册BeanDefinition的方法,在了解BeanDefinition注册之前,需要详细的了解一下doLoadDocument(InputSource inputSource, Resource resource) 这个方法的具体实现。
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
//...........
/**
* xml文件流对象解析为document对象
*/
Document doc = doLoadDocument(inputSource, resource);
/**
* 将document对象解析为BeanDefinition,并返回解析的数量
*/
int count = registerBeanDefinitions(doc, resource);
//...........
}
这个方法的实现很长一串,我们点进loadDocument( ... ... )这个方法。
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
/**
* 实际解析XML InputSource的方法,解析成一个Document对象
*/
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
可以看着这是一个SAX解析的常规流程,创建工厂,创建builder,解析输入流,然后返回Document。
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
/**
* XML解析封装document对象常规流程 SAX解析常用
* 1.创建工厂
* 2.创建builder
* 3.解析输入流
*/
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isTraceEnabled()) {
logger.trace("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
/**
* 解析成Document对象并返回
*/
return builder.parse(inputSource);
}
到此为止将一个XML解析成Document的内容就结束了,当然Document还需要进一步解析成一个个Element并提取每个元素的信息如果是Bean的话就组册BeanDefinition。
3. 解析Document,注册BeanDefinition
3.1 开始解析Document
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
//...........
/**
* xml文件流对象解析为document对象
*/
Document doc = doLoadDocument(inputSource, resource);
/**
* 将document对象解析为BeanDefinition,并返回解析的数量
*/
int count = registerBeanDefinitions(doc, resource);
//...........
}
回到doLoadBeanDefinitions这个方法,执行完将Resource转换为Document后,着重看解析document并注册BeanDefinition的内容。下面我们看registerBeanDefinitions(doc, resource)方法具体实现。
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
/**
* 委托设计模式:
* 把解析的工作委托给BeanDefinitionDocumentReader对象
* 已把xml -> document ,继续委托给DocumentReader解析document
*/
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
/**
* 解析Document对象并注册BeanDefiniton
* 方法:registerBeanDefinitions(args1,args2)
* 参数:Document doc, XmlReaderContext readerContext
*/
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
可以看到我们解析Document对象依旧是吧这个解析的过程委托给了BeanDefinitionDocumentReader类的实例。
上述执行流程中关注下 createReaderContext(resource) ,这个方法是返回XmlReaderContext类的实例,并且在构造实例的过程中,持有了当前的XmlBeanDefinitionReader的对象。之前我们提到过XmlBeanDefinitionReader持有了当前容器的引用,所以在createReaderContext(resource)方法的返回值XmlReaderContext实例中也间接持有了当前容器的引用,它可以去注册BeanDefinition。
继续看documentReader.registerBeanDefinitions这个调用方法的具体实现。
public interface BeanDefinitionDocumentReader {
void registerBeanDefinitions(Document doc, XmlReaderContext readerContext)
throws BeanDefinitionStoreException;
}
来到了BeanDefinitionDocumentReader接口,最终调用的是下面的DefaultBeanDefinitionDocumentReader实现类的方法。可以看到方法将readerContext的实例持有到自身的对象之中。并开始解析根结点。
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
/**
* 把Document根结点 (root) 传进去
*/
doRegisterBeanDefinitions(doc.getDocumentElement());
}
3.2 具体Element标签的解析
来到根节点Element的具体解析实现,其实可以忽略大部分的内容,我们只需要看parseBeanDefinitions(root, this.delegate)这个主要的解析标签方法就好了,因为获取delegate,这个delegate是用来解析自定义标签的,自定义标签解析内容很多,所以需要总结一篇详细的文章来描述自定义标签的解析,这里就暂时忽略了。
protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
/**
* 主要是获取delegate,用来委托给第三方解析起解析自定义标签
*/
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
// ...... 省略多行代码
}
/**
* 预处理模版方法 暂时无具体实现
*/
preProcessXml(root);
/**
* 主要看这个方法,标签的具体解析过程
*/
parseBeanDefinitions(root, this.delegate);
/**
* 后处理模版方法 暂时无具体实现
*/
postProcessXml(root);
this.delegate = parent;
}
这里他首先会获取根结点中的所有子结点,也就是我们配置的Bean,Import等传统标签,当然也有context-componentsacn等自定义的标签。这里我们主要看默认的传统标签解析。自定义标签解析需要详细的总结一篇文章来描述。
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
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)) {
/**
* 默认标签解析
*/
parseDefaultElement(ele, delegate);
}
else {
/**
* 自定义标签解析,委托给delegate解析
*/
delegate.parseCustomElement(ele);
}
}
}
}else {delegate.parseCustomElement(root);}
}
默认标签中我们常用的也就是<bean /> <import />标签,最重要的是<bean />标签,这个是我们最最常用的。我给标了重要程度五颗星。进入processBeanDefinition(ele, delegate);这个方法详细的看下。
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
/**
* import标签的解析,重要程度:*
*/
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
/**
* alias标签的解析 别名标签,重要程度:*
*/
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
/**
* bean标签的解析,重要程度:* * * * *
*/
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
/**
* recurse ,不重要 外层的beans
*/
doRegisterBeanDefinitions(ele);
}
}
到这里我们看其实我之前看源码的注释描述的还是很多的,可见这里面涉及到了很多知识点,但是不重要,我们只关注 Element 具体是如何解析的,并且这个解析好的BeanDefinitionHolder注册到了容器的哪个地方,有没有什么业务规则。
3.2.1 大致描述processBeanDefinition方法的实现:
- 将element解析成BeanDefinitionHolder
- 装饰这个BeanDefinitionHolder如果需要的话
- 注册这个BeanDefinitionHolder
接下来会把这三点拆分成3个小的点来描述。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
/**
* 方法:parseBeanDefinitionElement
* 解析document封装成beanDefinition
*
* 重要程度:* * * * *
*/
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
/**
* 没吊用,但需要学习设计思想。
*
* 装饰者设计模式,加上SPI(service provider interface)的设计思想SPI(Mybatis,Spring,Dubbo的SPI扩展)
* SPI用来解耦,扩展的设计思想,在不改变原有代码的前提下进行开发,实现热插拔。和策略模式有点像
* SPI简单来讲就是加载配置文件通过配置文件类中的类路径信息,再不修改核心代码的前提下扩展功能。
*
* 内容:
* 1.namespace uri和解析类建立映射关系
* 2.解析类实现统一接口完成多态
* 3.beandefinition的不断包装/装饰
*
* 不使用构造器和属性注入,在bean标签中使用前缀属性 p: c:进行注入 ,在xmlns中加入schema/p schema/c 约束
*
* 重要程度:*
*/
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
/**
* 逻辑很简单,构建 别名 -> beanname -> beandefinition 的映射。
*
* 完成document到BeanDefinition对象的转换,对BeandDefinition对象进行缓存注册
* 重要程度: * * *
*/
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
3.3 <bean /> 标签元素解析成BeanDefinitionHolder的过程
先进入 delegate.parseBeanDefinitionElement(ele);的方法看一下,依然是重载外面包个壳。
@Nullable
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
return parseBeanDefinitionElement(ele, null);
}
再次进入parseBeanDefinitionElement(ele, null);这个方法。
3.3.1 parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) 外层提取信息具体实现
@Nullable
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {
// 解析过程不复杂,但是解析项比较多。BeanDifinition的属性比较多。
/**
* 参数提取:提取标签的 ID 和 name
*/
String id = ele.getAttribute(ID_ATTRIBUTE);
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
/**
* 标签name = ","或";"分割的字符串 解析别名列表
*/
List<String> aliases = new ArrayList<>();
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
beanName = aliases.remove(0);
if (logger.isTraceEnabled()) {
logger.trace("No XML 'id' specified - using '" + beanName +
"' as bean name and " + aliases + " as aliases");
}
}
/**
* 检查beanname的唯一性
*/
if (containingBean == null) {
checkNameUniqueness(beanName, aliases, ele);
}
/**
* 解析这个元素 ele 剩余的全部的属性
* 返回 名为beanName的 AbstractBeanDefinition 对象
* 详细的解析过程:* * * * *
*/
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
if (beanDefinition != null) {
// ............. 省略了一大堆代码
String[] aliasesArray = StringUtils.toStringArray(aliases);
/**
* BeanDefinition再次进行一个包装 -> BeanDefinitionHolder
*
* BeanDefinitionHolder是BeanDefinition,名称,别名的组装
*/
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
}
return null;
}
代码概述:
- 先提取了标签的id和name,如果name的配置了多个别名还要拆一下,如果id是空的直接拿第一个别名用一下。
- 因为当前参数containingBean是空的,所以要检测一下Bean的唯一性,也就是beanName和别名的唯一性。方法里面内容不多很好理解,自己点进去看下就好了。
- parseBeanDefinitionElement方法是,具体的提取信息创建BeanDefinition,提取属性设置属性。(这个方法最重要)
- 包装成BeanDefinitionHolder返回,BeanDefinitionHolder和BeanDefinition区别就是BeanDefinitionHolder包装了除了类定义外的beanName和别名数组。
3.3.2 parseBeanDefinitionElement(Element ele, String beanName, @Nullable BeanDefinition containingBean) 属性设置具体实现
进入 parseBeanDefinitionElement(ele, beanName, containingBean); 方法查看具体实现。
@Nullable
public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, @Nullable BeanDefinition containingBean) {
this.parseState.push(new BeanEntry(beanName));
/*
* 获取它的class属性,如果有。
*/
String className = null;
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
/*
* 获取它的parent属性,如果有。
*/
String parent = null;
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
parent = ele.getAttribute(PARENT_ATTRIBUTE);
}
try {
/**
* 创建一个GenericBeanDefinition对象并设置父对象id
*/
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
/**
* 重要: * * * * *
* 解析bean标签的属性把属性设置到对象中
*/
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
/*
* Bean标签的Meta子标签的解析,没什么用
*/
parseMetaElements(ele, bd);
/**
* lookup-method demo05
* 替代某方法的返回值。 使用代理实现。
* 重要程度:* *
*/
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
/**
* replace-method demo06
* arg-type子标签区分重载参数类型
* 在不改变原有代码的基础上进行增强,可以用AOP替代.
* 重要程度:* *
*/
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
/**
* 解析Bean中的constructor-arg标签
* 重要程度:* *
*/
parseConstructorArgElements(ele, bd);
/**
* 解析Bean中的property, 属性注入可以用@Value替代
* 重要程度:* *
*/
parsePropertyElements(ele, bd);
/**
* @Qualifier指定注入哪个bean
* 重要程度:* *
*/
parseQualifierElements(ele, bd);
// .............省略
/**
* 整个bean标签就解析完了
*/
return bd;
}
// .............省略
}
具体实现内容概述:
- 获取标签中的class属性
- 获取标签中的parent属性
- 基于class和parent先创建一个BeanDefinition对象
- parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);这个方法里面会对BeanDefinition设置很多属性,如scop属性、abstract属性、lazy属性、autowireMode属性、depends-on属性、autowire-candidate属性、init-method属性、destory-method属性、factory-bean和factory-method属性等。如果对于属性含义不了解看下我之前等 《快速理解Spring加载流程》的那个文章,里面有对BeanDefinition的属性的一些描述。
- <meta />子标签解析,解析后的键值对会存储到BeanDefinition的attributes这个Map中。如果需要用到<meta />这个值,需要先获取BeanDefinition。
- lookup-method这个属性的作用是,如果创建当前这个类的Bean,会给这个类的某一个方法塞一个返回值。如下案例就是调用ShowSixClass类的getPeople方法,我返回woman。(就是这么个作用,实际信息存储在BeanDefinition的MethodOverrides属性中)
<bean id="people" class="com.jd.nlp.dev.muzi.spring5.exercise.demo05.ShowSixClass" >
<!--简单理解就是 给某个方法塞返回值 体现出一种多态的方式-->
<lookup-method name="getPeople" bean="woman" />
</bean>
- replace-method这个属性就是增强某个方法,这个可以使用AOP去代替,但是还是弄个案例看一下。这个方法需要进行业务功能增强,但是又不希望在原来基础上修改,可以用 replaced-method标签。下述案例就是originClass这个Bean在调用method(String str)方法的时候会调用replaceClass的reimplement方法。(实际信息存储在BeanDefinition的MethodOverrides属性中)
//-----------配置--------------
<bean id="replaceClass" class="com.jd.nlp.dev.muzi.spring5.exercise.demo06.ReplaceClass" />
<bean id="originClass" class="com.jd.nlp.dev.muzi.spring5.exercise.demo06.OriginClass">
<replaced-method name="method" replacer="replaceClass">
<!-- 使用arg-type来区分重载的方法 -->
<arg-type match="java.lang.String" />
</replaced-method>
</bean>
// ------------代码---------------
public class OriginClass {
public void method(String param) {
System.out.println();
System.out.println("I am origin method! param = " + param);
System.out.println();
}
public void method(List param) {
System.out.println();
System.out.println("I am origin method! param = " + param);
System.out.println();
}
}
public class ReplaceClass implements MethodReplacer {
@Override
public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
System.out.println("I am replace method -> reimplement -> begin");
System.out.println("obj:"+ obj.toString());
System.out.println("method:" + method.getName());
System.out.println("args:" + args);
System.out.println("I am replace method -> reimplement -> end");
return null;
}
}
- constructor-arg标签的内容最终会存储在 BeanDefinition的 ConstructorArgumentValues 属性中,用作实例化有参构造函数参数获取。
- 解析Bean中的property,在当前支持注解扫描的版本下,可以用@Value替代 。
- @Qualifier指定注入哪个bean
以上就是parseBeanDefinitionElement(ele, beanName, containingBean)方法的具体实现内容,其实梳理下来就很简单,无非就是提取<bean />标签中配置的参数嘛,然后都包装到BeanDefinition对应的属性当中,最终把这个BeanDefinition返回。
至此,一个Bean标签的基本信息就被解析完了。返回BeanDefinition并被包装成一个Holder对象,然后我们回到之前解析的主流程中。
3.4 BeanDefinitionHolder是否需要被装饰?
再次看一下解析一个传统<bean /> 标签主流程的代码。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
/**
* 方法:parseBeanDefinitionElement
* 解析document封装成beanDefinition
* 重要程度:* * * * *
*/
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
/**
* 没吊用,但需要学习设计思想。
* 装饰者设计模式,加上SPI(service provider interface)的设计思想SPI(Mybatis,Spring,Dubbo的SPI扩展)
* SPI用来解耦,扩展的设计思想,在不改变原有代码的前提下进行开发,实现热插拔。和策略模式有点像
* SPI简单来讲就是加载配置文件通过配置文件类中的类路径信息,再不修改核心代码的前提下扩展功能。
* 内容:
* 1.namespace uri和解析类建立映射关系
* 2.解析类实现统一接口完成多态
* 3.beandefinition的不断包装/装饰
* 不使用构造器和属性注入,在bean标签中使用前缀属性 p: c:进行注入 ,在xmlns中加入schema/p schema/c 约束
* 重要程度:*
*/
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
// ................................................
}
}
delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);装饰这个BeanDefinitionHolder如果需要的话?
3.4.1 什么是装饰?
在我理解,装饰就是不断的对一个对象进行包装,填充这个对象的可变的属性列表值,使得我们可以动态的去达到自己想要表达的内容。
有的装饰是通过继承不断的去修改最初始的内容,不断的扩充自己所拥有的东西。而在Spring的装饰中,其实就是通过 SPI(service provider interface)服务发现思想,去判断我这个Bean是否需要被装饰。
3.4.2 什么是SPI?
SPI是service provider interface的缩写,是一种服务发现机制。
Spring的SPI设计首先要理解什么是 " namespaceURI ",如下面代码所示 xmlns 后面配置的 "http://www.springframework.org/schema/beans" 就是 " namespaceURI "。xsi:schemaLocation里面配置的是你的自定义标签的具体Schema(自定义标签解析相关内容暂且不提)。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">/>
<bean id="decoratorBean" class="com.jd.nlp.dev.muzi.spring5.exercise.demo07.DecoratorBean"
p:username="jack" p:password="123"
c:age="12" c:sex="1"
/>
</beans>
我们看上面配置文件中bean标签中有两种很奇怪的属性 “p:” 和 “c:” ,其中 “p:” 配置和Property功能类似,“c:” 配置和construct-args子标签属性功能类似,但是在Spring中这种属性是如何解析的呢?
想要知道如何解析“p:”和“c:”,首先要打开spring-beans的这个jar包,找到META-INF目录,找到spring.handlers文件打开。这里面定义的就是“p:”和“c:”的解析类与namespaceURI的关系。
文件:META-INF/spring.handlers
http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.SimpleConstructorNamespaceHandler
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler
回到之前调用装饰方法的入口,看一下bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);具体的方法是怎么实现的?
依然是包装了一下,然后调用到实际的实现方法中,首先他是获取标签Element元素的全部Node去遍历,查看是否需要装饰。主要的方法是 decorateIfRequired 方法。
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder) {
return decorateBeanDefinitionIfRequired(ele, definitionHolder, null);
}
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) {
BeanDefinitionHolder finalDefinition = definitionHolder;
/**
* 通过元素的属性来装饰
*/
NamedNodeMap attributes = ele.getAttributes();
// 循环
for (int i = 0; i < attributes.getLength(); i++) {
Node node = attributes.item(i);
// 装饰如果需要
finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
}
/**
* 通过子标签装饰
*/
NodeList children = ele.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
}
}
return finalDefinition;
}
这个方法我们可以看到,它是在看当前Node是否能得到NamespaceURI,得到了NamespaceURI就会通过NamespaceURI获取NamespaceHandler。然后调用handler.decorate去装饰这个beanDefinition。
public BeanDefinitionHolder decorateIfRequired(
Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) {
// node 就是 p:xxxx="xxxx" demo07
// 根据node获取node的命名空间,形如:http://www.springframework.org/schema/p
String namespaceUri = getNamespaceURI(node);
if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) {
/**
* SPI服务发现思想,通过URI获取spring.handlers配置的处理类
*
* resolve(namespaceUri)方法中有详细的解析过程
*/
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler != null) {
/**
* 实际的解析类,调用装饰方法。 可以理解为 handler 是一个装饰者, beanDefinition是被装饰者。
*/
BeanDefinitionHolder decorated =
handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd));
// ................
}
// ................
}
return originalDef;
}
看下resolve具体实现,是如何获取到NamespaceHandler的?
- getHandlerMappings获取到所有jar包中的spring.handlers文件中的namespaceURI和其对应的处理类的类路径,放入一个Map中。
- 反射这个类,并创建这个累的实例,得到namespaceHandler。
- 调用namespaceHandler的init方法。
- 把实例好的对象和namespaceURI映射上
- 返回这个namespaceHandler对象。
这样上文通过resolve获得的namespaceHandler对象,就是spring.handlers配置的当前namespaceURI对应的解析类的实例。这样Spring就可以通过namespaceHandler调用decorate方法进行装饰了。
public NamespaceHandler resolve(String namespaceUri) {
/**
* 加载"META-INF/spring.handlers"文件,建立URI和处理类的映射关系
*
* uri和类的映射关系,通过uri唯一找到一个类
*
* 方法:getHandlerMappings
* 重要程度:* * *
*/
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据URI就可以找到唯一的处理类(字符串)
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}
else {
// 处理类(字符串)反射
String className = (String) handlerOrClassName;
try {
/**
* 反射这个类
*/
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
// ......................
}
/**
* 基于类对象来实例化
* 备注:所有处理类必须继承NamespaceHandler,实现多态。
* 例如:
* SimpleConstructorNamespaceHandler implements NamespaceHandler
*
* 所有spring.handlers这些命名解析类都有一个特点是必须实现NamespaceHandler接口,来实现多态
*/
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 调用处理类初始化方法
namespaceHandler.init();
// 替换映射关系key对应的值
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
// ......................
}
}
简单看下"p:"属性对应的namespaceURI对应的解析类把,这是p的spring.handlers的配置。对应的解析类是SimplePropertyNamespaceHandler。
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
这是p的解析类的代码,SimplePropertyNamespaceHandler类的 init 方法没有具体的内容,但是decorate有装饰的详细逻辑,就不解读了。就是往BeanDefinition对应的属性塞值。和之前那波设置属性的操作差不多。"p:" 对应之前的逻辑的就是"property"子标签属性设置。
public class SimplePropertyNamespaceHandler implements NamespaceHandler {
private static final String REF_SUFFIX = "-ref";
@Override
public void init() {
}
/**
* p:实际对应的装饰方法
*/
@Override
public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
/**
* 被装饰对象 definition
* 主要内容是 解析 p:xxxx="xxxx" 内容,封装到属性 MutablePropertyValues 列表元素中去
*/
if (node instanceof Attr) {
Attr attr = (Attr) node;
String propertyName = parserContext.getDelegate().getLocalName(attr);
String propertyValue = attr.getValue();
MutablePropertyValues pvs = definition.getBeanDefinition().getPropertyValues();
if (pvs.contains(propertyName)) {
parserContext.getReaderContext().error("Property '" + propertyName + "' is already defined using " +
"both <property> and inline syntax. Only one approach may be used per property.", attr);
}
if (propertyName.endsWith(REF_SUFFIX)) {
propertyName = propertyName.substring(0, propertyName.length() - REF_SUFFIX.length());
pvs.add(Conventions.attributeNameToPropertyName(propertyName), new RuntimeBeanReference(propertyValue));
}
else {
/**
* 把属性内容加入到definition的MutablePropertyValues列表中
* 这样一种反复的对definition进行装饰/包装,体现了装饰者设计模式的感觉。
*
* 具体谁是装饰者已经不重要了,对beanDefinition已经进行反复的修改了。
*/
pvs.add(Conventions.attributeNameToPropertyName(propertyName), propertyValue);
}
}
return definition;
}
}
3.5 BeanDefinition注册的位置在哪里?
回归到之前解析<bean >标签的主流程中, BeanDefinitionReaderUtils.registerBeanDefinition这个方法就是在注册BeanDefinition。
getReaderContext() 获取到的就是之前从第一次委托对象持有的容器本身的引用,一直被间接的持有着这个对象。容器本身是含有BeanFactory和注册器的,所以可以获取到BeanDefinitionRegistry。
BeanDefinitionRegistry其实就是DefaultListableBeanFactory,BeanDefinitionRegistry是个接口。
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
//.........................
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
//....................
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
}
// ................................................
}
}
3.5.1 看下BeanDefinitionReaderUtils.registerBeanDefinition具体实现:(注册BeanDefinition至特定容器)
- BeanName和BeanDefinition注册构建映射关系
- Alias和BeanName注册构建映射关系
可以理解,我们可以通过BeanName快速的找到BeanDefinition,当然也可以通过别名,找到BeanName间接的找到BeanDefinition。
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
/**
* BeanName和BeanDefinition注册构建映射关系
* 重要:* * *
*/
String beanName = definitionHolder.getBeanName();
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
/**
* Alias和BeanName注册构建映射关系
*/
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}
3.5.2 看下registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());具体实现:(DefaultListableBeanFactory类的实现方法)
- 代码有点长该省略的都省略了
- 装beanDefinition的容器是 beanDefinitionMap
- 装beanName的集合是 beanDefinitionNames
以上2和3需要牢记,玩Spring源码debug的时候需要去这里找内容。
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
/**
* 重要代码最下方
*/
// .......................省略一万行
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) {
// .......................省略一万行
}
else {
// .......................省略一万行
else {
/**
* 把BeanDefinition缓存到Map中
*/
this.beanDefinitionMap.put(beanName, beanDefinition);
/**
* 把 beanname 放到 BeanDefinitionNames 这个List中,在Bean实例化时需要使用到该List。
* 该List包含所有的beanDefinition的名称
*/
this.beanDefinitionNames.add(beanName);
this.manualSingletonNames.remove(beanName);
}
this.frozenBeanDefinitionNames = null;
}
// .......................
}
3.5.3 看下registry.registerAlias(beanName, alias);方法的具体实现:
GenericApplicationContext.class
public void registerAlias(String beanName, String alias) {
/**
* 构建别名和 beanName的映射关系
*/
this.beanFactory.registerAlias(beanName, alias);
}
SimpleAliasRegistry.class
public void registerAlias(String name, String alias) {
// ...................
/**
* 映射关系在最下面
*/
synchronized (this.aliasMap) {
// ................... 省略一万行
else {
String registeredName = this.aliasMap.get(alias);
// ................... 省略一万行
/**
* 装的是别名和beanName【id的名称】的映射关系
* 别名取对象:
* 别名 - beanName 有映射关系
* beanName - beanDefinition 有映射关系
*
* 总体来说如果通过别名找beanDefinition需要二级映射
*/
this.aliasMap.put(alias, name);
// ...................
}
}
}
可以看到 alias 和 beanName的映射关系是在 aliasMap 中存储的。最外层是循环别名数组,以当前item 别名为key,以beanName为value一对一对存的。所以通过任意一个定义好别名都可以找到对应的beanName。
至此,ClassPathXmlApplicationContext 的 解析XML注册BeanDefinition流程就梳理完了。梳理的很难受,模版设计模式扩展性不错,就是跳来跳去的,抓耳挠腮,看湿了 ... ...
三、总结
简单总结一下,虽然微服务的大环境下,ClassPathXmlApplicaitionContext容器在我们日常的代码中越来越少的去使用了,但是万变不离其宗,Spring不论再怎么演变,初始的结构就是这样,以后只能是扩展和兼容,相似的功能还会复用之前的代码。所以吃透一套流程,待我们分析注解配置启动容器的时候,也是小事一桩。
就目前来看BeanDefinition的这些属性,有一些我们是基本不会用到的,就比如lookup-method,init-method(实现InititalizeBean接口,@PostConstruct可以替代),factory-bean(实现Factorybean接口就是将 getObject()方法结果放入Spring的factoryBeanObjectCache这个容器里,我们根据类实际的beanName获得的bean其实是getObject()方法返回的bean,如果要获取类真正的实例Bean,需要在beanName前加个&符号,这种机制目前确实不知道有什么用。
总之,源码内容不要过分的深究,要一遍一遍的读,先从整体角度去考虑,然后针对细节做深化梳理,我总结的Spring相关内容也是基于以广度为先,按照流程的先后在细致的对每一个我想知道的知识点进行深度梳理。