面试官:给我讲讲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框架为了灵便的扩大定义了很多接口,并围绕设计模式进行开发。