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

11次阅读

共计 9723 个字符,预计需要花费 25 分钟才能阅读完成。

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

正文完
 0