回顾
前面几篇,将Bean的标签解析整个流程弄懂了,但对于整个XML配置文件的解析是包括以下标签的
- bean:用于创建IOC容器创建对象
- alias:用于给Bean起别名
- import:用于导入其他配置文件(有时候我们喜欢将Spring的配置文件分开单独处理,比如mybatis整合是一个文件、springmvc的整合又是另外一个文件,但Spring读取只会解析一个总文件,所以我们可以在总文件使用import标签导入其他配置文件)
回到我们刚开始的解析标签(解析标签的工作交由DefaultBeanDefinitionDocumentReader实现)
从这里开始去解析各类的标签,支持上面所示的4个标签
- import
- alias
- bean
- beans
下面来看一下alias标签、import标签如何解析的
alias标签解析
先去看下这个标签是如何进行使用的
<bean id="me" name="me0" class="xxx"/>
<alias name="me" alias="me1,me2"/>
在对bean提供名称的时候,Spring支持提供多个名称,所以有了alias标签去为bean指定别名,alias标签的alias的属性都指向同一个bean,起bean的id为me
下面就来看看容器是怎么解析alias标签的
对应的方法为processAliasRegistration
源码如下
protected void processAliasRegistration(Element ele) {
//获取alias标签里面的属性
//获取name属性(name属性为对应的beanid)
String name = ele.getAttribute(NAME_ATTRIBUTE);
//获取alias属性(alias属性为对应的别名)
String alias = ele.getAttribute(ALIAS_ATTRIBUTE);
//这个是一个校验操作结果标识
boolean valid = true;
//进行校验操作
//判断name属性和alias属性是否为空
if (!StringUtils.hasText(name)) {
//为空就报错
getReaderContext().error("Name must not be empty", ele);
valid = false;
}
if (!StringUtils.hasText(alias)) {
//为空就报错
getReaderContext().error("Alias must not be empty", ele);
valid = false;
}
//如果校验通过
if (valid) {
try {
//可以看到对应Alias标签没有进行复杂的解析
//这是因为alias标签就只有这两个属性
//使用registrt进行注册alias
getReaderContext().getRegistry().registerAlias(name, alias);
}
catch (Exception ex) {
getReaderContext().error("Failed to register alias '" + alias +
"' for bean with name '" + name + "'", ele, ex);
}
//通知监听器
getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
}
}
从代码上可以看到,对于alias标签没有bean标签的复杂,看到bean标签的解析流程,对于alias标签看起来就觉得很清秀
-
获取name、alias属性
-
进行校验操作
-
校验通过就进行注册alias标签
- 注册失败会抛错
-
alias标签注册成功就通知监听器
-
细节点:校验alias标签失败(name属性为空或者alias属性为空),不会抛错。。
-
注册工作交给AliasRegistrt接口(Bean的注册交给BeanDefinitionRegistry接口)
对于AliasRegistrt接口
可以看到这个接口是被BeanDefinitionRegistry接口继承了,而其默认的实现类依然是GenericApplicationContext
从代码上可以看到,GenericApplicationContext依然将注册的工作交给BeanFactory去做(DefaultListableBeanFactory)
但DefaultListableBeanFactory并没有重写这个方法,反而是交给了其父类SimpleAliasRegistry去实现了,所以归根到底是SimpleAliasRegistry负责Alias标签的注册
解析的源码如下
@Override
public void registerAlias(String name, String alias) {
//校验判断为空
Assert.hasText(name, "'name' must not be empty");
Assert.hasText(alias, "'alias' must not be empty");
//进行底层的aliasMap进行加锁
//(ConcurrentHashMap结构,而且这个容器在SimpleAliasRegistry中,默认容量为16)
//同样Spring
synchronized (this.aliasMap) {
//如果alias属性与name属性相同
if (alias.equals(name)) {
//会删除掉原来的注册过的alias标签。。。
//不太理解这种做法
//alias属性与name属性相等,其实就是没有起别名嘛
//而且Spring规定只有一个alias标签会起作用
//但干嘛要删除掉之前起的别名呢????
this.aliasMap.remove(alias);
if (logger.isDebugEnabled()) {
logger.debug("Alias definition '" + alias + "' ignored since it points to same name");
}
}
else {
// 如果alias与name属性不相同,证明alias标签是有效的
//从容器中获取这个alias标签对应的name
String registeredName = this.aliasMap.get(alias);
//判断是否存在
if (registeredName != null) {
//如果存在且name是相同的
//就代表这个alias标签重复了
//不需要进行重复注册,省略,所以如果有重复的alias并不会报错
if (registeredName.equals(name)) {
// An existing alias - no need to re-register
return;
}
//如果name不相同,那就代表出现歧义了
//判断是否可以进行覆盖
if (!allowAliasOverriding()) {
//如果不能进行覆盖就抛错
throw new IllegalStateException("Cannot define alias '" + alias + "' for name '" +
name + "': It is already registered for name '" + registeredName + "'.");
}
//如果可以进行覆盖,输出日志
if (logger.isDebugEnabled()) {
logger.debug("Overriding alias '" + alias + "' definition for registered name '" +
registeredName + "' with new target name '" + name + "'");
}
}
//再次检查判断alias标签是否注册
//这里判断蛮有意思的
checkForAliasCircle(name, alias);
//底层容器添加alias,key为alias,value为name
this.aliasMap.put(alias, name);
if (logger.isTraceEnabled()) {
logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'");
}
}
}
}
可以看到,其实alias的注册跟beanDefinition差不多
- 校验alias、name属性是否为空,不为空才能继续进行下一步
- 给底层容器进行上锁(明明已经是ConcurrentHashMap,却还要进行Synchronic上锁处理,没必要吧)
- 假如name和alias属性是相同的,代表alias标签是无效的,自己的别名起自己的名字?这是不符合常理的,所以,Spring的处理是认为这个alias标签是取消别名设置,即不需要别名(给自己的别名取自己的名字,不就是不需要别名吗?),就会将这个alias的之前的别名清空掉
- 假如不相同,代表alias标签是有效的,再判断之前是或否已经有注册过这个alias
- 如果注册过,判断是不是一样的name,一样的话直接Return
- 不一样的话,判断支不支持覆盖,不支持覆盖会报错
- 检查这个alias是否有循环引用问题
- 往aliasMap添加alias与name,alias为key,name为value
检查是否有循环引用问题
举个栗子
其实就是存在键值对,alias0 -> name0 -> … ->alias0,通过alias0寻找name0,然后通过name0寻找另外一个alias,一直找下去,发现结果最终回到了alias0,这就是循环引用
源码如下所示,注意上一层传的参数,此时name其实是alias,alias其实为name
public boolean hasAlias(String name, String alias) {
//根据name去获取看有没有另外一个循环引用的name
String registeredName = this.aliasMap.get(alias);
//判断获取到的registeredName是否和alias是否一样,如果一样就代表出现循环依赖了,直接返回true给上层然后抛错
//假如不一样,继续判断alias是否出现循环依赖
//递归一直到alias对应的registeredName为空获取出现循环引用了
return ObjectUtils.nullSafeEquals(registeredName, name) || (registeredName != null
&& hasAlias(name, registeredName));
}
可以看到,这里判断循环引用问题是一个递归过程
- 判断当前的alias是否出现循环引用,方式是判断有没有已经注册的alias标签的alias属性与当前的name属性是一致的
- 假如当前的name存在被引用,且其registeredName与alias是一致的,代表出现了循环引用问题
- 假如当前的name存在被引用,但其registeredName与alias并不一致,并且不为空,那就递归找alias与registeredName是否出现循环引用
- 假如如当前的name不存在被引用,代表没有出现循环引用问题
总结一下
对于alias标签的解析,有几个特殊需要注意的地方
- 循环引用问题的解决,递归去判断与当前alias标签的alias属性是否出现了循环引用
- 对于alias属性与name属性一致的问题,Spring会默认为取消alias别名
import标签解析
import标签可以让我们将庞大的ApplicationContext.xml细分成每个小的XML,比如Mybatis、SprigMVC进行拆分
下面就来看看是import标签是怎样解析的
源码如下
protected void importBeanDefinitionResource(Element ele) {
//获取import标签的resource属性
String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
//判空
if (!StringUtils.hasText(location)) {
getReaderContext().error("Resource location must not be empty", ele);
return;
}
// Resolve system properties: e.g. "${user.dir}"
//处理resource属性,因为可以采用配置环境方式的
location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);
Set<Resource> actualResources = new LinkedHashSet<>(4);
// Discover whether the location is an absolute or relative URI
boolean absoluteLocation = false;
try {
//判断是不是URL,如果不是URL就转换成URL并且判断是不是绝对路径
absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
}
//如果不能转化成URL代表可能是相对路径
//所以捕捉异常不会直接抛错,absoluteLocation仍然为false,代表为相对路径
catch (URISyntaxException ex) {
// cannot convert to an URI, considering the location relative
// unless it is the well-known Spring prefix "classpath*:"
}
// Absolute or relative?
//如果是相对路径
if (absoluteLocation) {
try {
//执行loadBeanDefinitions解析import的resource指向的XML文件
//前面已经看过了,就不赘述了
int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
if (logger.isTraceEnabled()) {
logger.trace("Imported " + importCount + " bean definitions from URL location [" + location + "]");
}
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error(
"Failed to import bean definitions from URL location [" + location + "]", ele, ex);
}
}
//如果是相对路径
else {
// No URL -> considering resource location as relative to the current file.
try {
//转换成相对路径
int importCount;
Resource relativeResource = getReaderContext().getResource().createRelative(location);
//判断相对路径是否存在
if (relativeResource.exists()) {
//如果存在进行解析XML文件
importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
actualResources.add(relativeResource);
}
//假如相对路径不存在
//代表可能改了相对地址
else {
//尝试将路径改为相对地址+相对路径再去解析XML文件
String baseLocation = getReaderContext().getResource().getURL().toString();
importCount = getReaderContext().getReader().loadBeanDefinitions(
StringUtils.applyRelativePath(baseLocation, location), actualResources);
}
if (logger.isTraceEnabled()) {
logger.trace("Imported " + importCount + " bean definitions from relative location [" + location + "]");
}
}
catch (IOException ex) {
getReaderContext().error("Failed to resolve current resource location", ele, ex);
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error(
"Failed to import bean definitions from relative location [" + location + "]", ele, ex);
}
}
//通知监听器
Resource[] actResArray = actualResources.toArray(new Resource[0]);
getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}
可以看到Import标签其实本质上就是解析了resource属性,然后去调用loadBeanDefinitions去解析XML文件,所以关键就在于对路径的解析和判断而已
- 首先判断是不是URL
- 如果不是URL,转化成URI,并且判断是不是绝对路径
- 如果是URL,那就代表是绝对路径了,解析XML文件
- 如果不是URL,并且转化成URI失败,那就代表可能是相对路径(所以捕捉转化失败的异常不会进行处理)
- 尝试转化为相对路径
- 如果转化成功,那就根据路径去解析XML文件
- 如果转化失败,那就尝试使用拼接的方式计算出绝对路径,拼接当前总配置文件的路径和resource属性形成路径,然后去解析XML文件
- 最后通知监听器,解析完成
Beans标签解析
其实这个本质上就是循环递归调用beans的解析过程
从之前解析Bean标签的时候可以看出,其实本质上就是循环递归去调用parseDefault方法,对于beans下的子标签进行遍历,递归调用parseDefaultElement方法,又判断这个子标签是4个标签的哪个(beans,bean,import,alias),对应又执行哪种标签的解析方法,
至此,Spring对配置文件的默认4个标签的解析就结束了