面试官:给我讲讲Spring加载配置

面试官:给我讲讲Spring加载配置

场景回顾

大家新年好呀,我是小张,今天是复工的第一天,回到寝室,小叶又闷闷不乐,也就有了如下场景。

面试官:小叶你好,我看简历上写了精通Spring,那么我想问一下在Spring中我们肯定会编写很多配置文件提供给Spring加载,那么你是怎么做的呢?

小叶:嗯嗯,我们在项目中会涉及到很多配置文件,比如数据库、Redis等多个配置文件,在早期Spring使用XML的时候,我们一般会定义一个.xml文件里面包含各种配置信息提供给容器加载。

面试官:嗯嗯,配置文件是使用XML格式的,那么里面用到的很多标签都是Spring提供给我们的,那在XML配置中是否可以包含我自定义的标签呢?

小叶:好像不可以自定义标签吧?(难道XML配置文件还可以自定义标签?应该不能吧)

面试官:今天的面试先到这里了。

案例编写

如下,在平时的开发中我们一般会在Resource目录下定义一个.xml文件里面定义Bean的信息。

<?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"
       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 https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

        <bean class="com.peter.person">
        </bean>
</beans>

然后在Main方法中编写如下代码启动容器。

public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("peter.xml");
        context.getBean("person");
    }

通过以上几行代码我们就可以获取到person对象,那么解析配置文件的代码肯定在实例化Spring Context上下文中完成的,我们也就可以通过DEBUG定位到何处调用了配置解析,找到具体解析配置文件的地方我们也就可以知道框架是否给我们提供了扩展的能力。

源码DEBUG

通过一路DEBUG,我们会发现加载XML配置文件存放于此方法中AbstractRefreshableApplicationContext#refreshBeanFactory。

@Override
    protected final void refreshBeanFactory() throws BeansException {
        ...
        try {
            ...
            // 加载BeanDefinition
            loadBeanDefinitions(beanFactory);
            ...
        }
        catch (IOException ex) {
            throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
        }
    }

我们会看到在这个方法里面调用了loadBeanDefinitions,我们暂时还不清楚该类里面做了什么操作,但通过名字我们也能够大概的猜出来它通过配置文件加载了配置,这也是我们平时需要学习的一个点,要做到见名知起意。

BeanDefinition

我们甚至可以大胆的猜测一下BeanDefinition可能会是一个接口,里面包含了配置文件解析后应该有的属性,那么我们就来小心验证一下是不是会有这个接口。

果不其然我们在BeanDefinition接口中发现了大量我们熟悉的属性:scope、beanClassName等我们会在XML文件中定义的属性,如果有必要的话通过接口我们可以很方便的扩展自己的BeanDefinition。

加载BeanDefinition

@Override
    protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
        // 创建Reader 使用适配器模式
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
        ...
        // 开始完成beanDefinition的加载
        loadBeanDefinitions(beanDefinitionReader);
    }

    protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
        // 以Resource的方式获得配置文件的资源位置
        Resource[] configResources = getConfigResources();
        if (configResources != null) {
            reader.loadBeanDefinitions(configResources);
        }
        // 以String的形式获得配置文件的位置
        String[] configLocations = getConfigLocations();
        if (configLocations != null) {
            reader.loadBeanDefinitions(configLocations);
        }
    }

    @Override
    public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {
        Assert.notNull(locations, "Location array must not be null");
        int count = 0;
        for (String location : locations) {
            count += loadBeanDefinitions(location);
        }
        return count;
    }

我们会看到这里通过将beanFactory对象传递给Reader构造函数,直接构造出XmlBeanDefinitionReader对象,这里就是我们熟悉的适配器模式

通过源码我们可以看出容器是支持多配置文件的读取的,框架将会循环从配置文件中获取BeanDefinition,继续DEBUG我们将会走到doLoadBeanDefinitions方法,在Spring源码中以do开头的函数都是实际干活的函数,也就是具体的实现。

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {

        try {
            // 此处获取xml文件的document对象
            Document doc = doLoadDocument(inputSource, resource);
            // 解析document对象并把解析的BeanDefinition添加到容器中
            int count = registerBeanDefinitions(doc, resource);
            if (logger.isDebugEnabled()) {
                logger.debug("Loaded " + count + " bean definitions from " + resource);
            }
            return count;
        }
        ...省略异常捕捉...
    }

相信了解XML解析的小伙伴看到Document很熟悉,因为这个对象代表XML文件已经被我们解析成了ROOT结点,我们能通过该对象遍历所有结点获取XML信息。

接下来我们进入registerBeanDefinitions方法中,我们会发现又出现了一些不认识的类,这也体现了Spring框架的灵活性,对于类的设计是专责模式,每个类都是其着相关的作用,不会将不相关的内容放入一个接口/类中。

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        // 对xml的beanDefinition进行解析
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        // 获取之前容器中已有的BeanDefinition
        int countBefore = getRegistry().getBeanDefinitionCount();
        // 完成具体的解析过程
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

此处documentReader作用就是解析XML文件并将BeanDefinition添加到容器中,这里的register可以理解为添加。

接着代码将会进入doRegisterBeanDefinitions方法,又看到了do方法大家应该可以见名知意了吧。

protected void doRegisterBeanDefinitions(Element root) {
        ...
        // 解析前处理
        preProcessXml(root);
        // 执行解析
        parseBeanDefinitions(root, this.delegate);
        // 解析后处理
        postProcessXml(root);
        ...
    }

protected void preProcessXml(Element root) {
    }
protected void postProcessXml(Element root) {
    }

看到preProcessXml和postProcessXml这两个方法大家可能会觉得很奇怪,怎么里面会是空的实现?其实这里是框架提供给用户做扩展使用,我们只要子类继承并重写这两个方法也就扩展了该方法。

我们将视线转移到具体解析节点的地方,我们会看到一些命名空间相关内容,如果对于xml没有基础的同学可以通过 xml命名空间一文了解命名空间是什么。

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.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

public boolean isDefaultNamespace(@Nullable String namespaceUri) {
        return !StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri);
    }

public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";

通过源码我们会发现,Spring会将beans作为默认的命名空间,但是我们在配置文件中,还见到过<context:component-scan base-package="xxx"/>这种配置文件,在配置文件我们会发现context的命名空间是xmlns:context="http://www.springframework.org/schema/context",并不是默认的命名空间,那么Spring是如何实现的呢?

通过源码我们会看出如当前节点命名空间非默认节点的话,会进入parseCustomElement方法,那么非默认命名空间的解析肯定在这里了。

    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
        // 获取对应的命名空间
        String namespaceUri = getNamespaceURI(ele);
        if (namespaceUri == null) {
            return null;
        }
        // 根据命名空间找到对应的NamespaceHandlerspring
        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
        if (handler == null) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
            return null;
        }
        // 调用自定义的NamespaceHandler进行解析
        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
    }

通过源码我们将非默认命名空间去寻找NamespaceHandler,具体起什么作用将在下面展示,我们先看如何通过命名空间去寻找执行器。

@Override
    @Nullable
    public NamespaceHandler resolve(String namespaceUri) {
        // 获取所有已经配置好的handler映射
        Map<String, Object> handlerMappings = getHandlerMappings();
        // 根据命名空间找到对应的信息
        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);
                // 实例化类
                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
                // 调用自定义的namespaceHandler的初始化方法
                namespaceHandler.init();
                // 讲结果记录在缓存中
                handlerMappings.put(namespaceUri, namespaceHandler);
                return namespaceHandler;
            }
        }
    }

我们会发现它在META-INF目录下存放了一个spring-hadlers的文件,里面就保存着命名空间和解析器的关系。

下图展示了存放在spring-beans项目中的映射关系,那么我们也就可以照葫芦画瓢造出一个自定义标签解析器来。

通过上图我们会发现还有一个名为spring.schemas的文件,里面存放着schem URL和schema文件路径的关系,它是XML文件解析成Document对象时,通过schema文件进行校验的。

开始扩展

通过以上源码的阅读,我们可以自定义标签需要以下几步。

  1. 编写schema文件
  2. 在META-INF/spring.schemas文件中编写schema url和schema文件的映射关系
  3. 创建自定义标签解析器类
  4. 在META-INF/spring.handlers文件中编写schema url和自定义标签解析器的映射关系

Schema文件编写

我们可以参考spring-beans.xsd编写一个属于自己的xsd文件。

<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.xxx.com/schema/peter"
        xmlns:tns="http://www.xxx.com/schema/peter"
        elementFormDefault="qualified">
<element name="user">
    <complexType>
        <attribute name ="id" type = "string"/>
        <attribute name ="userName" type = "string"/>
    </complexType>
</element>
</schema>

设置schema映射关系

http\://www.xxx.com/schema/peter.xsd=peter.xsd

创建标签解析器类

public class User {
    private String username;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

public class UserHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }

    private static class UserBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser {

        @Override
        protected Class<?> getBeanClass(Element element) {
            return User.class;
        }

        @Override
        protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
            String username = element.getAttribute("userName");
            if (!StringUtils.hasText(username)) {
                builder.addPropertyValue("username", username);
            }

        }
    }
}

创建标签解析器映射关系

http\://www.xxx.com/schema/peter=com.peter.UserHandler

编写配置文件

<?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"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:peter="http://www.xxx.com/schema/peter"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.xxx.com/schema/peter http://www.xxx.com/schema/peter.xsd">
        <peter:user id="peter" userName="peter"/>
</beans>

程序启动

public class Main {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("peter.xml");
        User bean = context.getBean(User.class);
        System.out.println();
    }
}

写在最后

通过Spring读取配置文件的源码阅读,我们可以很清楚的通过方法名和类名去读懂大概的意思,这在平时的开发中其实是非常重要的一点,如果做到见名知其义,那么其他人在阅读你的代码的效率就会大幅上升。我们在平时的开发中也可以借鉴这套解析的思想实现灵活的扩展,最后很多人读过Spring源码都会有一个感受,就是类太复杂太多了,其实Spring框架为了灵活的扩展定义了很多接口,并围绕设计模式进行开发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值