【Spring】xml解析和BeanDefinition封装

Spring扫描一个Java Bean到内部容器,可以有两种方式。

  1. 第一种在xml里面配置<bean/>标签,指定扫描路径下的类文件。
  <bean class="com.wintig.bean.Student" id="student" scope="singleton" primary="true"/>
  1. 第二种使用一个特殊的标签,指定要扫描路径下的所有文件
<context:component-scan base-package="com.wintig"/>

第二种明显更试用与我们的工程,毕竟类文件那么多,我们不可能一个个去配置吧。底层究竟是如何实现的,今天我们就来揭开这里面的奥妙。

Java Bean和Spring Bean

Java Bean简单来说就是运行在JVM平台上的一个个对象,我们平常New User()会经过一系列加载验证,然后从方法区中获取类的模板信息,然后基于类模板信息在堆中为其分配内存。所以Class就是JVM建模对象。

Spring对Class又进一步的增强,因为Class里面的基础信息并不能满足Spring的业务需要,所以Spring引入BeanDegingion,里面不仅仅包含了类的Class信息,还包括比如bean的作用域,是否懒加载,是否是个抽象类等信息。

所以Spring中的Bean就是基于DeanDefinition抽象出来的信息,然后经历一系列生命周期而生成类,就叫Spring的Bean对象。

自定义标签讲解

首先启动Spring容器使用ClassPathXmlApplicationContext去加载spring.xml(配置如上)

public static void main(String[] args) {
    
    ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
    
}

顺着ClassPathXmlApplicationContext的refresh()方法,就来到了我们今天的核心方法obtainFreshBeanFactory()

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

这个方法主要做的事情很好理解

  1. 创建一个BeanFactory工厂对象
  2. 之后根据我们传入的"spring.xml"对里面的配置类进行解析
  3. 把解析出来的xml标签封装成BeanDefinition对象

也很好理解,spring也没有什么神秘的。其实是这样的,前2步虽然描述很简单,但是具体实现也只是多了很多校验和为了扩展性做的设计在里面,都很好想到。但是第3步就要费点脑筋了,对标签的解析我们最开始也说道了,Spring有两类标签形如<Bean>的默认标签,我们可以直接解析;还有一类<context:component-scan>自定义标签。

这两种标签的解析最终是由parseBeanDefinitions方法去完成的,我们接下来分情况进行讲解。

    protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        if (delegate.isDefaultNamespace(root)) {
            NodeList nl = root.getChildNodes();
            // 从xml的根节点开始,循环的解析所有的变迁
            for (int i = 0; i < nl.getLength(); i++) {
                Node node = nl.item(i);
                if (node instanceof Element) {
                    Element ele = (Element) node;
                    // 是否有namespace,如果拿不到说明是默认标签
                    if (delegate.isDefaultNamespace(ele)) {
                        // 默认标签解析 <bean>
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        // 自定义标签解析  有前缀的 <context:component-scan base-package="com.wintig"/>
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

接下来,我们分别对这两类标签进行展开解析。

默认标签

默认标签标签除了上面所说的<bean>,还有<import>、<alias>、<beans>核心的其实就是bean标签。默认标签其实没啥好讲的,因为标签里面已经包含了所有的信息。

自定义标签

默认标签很简单吧,但是我们请看spring是如何判断一个标签是默认标签或自定义标签的?它通过了一个isDefaultNamespace方法来判断,意思就是说如果这个标签没有对应的namespace他就属于默认标签。namespace是什么?他就是xml文件最上面的xmlns约束。

<?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"
           
    <context:component-scan base-package="com.wintig"/>
    
</beans>

这时候我们来到了<context>标签,我们扫描到了<context>标签有对应的namespace所以会走自定义标签解析,就会进入​​parseCustomElement​方法。

    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
        // <context:component-scan/>
        // 根据标签拿到namespaceUri,根据namespaceUri拿到对应的NamespaceHandler处理类
        String namespaceUri = getNamespaceURI(ele);
        if (namespaceUri == null) {
            return null;
        }
        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
        if (handler == null) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
            return null;
        }
        // 解析默认标签的时候调用的decorate(装饰),而这里调用的是parse(解析)
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }

parseCustomElement

parseCustomElement方法首先调用了一个getNamespaceURI方法,这个方法是根据<context>标签拿到跟后根据xml上面的xmlns获取到namespaceUri。<context>标签对应的namespaceUri是​​http://www.springframework.org/schema/context

resolve

根据标签然后拿到namespaceUri,接下来就进入了解析过程,这个方法很复杂我对代码进行了简化如下:

public NamespaceHandler resolve(String namespaceUri) {
    
    // 获取spring中所有jar包里面的"/META-INF/spring.handlers"文件
    // 并且建立uri-class的映射关系
    Map<String, Object> handlerMappings = getHandlerMappings();

    // 根据namespaceUri=http://www.springframework.org/schema/p,获取这个命名空间的处理类
    // 通过建立的uri-class的映射关系,通过一个namespaceUri就可以唯一的确定一个处理类
    Object handlerOrClassName = handlerMappings.get(namespaceUri);

    String className = (String) handlerOrClassName;

    // 反射获取Class
    Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);

    // 然后实例化
    NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);

    // 调用处理类的init方法,在init方法中完成标签元素解析类的注册
    namespaceHandler.init();
    handlerMappings.put(namespaceUri, namespaceHandler);
    return namespaceHandler;
}

getHandlerMappings()

首先会调用getHandlerMappings方法,接下来根据根据namespaceUri拿到对应的处理类的映射关系,这里使用了SPI的思想。spring会加载所有jar包下面的​​resource/META-INF/spring.handlers​文件,我们打开spring.handlers文件发现里面存着一系列的url-class的映射关系结构的信息如下,根据如下信息建立映射关系。

http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler

然后我们namespaceUri拿到对应的NamespaceHandler处理类。然后根据http://www.springframework.org/schema/context就可以找到一个对应的处理类​​org.springframework.context.config.ContextNamespaceHandler

public class ContextNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
        registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
        registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
        registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
        registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
        registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
        registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
        registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
    }
}

接下来的操作就很简单了

  1. 反射生成类
  2. 调用init()方法
  3. 把标签对应的处理类注册到spring容器里面
  4. 建立namespaceUri和处理类的映射关系

我们有了NamespaceHandler处理类,接下来就根据子标签​​<context:component-scan/>​获取子标签component-scan​获取子标签处理类ComponentScanBeanDefinitionParser然后调用它的parse方法​

public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 获取basePackage属性:要扫描的路径
    String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
    basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
    // 可以用逗号分开
    String[] basePackages = StringUtils.tokenizeToStringArray(basePackage,
            ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

    // Actually scan for bean definitions and register them.
    // 创建注解扫描器
    ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);

    // + 扫描并把扫描的类封装成beanDefinition对象
    //  1 根据basePackage路径,递归扫描文件夹
    //  2 使用ASM技术,把找到的类封装成metadataReader,
    //  3 遍历metadataReader,如果类属性上有注解则完成bd注册
    Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);

    // + 基本组件注册 beanPostProcess
    registerComponents(parserContext.getReaderContext(), beanDefinitions, element);

    return null;
}

到这里其实已经很清楚了,自定义标签的实质其实就是配置namespace告诉spring你要处理的这个标签我来处理,然后根据spi找到对应的处理类。不同的子标签对应不同的处理逻辑,这也就是策略模式。

猜你喜欢

转载自blog.csdn.net/qq_25448409/article/details/111502225