歡迎光臨
每天分享高質量文章

【死磕 Spring】—– IOC 之解析自定義標簽

精品專欄

 

在部落格 【死磕Spring】—– IOC 之 註冊 BeanDefinition 中提到:獲取 Document 物件後,會根據該物件和 Resource 資源物件呼叫 registerBeanDefinitions() 方法,開始註冊 BeanDefinitions 之旅。在註冊 BeanDefinitions 過程中會呼叫 parseBeanDefinitions() 開啟 BeanDefinition 的解析過程。在該方法中,它會根據名稱空間的不同呼叫不同的方法進行解析,如果是預設的名稱空間,則呼叫 parseDefaultElement() 進行預設標簽解析,否則呼叫 parseCustomElement() 方法進行自定義標簽解析。前面 6 篇部落格都是分析預設標簽的解析工作,這篇部落格分析自定義標簽的解析過

預設標簽的解析部落格如下:

  1. 【死磕 Spring】—– IOC 之解析Bean:解析 import 標簽

  2. 【死磕 Spring】—– IOC 之解析 bean 標簽:開啟解析行程

  3. 【死磕 Spring】—– IOC 之解析 bean 標簽:BeanDefinition

  4. 【死磕 Spring】—– IOC 之解析 bean 標簽:meta、lookup-method、replace-method

  5. 【死磕 Spring】—– IOC 之解析 bean 標簽:constructor-arg、property 子元素

  6. 【死磕 Spring】—– IOC 之解析 bean 標簽:解析自定義標簽

  7. 【死磕 Spring】—– IOC 之註冊解析的 BeanDefinition

在分析自定義標簽的解析之前,我們有必要瞭解自定義標簽的使用。

使用自定義標簽

擴充套件 Spring 自定義標簽配置一般需要以下幾個步驟:

  1. 建立一個需要擴充套件的元件

  2. 定義一個 XSD 檔案,用於描述元件內容

  3. 建立一個實現 AbstractSingleBeanDefinitionParser 介面的類,用來解析 XSD 檔案中的定義和元件定義

  4. 建立一個 Handler,繼承 NamespaceHandlerSupport ,用於將元件註冊到 Spring 容器

  5. 編寫 Spring.handlers 和 Spring.schemas 檔案

下麵就按照上面的步驟來實現一個自定義標簽元件。

建立元件

該元件就是一個普通的 JavaBean,沒有任何特別之處。

  1. public class User {

  2.    private String id;

  3.    private String userName;

  4.    private String email;

  5. }

定義 XSD 檔案

  1. "1.0" encoding="UTF-8"?>

  2. xmlns:xsd="http://www.w3.org/2001/XMLSchema"

  3.            xmlns="http://www.cmsblogs.com/schema/user" targetNamespace="http://www.cmsblogs.com/schema/user"

  4.            elementFormDefault="qualified">

  5.     name="user">

  6.        

  7.             name="id" type="xsd:string" />

  8.             name="userName" type="xsd:string" />

  9.             name="email" type="xsd:string" />

  10.        

  •    

  • 上面除了對 User 這個 JavaBean 進行了描述外,還定義了 xmlns="http://www.cmsblogs.com/schema/user"targetNamespace="http://www.cmsblogs.com/schema/user" 這兩個值,這兩個值在後面是有大作用的。

    Parser 類

    定義一個 Parser 類,該類繼承 AbstractSingleBeanDefinitionParser ,並實現 getBeanClass()doParse() 兩個方法。主要是用於解析 XSD 檔案中的定義和元件定義。

    1. public class UserDefinitionParser extends AbstractSingleBeanDefinitionParser {

    2.    @Override

    3.    protected Class> getBeanClass(Element element) {

    4.        return User.class;

    5.    }

    6.    @Override

    7.    protected void doParse(Element element, BeanDefinitionBuilder builder) {

    8.        String id = element.getAttribute("id");

    9.        String userName=element.getAttribute("userName");

    10.        String email=element.getAttribute("email");

    11.        if(StringUtils.hasText(id)){

    12.            builder.addPropertyValue("id",id);

    13.        }

    14.        if(StringUtils.hasText(userName)){

    15.            builder.addPropertyValue("userName", userName);

    16.        }

    17.        if(StringUtils.hasText(email)){

    18.            builder.addPropertyValue("email", email);

    19.        }

    20.    }

    21. }

    Handler 類

    定義 Handler 類,繼承 NamespaceHandlerSupport ,主要目的是將元件註冊到 Spring 容器中。

    1. public class UserNamespaceHandler extends NamespaceHandlerSupport {

    2.    @Override

    3.    public void init() {

    4.        registerBeanDefinitionParser("user",new UserDefinitionParser());

    5.    }

    6. }

    Spring.handlers

    1. http\://www.cmsblogs.com/schema/user=org.springframework.core.customelement.UserNamespaceHandler

    Spring.schemas

    1. http\://www.cmsblogs.com/schema/user.xsd=user.xsd

    經過上面幾個步驟,就可以使用自定義的標簽了。在 xml 配置檔案中使用如下:

    1. "1.0" encoding="UTF-8"?>

    2. xmlns="http://www.springframework.org/schema/beans"

    3.       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    4.       xmlns:myTag="http://www.cmsblogs.com/schema/user"

    5.       xsi:schemaLocation="http://www.springframework.org/schema/beans

    6.       http://www.springframework.org/schema/beans/spring-beans.xsd

    7.        http://www.cmsblogs.com/schema/user http://www.cmsblogs.com/schema/user.xsd">

    8.     id="user" email="12233445566@qq.com" userName="chenssy" />

    測試:

    1.    public static void main(String[] args){

    2.        ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");

    3.        User user = (User) context.getBean("user");

    4.        System.out.println(user.getUserName() + "----" + user.getEmail());

    5.    }

    6. }

    執行結果:

    解析自定義標簽

    上面已經演示了 Spring 自定義標簽的使用,下麵就來分析自定義標簽的解析過程。

    DefaultBeanDefinitionDocumentReader.parseBeanDefinitions() 負責標簽的解析工作,其中它根據名稱空間的不同進行不同標簽的解析,其中自定義標簽由 delegate.parseCustomElement() 實現。如下:

    1.    public BeanDefinition parseCustomElement(Element ele) {

    2.        return parseCustomElement(ele, null);

    3.    }

    呼叫 parseCustomElement() 方法,如下:

    1.    public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {

    2.        // 獲取 namespaceUri

    3.        String namespaceUri = getNamespaceURI(ele);

    4.        if (namespaceUri == null) {

    5.            return null;

    6.        }

    7.        // 根據 namespaceUri 獲取相應的 Handler

    8.        NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);

    9.        if (handler == null) {

    10.            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);

    11.            return null;

    12.        }

    13.        // 呼叫自定義的 Handler 處理

    14.        return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));

    15.    }

    處理過程分為三步:

    1. 獲取 namespaceUri

    2. 根據 namespaceUri 獲取相應的 Handler

    3. 呼叫自定義的 Handler 處理

    這個處理過程很簡單明瞭,根據 namespaceUri 獲取 Handler,這個對映關係我們在 Spring.handlers 中已經定義了,所以只需要找到該類,然後初始化傳回,最後呼叫該 Handler 物件的 parse() 方法處理,該方法我們也提供了實現。所以上面的核心就在於怎麼找到該 Handler 類。呼叫方法為: this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)

    getNamespaceHandlerResolver() 方法傳回的名稱空間的解析器,該解析定義在 XmlReaderContext 中,如下:

    1.    public final NamespaceHandlerResolver getNamespaceHandlerResolver() {

    2.        return this.namespaceHandlerResolver;

    3.    }

    這裡直接傳回,那是在什麼時候初始化的呢?這裡需要回退到博文:【死磕Spring】----- IOC 之 註冊 BeanDefinition ,在這篇部落格中提到在註冊 BeanDefinition 時,首先是透過 createBeanDefinitionDocumentReader() 獲取 Document 解析器 BeanDefinitionDocumentReader 實體,然後呼叫該實體 registerBeanDefinitions() 方法進行註冊。 registerBeanDefinitions() 方法需要提供兩個引數,一個是 Document 實體 doc,一個是 XmlReaderContext 實體 readerContext,readerContext 實體物件由 createReaderContext() 方法提供。namespaceHandlerResolver 實體物件就是在這個時候初始化的。如下:

    1.    public XmlReaderContext createReaderContext(Resource resource) {

    2.        return new XmlReaderContext(resource, this.problemReporter, this.eventListener,

    3.                this.sourceExtractor, this, getNamespaceHandlerResolver());

    4.    }

    XmlReaderContext 建構式中最後一個引數就是 NamespaceHandlerResolver 物件,該物件由 getNamespaceHandlerResolver() 提供,如下:

    1.    public NamespaceHandlerResolver getNamespaceHandlerResolver() {

    2.        if (this.namespaceHandlerResolver == null) {

    3.            this.namespaceHandlerResolver = createDefaultNamespaceHandlerResolver();

    4.        }

    5.        return this.namespaceHandlerResolver;

    6.    }

    7.    protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {

    8.        ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());

    9.        return new DefaultNamespaceHandlerResolver(cl);

    10.    }

    所以 getNamespaceHandlerResolver().resolve(namespaceUri) 呼叫的就是 DefaultNamespaceHandlerResolver 的 resolve()。如下:

    1.   public NamespaceHandler resolve(String namespaceUri) {

    2.        // 獲取所有已經配置的 Handler 對映

    3.        Map<String, Object> handlerMappings = getHandlerMappings();

    4.        // 根據 namespaceUri 獲取 handler的資訊:這裡一般都是類路徑

    5.        Object handlerOrClassName = handlerMappings.get(namespaceUri);

    6.        if (handlerOrClassName == null) {

    7.            return null;

    8.        }

    9.        else if (handlerOrClassName instanceof NamespaceHandler) {

    10.            // 如果已經做過解析,直接傳回

    11.            return (NamespaceHandler) handlerOrClassName;

    12.        }

    13.        else {

    14.            String className = (String) handlerOrClassName;

    15.            try {

    16.                Class> handlerClass = ClassUtils.forName(className, this.classLoader);

    17.                if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {

    18.                    throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +

    19.                            "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");

    20.                }

    21.                // 初始化類

    22.                NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);

    23.                // 呼叫 init() 方法

    24.                namespaceHandler.init();

    25.                // 記錄在快取

    26.                handlerMappings.put(namespaceUri, namespaceHandler);

    27.                return namespaceHandler;

    28.            }

    29.            catch (ClassNotFoundException ex) {

    30.                throw new FatalBeanException("Could not find NamespaceHandler class [" + className +

    31.                        "] for namespace [" + namespaceUri + "]", ex);

    32.            }

    33.            catch (LinkageError err) {

    34.                throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" +

    35.                        className + "] for namespace [" + namespaceUri + "]", err);

    36.            }

    37.        }

    38.    }

    首先呼叫 getHandlerMappings() 獲取所有配置檔案中的對映關係 handlerMappings ,該關係為 ,然後根據名稱空間 namespaceUri 從對映關係中獲取相應的資訊,如果為空或者已經初始化了就直接傳回,否則根據反射對其進行初始化,同時呼叫其 init() 方法,最後將該 Handler 物件快取。

    init() 方法主要是將自定義標簽解析器進行註冊,如我們自定義的 init()

    1.    @Override

    2.    public void init() {

    3.        registerBeanDefinitionParser("user",new UserDefinitionParser());

    4.    }

    直接呼叫父類的 registerBeanDefinitionParser() 方法進行註冊:

    1.    protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) {

    2.        this.parsers.put(elementName, parser);

    3.    }

    其實就是將對映關係放在一個 Map 結構的 parsers 物件中: privatefinalMapparsers

    完成後傳回 NamespaceHandler 物件,然後呼叫其 parse() 方法開始自定義標簽的解析,如下:

    1.    public BeanDefinition parse(Element element, ParserContext parserContext) {

    2.        BeanDefinitionParser parser = findParserForElement(element, parserContext);

    3.        return (parser != null ? parser.parse(element, parserContext) : null);

    4.    }

    呼叫 findParserForElement() 方法獲取 BeanDefinitionParser 實體,其實就是獲取在 init() 方法裡面註冊的實體物件。如下:

    1.    private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {

    2.        String localName = parserContext.getDelegate().getLocalName(element);

    3.        BeanDefinitionParser parser = this.parsers.get(localName);

    4.        if (parser == null) {

    5.            parserContext.getReaderContext().fatal(

    6.                    "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);

    7.        }

    8.        return parser;

    9.    }

    獲取 localName,在上面的例子中就是 : user,然後從 Map 實體 parsers 中獲取 BeanDefinitionParser 物件。傳回 BeanDefinitionParser 物件後,呼叫其 parse(),該方法在 AbstractBeanDefinitionParser 中實現:

    1.    public final BeanDefinition parse(Element element, ParserContext parserContext) {

    2.        AbstractBeanDefinition definition = parseInternal(element, parserContext);

    3.        if (definition != null && !parserContext.isNested()) {

    4.            try {

    5.                String id = resolveId(element, definition, parserContext);

    6.                if (!StringUtils.hasText(id)) {

    7.                    parserContext.getReaderContext().error(

    8.                            "Id is required for element '" + parserContext.getDelegate().getLocalName(element)

    9.                                    + "' when used as a top-level tag", element);

    10.                }

    11.                String[] aliases = null;

    12.                if (shouldParseNameAsAliases()) {

    13.                    String name = element.getAttribute(NAME_ATTRIBUTE);

    14.                    if (StringUtils.hasLength(name)) {

    15.                        aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));

    16.                    }

    17.                }

    18.                BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);

    19.                registerBeanDefinition(holder, parserContext.getRegistry());

    20.                if (shouldFireEvents()) {

    21.                    BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);

    22.                    postProcessComponentDefinition(componentDefinition);

    23.                    parserContext.registerComponent(componentDefinition);

    24.                }

    25.            }

    26.            catch (BeanDefinitionStoreException ex) {

    27.                String msg = ex.getMessage();

    28.                parserContext.getReaderContext().error((msg != null ? msg : ex.toString()), element);

    29.                return null;

    30.            }

    31.        }

    32.        return definition;

    33.    }

    核心在方法 parseInternal() 為什麼這麼說,以為該方法傳回的是 AbstractBeanDefinition 物件,從前面預設標簽的解析工作中我們就可以判斷該方法就是將標簽解析為 AbstractBeanDefinition ,且後續程式碼都是將 AbstractBeanDefinition 轉換為 BeanDefinitionHolder,所以真正的解析工作都交由 parseInternal() 實現,如下:

    1.    protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {

    2.        // 獲取

    3.        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();

    4.        // 獲取父類元素

    5.        String parentName = getParentName(element);

    6.        if (parentName != null) {

    7.            builder.getRawBeanDefinition().setParentName(parentName);

    8.        }

    9.        // 獲取自定義標簽中的 class,這個時候會去呼叫自定義解析中的 getBeanClass()

    10.        Class> beanClass = getBeanClass(element);

    11.        if (beanClass != null) {

    12.            builder.getRawBeanDefinition().setBeanClass(beanClass);

    13.        }

    14.        else {

    15.            // beanClass 為 null,意味著子類並沒有重寫 getBeanClass() 方法,則嘗試去判斷是否重寫了 getBeanClassName()

    16.            String beanClassName = getBeanClassName(element);

    17.            if (beanClassName != null) {

    18.                builder.getRawBeanDefinition().setBeanClassName(beanClassName);

    19.            }

    20.        }

    21.        builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));

    22.        BeanDefinition containingBd = parserContext.getContainingBeanDefinition();

    23.        if (containingBd != null) {

    24.            // Inner bean definition must receive same scope as containing bean.

    25.            builder.setScope(containingBd.getScope());

    26.        }

    27.        if (parserContext.isDefaultLazyInit()) {

    28.            // Default-lazy-init applies to custom bean definitions as well.

    29.            builder.setLazyInit(true);

    30.        }

    31.        // 呼叫子類的 doParse() 進行解析

    32.        doParse(element, parserContext, builder);

    33.        return builder.getBeanDefinition();

    34.    }

    在該方法中我們主要關註兩個方法: getBeanClass()doParse()。對於 getBeanClass() 方法,AbstractSingleBeanDefinitionParser 類並沒有提供具體實現,而是直接傳回 null,意味著它希望子類能夠重寫該方法,當然如果沒有重寫該方法,這會去呼叫 getBeanClassName() ,判斷子類是否已經重寫了該方法。對於 doParse() 則是直接空實現。所以對於 parseInternal() 而言它總是期待它的子類能夠實現 getBeanClass()doParse(),其中 doParse() 尤為重要,如果你不提供實現,怎麼來解析自定義標簽呢?最後將自定義的解析器:UserDefinitionParser 再次回觀。

    1. public class UserDefinitionParser extends AbstractSingleBeanDefinitionParser {

    2.    @Override

    3.    protected Class> getBeanClass(Element element) {

    4.        return User.class;

    5.    }

    6.    @Override

    7.    protected void doParse(Element element, BeanDefinitionBuilder builder) {

    8.        String id = element.getAttribute("id");

    9.        String userName=element.getAttribute("userName");

    10.        String email=element.getAttribute("email");

    11.        if(StringUtils.hasText(id)){

    12.            builder.addPropertyValue("id",id);

    13.        }

    14.        if(StringUtils.hasText(userName)){

    15.            builder.addPropertyValue("userName", userName);

    16.        }

    17.        if(StringUtils.hasText(email)){

    18.            builder.addPropertyValue("email", email);

    19.        }

    20.    }

    21. }

    至此,自定義標簽的解析過程已經分析完成了。其實整個過程還是較為簡單:首先會載入 handlers 檔案,將其中內容進行一個解析,形成這樣的一個對映,然後根據獲取的 namespaceUri 就可以得到相應的類路徑,對其進行初始化等到相應的 Handler 物件,呼叫 parse() 方法,在該方法中根據標簽的 localName 得到相應的 BeanDefinitionParser 實體物件,呼叫 parse() ,該方法定義在 AbstractBeanDefinitionParser 抽象類中,核心邏輯封裝在其 parseInternal() 中,該方法傳回一個 AbstractBeanDefinition 實體物件,其主要是在 AbstractSingleBeanDefinitionParser 中實現,對於自定義的 Parser 類,其需要實現 getBeanClass() 或者 getBeanClassName()doParse()

    【死磕 Spring】----- IOC 之 IOC 初始化總結

    【死磕 Spring】----- IOC 之 獲取 Document 物件

    【死磕 Spring】----- IOC 之 註冊 BeanDefinition

    【死磕 Spring】----- IOC 之 獲取驗證模型

    【死磕 Spring】----- IOC 之 Spring 統一資源載入策略

    【死磕 Spring】----- IOC 之深入理解 Spring IoC

    END


    >>>>>> 加群交流技術 <<<<<<

    贊(0)

    分享創造快樂

    © 2024 知識星球   網站地圖