spring源碼學習之1--spring擴展之自定義XmlWebApplicationContext和DefaultBeanDefinitionDocumentReader實現動態修改配置文件內容

spring擴展之自定義XmlWebApplicationContext和DefaultBeanDefinitionDocumentReader

斷點加載配置文件的流程

  1. 首先我們在AbstractApplicationContext文件的refresh()方法加上斷點
    在這裏插入圖片描述
  2. 進入obtainFreshBeanFactory()方法
  3. 進入refreshBeanFactory()方法,創建了默認的DefaultListableBeanFactory
  4. 進入loadBeanDefinitions(beanFactory)方法,根據上面創建發factory去加載配置文件到beandefinition
    在這裏插入圖片描述
  5. 可以看到initBeanDefinitionReader()方法在XmlWebApplicationContext類裏面第空實現。也就是說如果我們自定義一個類集成XmlWebApplicationContext後,再實現XmlWebApplicationContext的initBeanDefinitionReader()方法,然後loadBeanDefinitions()方法就會調用我們自定義的initBeanDefinitionReader()方法。這個是本文第一個需要擴展的地方。
  6. 進入loadBeanDefinitions(beanDefinitionReader)方法
  7. 進入reader.loadBeanDefinitions(configLocation)方法
  8. 進入loadBeanDefinitions(location, null)方法
  9. 進入loadBeanDefinitions(Resource… resources)方法
  10. 進入doLoadBeanDefinitions(InputSource inputSource, Resource resource)方法
  11. 進入registerBeanDefinitions(Document doc, Resource resource)方法
  12. 進入
    在這裏插入圖片描述
  13. 在這裏插入圖片描述
  14. this.documentReaderClass的默認初始化是private Class<?> documentReaderClass = DefaultBeanDefinitionDocumentReader.class;
  15. 進入documentReader.registerBeanDefinitions(doc, createReaderContext(resource))方法
  16. 進入doRegisterBeanDefinitions(root)方法
  17. 在這裏插入圖片描述
  18. 可以看到,在doRegisterBeanDefinitions方法裏面處理Element節點的信息的時候會有一個preProcessXml()方法和一個postProcessXml()方法,這2個方法都是空實現。
  19. Element節點就是加載xml配置文件後的信息存放的地方,例如一個配置爲
<context:component-scan base-package="com.tqy.document.reader.extention.service"/>

的配置文件,加載都Element後,使用

((DeferredElementNSImpl)element).getNodeName()得到的就是"context:component-scan"

例如配置爲

<bean class="com.tqy.document.reader.extention.test.Test" id="test"/>

使用

((DeferredElementNSImpl)element).getAttributeNode("class")得到的就是"com.tqy.document.reader.extention.test.Test"
  1. 由於doRegisterBeanDefinitions(Element root)方法是DefaultBeanDefinitionDocumentReader類的方法,而前面又提到的第二個擴展到設置的就是這個類,所以如果我們能在設置這個類的時候改變DefaultBeanDefinitionDocumentReader爲我們自己的,就可以成功覆蓋執行preProcessXml()和postProcessXml()方法了

自定義配置文件和類文件實現修改配置

  1. XmlWebApplicationContext類的加載時機:在FrameworkServlet類初始化的時候,默認就定義了
	public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;

而我們在web.xml裏面配置的DispatcherServlet默認就是繼承的FrameworkServlet
所以,我們在web.xml裏面配置的時候就可以加上一個默認的參數

  <servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <!--配置dispatcher.xml作爲mvc的配置文件-->
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:dispatcher-servlet.xml</param-value>
    </init-param>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>com.tqy.document.reader.extention.context.MyXmlWebApplicationContext</param-value> //使用自定義的MyXmlWebApplicationContext
    </init-param>
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
  </servlet>
  <servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

此時,就使用了我們自己定義的MyXmlWebApplicationContext替換了原始的XmlWebApplicationContext

  1. MyXmlWebApplicationContext只是提供了initBeanDefinitionReader( XmlBeanDefinitionReader beanDefinitionReader )方法覆蓋調用的機會,這個方法裏面可以做一些我們想做的事。比如,改變DefaultBeanDefinitionDocumentReader.class爲我們自己的。代碼如下
package com.tqy.document.reader.extention.context;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.web.context.support.XmlWebApplicationContext;

/**
 * @author tengqingya
 * @create 2019-04-10 19:24
 */
public class MyXmlWebApplicationContext extends XmlWebApplicationContext {
    private static final Logger LOGGER = Logger.getLogger( MyXmlWebApplicationContext.class );
    @Override
    protected void initBeanDefinitionReader( XmlBeanDefinitionReader beanDefinitionReader ) {
        LOGGER.info("initBeanDefinitionReader......................");
        beanDefinitionReader.setDocumentReaderClass(MyDefaultBeanDefinitionDocumentReader.class);
    }
}
  1. MyDefaultBeanDefinitionDocumentReader類中複寫了preProcessXml()和postProcessXml()方法,這樣,當調用MyDefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(Element root)方法時,裏面調用的preProcessXml()和postProcessXml()方法就是我們自定義的方法了。就可以達到修改Element的目的。而Element是實例化bean的基礎配置信息,所以就可以做到動態修改配置文件的信息,爲所欲爲。
  2. MyDefaultBeanDefinitionDocumentReader類如下
package com.tqy.document.reader.extention.context;

import com.sun.org.apache.xerces.internal.dom.DeferredElementNSImpl;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * @author tengqingya
 * @create 2019-04-10 19:31
 */
public class MyDefaultBeanDefinitionDocumentReader extends DefaultBeanDefinitionDocumentReader {
    private static final Logger LOGGER = Logger.getLogger(MyDefaultBeanDefinitionDocumentReader.class);

    @Override
    protected void preProcessXml( Element root ) {
        LOGGER.info("preProcessXml...");
        NodeList nl = root.getChildNodes();
        for( int i = 0; i < nl.getLength(); i++ ) {
            Node node = nl.item(i);
            if( node instanceof Element && node instanceof DeferredElementNSImpl ) {
                DeferredElementNSImpl deferredElementNS = (DeferredElementNSImpl)node;
                String nodeName = deferredElementNS.getNodeName();
                if( "context:component-scan".equals(nodeName) ) {
                    NamedNodeMap attributes = deferredElementNS.getAttributes();
                    LOGGER.info(attributes.getNamedItem("base-package"));
                }
                NamedNodeMap attributes = deferredElementNS.getAttributes();
                LOGGER.info(attributes.getNamedItem("class"));
                if( "bean".equals(nodeName) && attributes.getNamedItem("class").toString().contains("extention") ) {

                    Attr aClass = deferredElementNS.getAttributeNode("class");
                    Attr id = deferredElementNS.getAttributeNode("id");
                    aClass.setValue("com.tqy.document.reader.extention.test.Test2");
                    id.setValue("test2");
                }
                //base-package="com.tqy.document.reader.extention.controller"
                //id="viewResolver"
            }
        }
    }

    @Override
    protected void postProcessXml( Element root ) {
        LOGGER.info("postProcessXml...");
        String bean = root.getAttribute("bean");
        NodeList context = root.getElementsByTagName("context");
        String nodeName = root.getNodeName();
        LOGGER.info(bean);
        LOGGER.info(context);
        LOGGER.info(nodeName);
    }
}

其中關鍵代碼就是

					Attr aClass = deferredElementNS.getAttributeNode("class");
                    Attr id = deferredElementNS.getAttributeNode("id");
                    aClass.setValue("com.tqy.document.reader.extention.test.Test2");
                    id.setValue("test2");

含義是:
找到節點的class屬性"com.tqy.document.reader.extention.test.Test"替換成新的"com.tqy.document.reader.extention.test.Test2"
找到節點的id屬性"test"替換成"test2"
也就相當於把配置

<bean class="com.tqy.document.reader.extention.test.Test" id="test"/>

替換成了

<bean class="com.tqy.document.reader.extention.test2.Test" id="test2"/>

所以我們在其他地方使用bean的時候,就可以直接使用test2,但是使用不了test。

controller文件

package com.tqy.document.reader.extention.controller;

import com.tqy.document.reader.extention.test.Test2;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * @author tengqingya
 * @create 2019-04-10 13:05
 */
@Controller
public class TestController {
    /**
     * 此處注入的test2沒有在配置文件中配置
     * 配置文件中配置的是test
     * 在preProcessXml中修改test的配置爲test2
     */
    @Autowired
    private Test2 test2;
    private static final Logger LOGGER = Logger.getLogger( TestController.class );
    @RequestMapping("/test")
    @ResponseBody
    public String test( HttpServletRequest httpRequest ){
        LOGGER.info(httpRequest.getRequestURI());
        return test2.test("hello ");
    }
}

效果展示

  1. 直接注入test2報錯在這裏插入圖片描述
  2. 但是運行卻正常在這裏插入圖片描述
  3. 直接配置test,不報錯在這裏插入圖片描述
  4. 但是運行報錯在這裏插入圖片描述

總結

通過2個擴展點的配置,實現了運行過程中的修改配置文件內容的需求。
可以實現如請求http接口根據環境動態加載配置文件的需求。或者····自行腦補的需求···
附:github源碼
版權所有:tengqingya 轉載請註明出處

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章