前面松哥写文章和小伙伴们仔细捋了捋 Spring 中的 BeanDefinition 存在的几种情况,那么 BeanDefinition 是谁来加载呢?如果是 Java 代码配置,那不用说,都是注解扫描去加载 BeanDefinition 的,但是如果是 XML 或者其他格式的配置文件,则有专门的 BeanDefinition 加载器,今天咱们就来看看这个专门的 BeanDefinition 加载器。
1. BeanDefinitionReader
这是一个接口,从名字上就能看出来专门用来读取 BeanDefinition,接口如下:
代码语言:javascript复制public interface BeanDefinitionReader {
BeanDefinitionRegistry getRegistry();
@Nullable
ResourceLoader getResourceLoader();
@Nullable
ClassLoader getBeanClassLoader();
BeanNameGenerator getBeanNameGenerator();
int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}
这里几个方法我来说一下:
- getRegistry:这个是获取一个 BeanDefinition 的注册器,就是把 BeanDefinition 加载出来之后,我们要将之注册到容器中,所以这里的 BeanDefinitionRegistry 说白了其实就是某一个容器。
- getResourceLoader:这个是获取配置资源的加载器,像我们平时加载 XML 配置文件的时候,只需要写文件名即可,系统会自动去 classpath 下查找文件,就依赖于这里的 ResourceLoader。
- getBeanClassLoader:这个地方就是返回类加载器。
- getBeanNameGenerator:这个是获取 beanName 的生成器,有的时候,我们在 XML 文件中配置的时候,可能没有给 Bean 设置 name 或者 id 属性,那么将会通过这个生成器去生成 beanName,这个松哥在最近录制的 Spring 源码分析视频中已经详细分析过了,这里就不再啰嗦了。
- loadBeanDefinitions:这是四个重载方法,都是根据传入的配置文件,去加载 BeanDefinition。虽然有四个重载方法,真正去加载 BeanDefinition 的方法其实是
loadBeanDefinitions(Resource resource)
,另外三个方法兜兜转转最终都会来到这个方法中。
2. AbstractBeanDefinitionReader
BeanDefinitionReader 是接口,定义了操作规范,而 AbstractBeanDefinitionReader 为这个接口提供基本的实现。AbstractBeanDefinitionReader 也还有子类,不过 AbstractBeanDefinitionReader 的子类主要是为了处理不同类型的配置文件,如 properties 配置、XML 配置等,其他的通用操作基本上就由 AbstractBeanDefinitionReader 来完成了。
这个类的源码比较长,我这里就不贴出来了。该类主要解决了 getRegistry、getResourceLoader、getBeanClassLoader 以及 getBeanNameGenerator 方法的返回值,这四个方法的返回值其实都好处理,传入对应的值返回就行了。
loadBeanDefinitions 由于存在四个重载方法,这个方法的大致处理流程是 loadBeanDefinitions(String... locations)
调用 loadBeanDefinitions(String location)
,loadBeanDefinitions(String location)
调用 loadBeanDefinitions(Resource... resources)
,loadBeanDefinitions(Resource... resources)
则调用 loadBeanDefinitions(Resource resource)
,所以最后的重任都落在 loadBeanDefinitions(Resource resource)
方法上了,另外三个重载方法只是为了处理加载的配置资源而已。而真正干活的 loadBeanDefinitions(Resource resource)
方法在 AbstractBeanDefinitionReader 类中并未重写,这个将来要在各个具体的实现类里边去完成了。
2.1 PropertiesBeanDefinitionReader
这个是用来读取 properties 配置文件的,我们平时可能通过 XML 文件来配置 Bean,其实 Spring 里边也支持使用 properties 来配置 Bean,不过 properties 配置起来比较麻烦,所以这个配置目前已经废弃了。
2.2 XmlBeanDefinitionReader
这个就是各位小伙伴最熟悉的通过 XML 文件来配置 Bean 了。经过前面的解析,小伙伴们已经知道了 XmlBeanDefinitionReader 是重写了 loadBeanDefinitions(Resource resource)
方法,然后通过这个方法去加载配置文件的。
@Override
public int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException {
return loadBeanDefinitions(new EncodedResource(resource));
}
可以看到,这里将传入的 resource 包装为一个 EncodedResource 对象,然后调用另外一个重载方法,EncodedResource 对象主要是处理配置文件的编码问题,防止出现乱码。
代码语言:javascript复制public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " encodedResource " - check your import definitions!");
}
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
当前类里边定义了一个 resourcesCurrentlyBeingLoaded 对象,这是一个 ThreadLocal,用来保存当前正在解析的 XML 文件,首先会尝试将当前的要解析的 encodedResource 加入到 currentResources 集合中,如果加不进去,说明当前的 encodedResource 正在处理中,那么就抛出异常。
否则就去读取 encodedResource 中的内容,并将之转为一个 InputSource,这个 InputSource 是 XML 解析中用到的对象。然后调用 doLoadBeanDefinitions 方法去做进一步的解析。
代码语言:javascript复制protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
Document doc = doLoadDocument(inputSource, resource);
int count = registerBeanDefinitions(doc, resource);
return count;
}
这个方法首先调用 doLoadDocument 将 XML 文件解析为一个 Document 对象。
Document 就是 XML 解析时获取到的文档对象,Document 对象代表了一个 XML 文档的模型树,所有的其他 Node 都以一定的顺序包含在 Document 对象之内,排列成一个树状结构,以后对 XML 文档的所有操作都与解析器无关,直接在这个 Document 对象上进行操作即可。主流的 XML 解析方式有 SAX 解析、DOM 解析以及 Pull 解析。如果大家对于 XML 文件解析不熟悉的话,可以自行复习,松哥这里就不再啰嗦了。
接下来就是调用 registerBeanDefinitions 方法去解析这个 doc 对象,这个方法的返回值就是一共发现了多少个 BeanDefinition。
代码语言:javascript复制public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
int countBefore = getRegistry().getBeanDefinitionCount();
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
return getRegistry().getBeanDefinitionCount() - countBefore;
}
可以看到,这里又有了一个新的对象是 BeanDefinitionDocumentReader,这是一个接口,该接口只有一个实现类 DefaultBeanDefinitionDocumentReader,并且这个接口中也只有一个 registerBeanDefinitions 方法,这个方法就是用来解析 Document 对象并注册成为 BeanDefinition 的。
代码语言:javascript复制@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
doRegisterBeanDefinitions(doc.getDocumentElement());
}
protected void doRegisterBeanDefinitions(Element root) {
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
return;
}
}
}
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);
this.delegate = parent;
}
小伙伴们看到,从 doRegisterBeanDefinitions 方法就开始 XML 解析了,这里首先读取节点的 profile 属性,由于 profile 属性在配置的时候,可以有多个,多个的话用 ,
、;
或者空格隔开,所以这里拿到 profile 之后,先用 MULTI_VALUE_ATTRIBUTE_DELIMITERS
去做一个字符串拆分(还记得松哥之前讲的 name 属性的配置方式吗?就是那个!),将 profile 属性值拆分为一个数组 specifiedProfiles,然后判断系统当前环境是否包含 profile 中的值,如果不包含,就直接 return,后面的内容就不需要解析了。
接下来就是 XML 处理了,有预处理和后置处理两个方法,但是这两个目前都是空方法,没有实现。中间的 parseBeanDefinitions 方法是 XML 解析方法:
代码语言:javascript复制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 ele) {
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
这里就是遍历 XML 中的所有子节点,遍历然后判断这个子节点是默认标签还是自定义标签,像我们平时写的 import
、alias
、bean
、beans
这些都算是默认标签,而像导入 properties 的 <context:property-placeholder location="classpath:db.properties"/>
这种标签就属于自定义标签。
我们先来看默认标签的解析:
代码语言:javascript复制private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}
大家看到,根据不同的节点类型,调用不同的解析方法。如果节点是 beans,beans 中还是定义 bean 的,所以对于 beans 标签递归调用 doRegisterBeanDefinitions 方法进行解析。
咱们看一个比较常见的 bean 标签解析吧,其他标签解析也都差不多:
代码语言:javascript复制protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
try {
// Register the final decorated instance.
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));
}
}
这个方法做了三件事:
- 首先调用
delegate.parseBeanDefinitionElement
方法去解析 Element,解析 XML 标签中的各种属性值,解析完成之后,将属性值填充到 BeanDefinition 对象中并返回,这里创建的 BeanDefinition 类型就是 GenericBeanDefinition 类型的。 delegate.decorateBeanDefinitionIfRequired
方法去检查当前 Bean 的各种属性是否需要装饰。什么样的情况需要装饰呢?这个主要是针对一些比较特殊的属性,例如我们之前讲 Bean 的属性注入的时候,有一种叫做 p namespace 的属性注入,就是在标签中添加p:username='javaboy'
来实现属性值的注入,那么这种属性就需要特殊处理,这个方法就是用来解决这一类问题的。- 调用
BeanDefinitionReaderUtils.registerBeanDefinition
方法完成 BeanDefinition 的注册,注册到一个 Map 集合中,集合的 key 就是 beanName,集合的 value 则是 BeanDefinition 对象。
好啦,这就是 XmlBeanDefinitionReader 中的一个大致流程。
2.3 GroovyBeanDefinitionReader
这个是使用 groovy 脚本配置 Bean,groovy 我们这里就不展开了。
今天就主要和小伙伴们介绍了 Spring 中 XML 文件的加载规则。