Spring源码分析:
- Spring源码之XML默认标签解析
- Spring自定义标签 component-scan 源码解析
自定义标签解析
除了 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 ,其中之一的作用就是可以表示继承关系。
|