Spring自定義標簽解析與實現


       在Spring Bean注冊解析(一)Spring Bean注冊解析(二)中我們講到,Spring在解析xml文件中的標簽的時候會區分當前的標簽是四種基本標簽(import、alias、bean和beans)還是自定義標簽,如果是自定義標簽,則會按照自定義標簽的邏輯解析當前的標簽。另外,即使是bean標簽,其也可以使用自定義的屬性或者使用自定義的子標簽。本文將對自定義標簽和自定義屬性的使用方式進行講解,並且會從源碼的角度對自定義標簽和自定義屬性的實現方式進行講解。

1. 自定義標簽

1.1 使用方式

       對於自定義標簽,其主要包含兩個部分:命名空間和轉換邏輯的定義,而對於自定義標簽的使用,我們只需要按照自定義的命名空間規則,在Spring的xml文件中定義相關的bean即可。假設我們有一個類Apple,並且我們需要在xml文件使用自定義標簽聲明該Apple對象,如下是Apple的定義:

public class Apple {
  private int price;
  private String origin;

  public int getPrice() {
    return price;
  }

  public void setPrice(int price) {
    this.price = price;
  }

  public String getOrigin() {
    return origin;
  }

  public void setOrigin(String origin) {
    this.origin = origin;
  }
}

       如下是我們使用自定義標簽在Spring的xml文件中為其聲明對象的配置:

<?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:myapple="http://www.lexueba.com/schema/apple"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.lexueba.com/schema/apple http://www.lexueba.com/schema/apple.xsd">

    <myapple:apple id="apple" price="123" origin="Asia"/>
</beans>

       我們這里使用了myapple:apple標簽聲明名為apple的bean,這里myapple就對應了上面的xmlns:myapple,其后指定了一個鏈接:http://www.lexueba.com/schema/apple,Spring在解析到該鏈接的時候,會到META-INF文件夾下找Spring.handlers和Spring.schemas文件(這里META-INF文件夾放在maven工程的resources目錄下即可),然后讀取這兩個文件的內容,如下是其定義:

Spring.handlers
http\://www.lexueba.com/schema/apple=chapter4.eg3.MyNameSpaceHandler
Spring.schemas
http\://www.lexueba.com/schema/apple.xsd=META-INF/custom-apple.xsd

       可以看到,Spring.handlers指定了當前命名空間的處理邏輯類,而Spring.schemas則指定了一個xsd文件,該文件中則聲明了myapple:apple各個屬性的定義。我們首先看下自定義標簽各屬性的定義:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.lexueba.com/schema/apple"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://www.lexueba.com/schema/apple"
            elementFormDefault="qualified">

    <xsd:complexType name="apple">
        <xsd:attribute name="id" type="xsd:string">
            <xsd:annotation>
                <xsd:documentation>
                    <![CDATA[ The unique identifier for a bean. ]]>
                </xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
        <xsd:attribute name="price" type="xsd:int">
            <xsd:annotation>
                <xsd:documentation>
                    <![CDATA[ The price for a bean. ]]>
                </xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
        <xsd:attribute name="origin" type="xsd:string">
            <xsd:annotation>
                <xsd:documentation>
                    <![CDATA[ The origin of the bean. ]]>
                </xsd:documentation>
            </xsd:annotation>
        </xsd:attribute>
    </xsd:complexType>

    <xsd:element name="apple" type="apple">
        <xsd:annotation>
            <xsd:documentation><![CDATA[ The service config ]]></xsd:documentation>
        </xsd:annotation>
    </xsd:element>

</xsd:schema>

       可以看到,該xsd文件中聲明了三個屬性:id、price和origin。需要注意的是,這三個屬性與我們的Apple對象的屬性price和origin沒有直接的關系,這里只是一個xsd文件的聲明,以表征Spring的applicationContext.xml文件中使用當前命名空間時可以使用的標簽屬性。接下來我們看一下Spring.handlers中定義的MyNameSpaceHandler聲明:

public class MyNameSpaceHandler extends NamespaceHandlerSupport {
  @Override
  public void init() {
    registerBeanDefinitionParser("apple", new AppleBeanDefinitionParser());
  }
}

       MyNameSpaceHandler只是注冊了apple的標簽的處理邏輯,真正的轉換邏輯在AppleBeanDefinitionParser中。這里注冊的apple必須與Spring的applicationContext.xml文件中myapple:apple標簽后的apple保持一致,否則將找不到相應的處理邏輯。如下是AppleBeanDefinitionParser的處理邏輯:

public class AppleBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
  @Override
  protected Class<?> getBeanClass(Element element) {
    return Apple.class;
  }

  @Override
  protected void doParse(Element element, BeanDefinitionBuilder builder) {
    String price = element.getAttribute("price");
    String origin = element.getAttribute("origin");
    if (StringUtils.hasText(price)) {
      builder.addPropertyValue("price", Integer.parseInt(price));
    }

    if (StringUtils.hasText(origin)) {
      builder.addPropertyValue("origin", origin);
    }
  }
}

       可以看到,該處理邏輯中主要是獲取當前標簽中定義的price和origin屬性的值,然后將其按照一定的處理邏輯注冊到當前的BeanDefinition中。這里還實現了一個getBeanClass()方法,該方法用於表明當前自定義標簽對應的BeanDefinition所代表的類的類型。如下是我們的入口程序,用於檢查當前的自定義標簽是否正常工作的:

public class CustomSchemaApp {
  public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
    Apple apple = applicationContext.getBean(Apple.class);
    System.out.println(apple.getPrice() + ", " + apple.getOrigin());
  }
}

       運行結果如下:

123, Asia

1.2 實現方式

       我們還是從對整個applicationContext.xml文件開始讀取的入口方法開始進行講解,即DefaultBeanDefinitionDocumentReader.parseBeanDefinitions()方法,如下是該方法的源碼:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    // 判斷根節點使用的標簽所對應的命名空間是否為Spring提供的默認命名空間,
    // 這里根節點為beans節點,該節點的命名空間通過其xmlns屬性進行了定義
    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)) {
                    // 當前標簽使用的是默認的命名空間,如bean標簽,
                    // 則按照默認命名空間的邏輯對其進行處理
                    parseDefaultElement(ele, delegate);
                } else {
                    // 判斷當前標簽使用的命名空間是自定義的命名空間,如這里myapple:apple所
                    // 使用的就是自定義的命名空間,那么就按照定義命名空間邏輯進行處理
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        // 如果根節點使用的命名空間不是默認的命名空間,則按照自定義的命名空間進行處理
        delegate.parseCustomElement(root);
    }
}

       可以看到,該方法首先會判斷當前文件指定的xmlns命名空間是否為默認命名空間,如果是,則按照默認命名空間進行處理,如果不是則直接按照自定義命名空間進行處理。這里需要注意的是,即使在默認的命名空間中,當前標簽也可以使用自定義的命名空間,我們定義的myapple:apple就是這種類型,這里myapple就關聯了xmlns:myapple后的myapple。如下是自定義命名空間的處理邏輯:

@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
    // 獲取當前標簽對應的命名空間指定的url
    String namespaceUri = getNamespaceURI(ele);
    if (namespaceUri == null) {
        return null;
    }
    
    // 獲取當前url所對應的NameSpaceHandler處理邏輯,也即我們定義的MyNameSpaceHandler
    NamespaceHandler handler = this.readerContext
        .getNamespaceHandlerResolver()
        .resolve(namespaceUri);
    if (handler == null) {
        error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + 
              namespaceUri + "]", ele);
        return null;
    }
    
    // 調用當前命名空間處理邏輯的parse()方法,以對當前標簽進行轉換
    return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}

       這里getNamespaceURI()方法的作用是獲取當前標簽對應的命名空間url。在獲取url之后,會調用NamespaceHandlerResolver.resolve(String)方法,該方法會通過當前命名空間的url獲取META-INF/Spring.handlers文件內容,並且查找當前命名空間url對應的處理邏輯類。如下是該方法的聲明:

@Nullable
public NamespaceHandler resolve(String namespaceUri) {
    // 獲取handlerMapping對象,其鍵為當前的命名空間url,
    // 值為當前命名空間的處理邏輯類對象,或者為處理邏輯類的包含全路徑的類名
    Map<String, Object> handlerMappings = getHandlerMappings();
    // 查看是否存在當前url的處理類邏輯,沒有則返回null
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    if (handlerOrClassName == null) {
        return null;
    } else if (handlerOrClassName instanceof NamespaceHandler) {
        // 如果存在當前url對應的處理類對象,則直接返回該處理對象
        return (NamespaceHandler) handlerOrClassName;
    } else {
        // 如果當前url對應的處理邏輯還是一個沒初始化的全路徑類名,則通過反射對其進行初始化
        String className = (String) handlerOrClassName;
        try {
            Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
            // 判斷該全路徑類是否為NamespaceHandler接口的實現類
            if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                throw new FatalBeanException("Class [" + className + "] for namespace [" + 
                    namespaceUri + "] does not implement the [" + 
                    NamespaceHandler.class.getName() + "] interface");
            }
            NamespaceHandler namespaceHandler = 
                (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
            namespaceHandler.init();  // 調用處理邏輯的初始化方法
            handlerMappings.put(namespaceUri, namespaceHandler);  //緩存處理邏輯類對象
            return namespaceHandler;
        }
        catch (ClassNotFoundException ex) {
            throw new FatalBeanException("Could not find NamespaceHandler class [" 
               + className + "] for namespace [" + namespaceUri + "]", ex);
        }
        catch (LinkageError err) {
            throw new FatalBeanException("Unresolvable class definition for" 
               + "NamespaceHandler class [" + className + "] for namespace [" 
               +  namespaceUri + "]", err);
        }
    }
}

       可以看到,在處理命名空間url的時候,首先會判斷是否存在當前url的處理邏輯,不存在則直接返回。如果存在,則會判斷其為一個NamespaceHandler對象,還是一個全路徑的類名,是NamespaceHandler對象則強制類型轉換后返回,否則通過反射初始化該類,並調用其初始化方法,然后才返回。

       我們繼續查看NamespaceHandler.parse()方法,如下是該方法的源碼:

@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
    // 獲取當前標簽使用的parser處理類
    BeanDefinitionParser parser = findParserForElement(element, parserContext);
    // 按照定義的parser處理類對當前標簽進行處理,這里的處理類即我們定義的AppleBeanDefinitionParser
    return (parser != null ? parser.parse(element, parserContext) : null);
}

       這里的parse()方法首先會查找當前標簽定義的處理邏輯對象,找到后則調用其parse()方法對其進行處理。這里的parser也即我們定義的AppleBeanDefinitionParser.parse()方法。這里需要注意的是,我們在前面講過,在MyNameSpaceHandler.init()方法中注冊的處理類邏輯的鍵(即apple)必須與xml文件中myapple:apple后的apple一致,這就是這里findParserForElement()方法查找BeanDefinitionParser處理邏輯的依據。如下是findParserForElement()方法的源碼:

@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
    // 獲取當前標簽命名空間后的局部鍵名,即apple
    String localName = parserContext.getDelegate().getLocalName(element);
    // 通過使用的命名空間鍵獲取對應的BeanDefinitionParser處理邏輯
    BeanDefinitionParser parser = this.parsers.get(localName);
    if (parser == null) {
        parserContext.getReaderContext().fatal(
           "Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
    }
    return parser;
}

       這里首先獲取當前標簽的命名空間后的鍵名,即myapple:apple后的apple,然后在parsers中獲取該鍵對應的BeanDefinitionParser對象。其實在MyNameSpaceHandler.init()方法中進行的注冊工作就是將其注冊到了parsers對象中。

2. 自定義屬性

2.1 使用方式

       自定義屬性的定義方式和自定義標簽非常相似,其主要也是進行命名空間和轉換邏輯的定義。假設我們有一個Car對象,我們需要使用自定義標簽為其添加一個描述屬性。如下是Car對象的定義:

public class Car {
  private long id;
  private String name;
  private String desc;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getDesc() {
    return desc;
  }

  public void setDesc(String desc) {
    this.desc = desc;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}

       如下是在applicationContext.xml中該對象的定義:

<?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:car="http://www.lexueba.com/schema/car-desc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
    
    <bean id="car" class="chapter4.eg2.Car" car:car-desc="This is test custom attribute">
        <property name="id" value="1"/>
        <property name="name" value="baoma"/>
    </bean>
</beans>

       可以看到,car對象的定義使用的就是一般的bean定義,只不過其多了一個屬性car:car-desc的使用。這里的car:car-desc對應的命名空間就是上面的http://www.lexueba.com/schema/car-desc。同自定義標簽一樣,自定義屬性也需要在META-INF下的Spring.handlers和Spring.schemas文件中指定當前的處理邏輯和xsd定義,如下是這兩個文件的定義:

Spring.handlers
http\://www.lexueba.com/schema/car-desc=chapter4.eg2.MyCustomAttributeHandler
Spring.schemas
http\://www.lexueba.com/schema/car.xsd=META-INF/custom-attribute.xsd

       對應的xsd文件的定義如下:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://www.lexueba.com/schema/car-desc"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://www.lexueba.com/schema/car-desc"
            elementFormDefault="qualified">

    <xsd:attribute name="car-desc" type="xsd:string"/>

</xsd:schema>

       可以看到,該xsd文件中只定義了一個屬性,即car-desc。如下是MyCustomAttributeHandler的聲明:

public class MyCustomAttributeHandler extends NamespaceHandlerSupport {
  @Override
  public void init() {
    registerBeanDefinitionDecoratorForAttribute("car-desc", 
      new CarDescInitializingBeanDefinitionDecorator());
  }
}

       需要注意的是,和自定義標簽不同的是,自定義標簽是將處理邏輯注冊到parsers對象中,這里自定義屬性是將處理邏輯注冊到attributeDecorators中。如下CarDescInitializingBeanDefinitionDecorator的邏輯:

public class CarDescInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator {
  @Override
  public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
    String desc = ((Attr) node).getValue();
    definition.getBeanDefinition().getPropertyValues().addPropertyValue("desc", desc);
    return definition;
  }
}

       可以看到,對於car-desc的處理邏輯就是獲取當前定義的屬性的值,由於知道其是當前標簽的一個屬性,因而可以將其強轉為一個Attr類型的對象,並獲取其值,然后將其添加到指定的BeandDefinitionHolder中。這里需要注意的是,自定義標簽繼承的是AbstractSingleBeanDefinitionParser類,實際上是實現的BeanDefinitionParser接口,而自定義屬性實現的則是BeanDefinitionDecorator接口。

2.2 實現方式

       關於自定義屬性的實現方式,需要注意的是,自定義屬性只能在bean標簽中使用,因而我們可以直接進入對bean標簽的處理邏輯中,即DefaultBeanDefinitionDocumentReader.processBeanDefinition()方法,如下是該方法的聲明:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
    // 對bean標簽的默認屬性和子標簽進行處理,將其封裝為一個BeanDefinition對象,
    // 並放入BeanDefinitionHolder中
    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
    if (bdHolder != null) {
        // 進行自定義屬性或自定義子標簽的裝飾
        bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
        try {
            // 注冊當前的BeanDefinition
            BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder,
               getReaderContext().getRegistry());
        }catch (BeanDefinitionStoreException ex) {
            getReaderContext().error("Failed to register bean definition with name '" +
                                     bdHolder.getBeanName() + "'", ele, ex);
        }
        
        // 調用注冊了bean標簽解析完成的事件處理邏輯
        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
    }
}

       這里我們直接進入BeanDefinitionParserDelegate.decorateBeanDefinitionIfRequired()方法中:

public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
    Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) {

    BeanDefinitionHolder finalDefinition = definitionHolder;

    // 處理自定義屬性
    NamedNodeMap attributes = ele.getAttributes();
    for (int i = 0; i < attributes.getLength(); i++) {
        Node node = attributes.item(i);
        finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
    }

    // 處理自定義子標簽
    NodeList children = ele.getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        Node node = children.item(i);
        if (node.getNodeType() == Node.ELEMENT_NODE) {
            finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
        }
    }
    return finalDefinition;
}

       可以看到,自定義屬性和自定義子標簽的解析都是通過decorateIfRequired()方法進行的,如下是該方法的定義:

public BeanDefinitionHolder decorateIfRequired(
    Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) {

    // 獲取當前自定義屬性或子標簽的命名空間url
    String namespaceUri = getNamespaceURI(node);
    // 判斷其如果為spring默認的命名空間則不對其進行處理
    if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) {
        // 獲取當前命名空間對應的NamespaceHandler對象
        NamespaceHandler handler = this.readerContext
            .getNamespaceHandlerResolver()
            .resolve(namespaceUri);
        if (handler != null) {
            // 對當前的BeanDefinitionHolder進行裝飾
            BeanDefinitionHolder decorated =
                handler.decorate(node, originalDef, 
                   new ParserContext(this.readerContext, this, containingBd));
            if (decorated != null) {
                return decorated;
            }
        }
        else if (namespaceUri.startsWith("http://www.springframework.org/")) {
            error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + 
                  namespaceUri + "]", node);
        }
        else {
            // A custom namespace, not to be handled by Spring - maybe "xml:...".
            if (logger.isDebugEnabled()) {
                logger.debug("No Spring NamespaceHandler found for XML schema namespace [" 
                             + namespaceUri + "]");
            }
        }
    }
    return originalDef;
}

       decorateIfRequired()方法首先會獲取當前自定義屬性或子標簽對應的命名空間url,然后根據該url獲取當前命名空間對應的NamespaceHandler處理邏輯,並且調用其decorate()方法進行裝飾,如下是該方法的實現:

@Nullable
public BeanDefinitionHolder decorate(
    Node node, BeanDefinitionHolder definition, ParserContext parserContext) {
    // 獲取當前自定義屬性或子標簽注冊的BeanDefinitionDecorator對象
    BeanDefinitionDecorator decorator = findDecoratorForNode(node, parserContext);
    // 調用自定義的BeanDefinitionDecorator.decorate()方法進行裝飾,
    // 這里就是我們實現的CarDescInitializingBeanDefinitionDecorator類
    return (decorator != null ? decorator.decorate(node, definition, parserContext) : null);
}

       和自定義標簽不同的是,自定義屬性或自定義子標簽查找當前Decorator的方法是需要對屬性或子標簽進行分別判斷的,如下是findDecoratorForNode()的實現:

@Nullable
private BeanDefinitionDecorator findDecoratorForNode(Node node,
        ParserContext parserContext) {
    BeanDefinitionDecorator decorator = null;
    // 獲取當前標簽或屬性的局部鍵名
    String localName = parserContext.getDelegate().getLocalName(node);
    // 判斷當前節點是屬性還是子標簽,根據情況不同獲取不同的Decorator處理邏輯
    if (node instanceof Element) {
        decorator = this.decorators.get(localName);
    } else if (node instanceof Attr) {
        decorator = this.attributeDecorators.get(localName);
    } else {
        parserContext.getReaderContext().fatal(
            "Cannot decorate based on Nodes of type [" + node.getClass().getName() 
            + "]", node);
    }
    if (decorator == null) {
        parserContext.getReaderContext().fatal(
            "Cannot locate BeanDefinitionDecorator for " + (node instanceof Element 
            ? "element" : "attribute") + " [" + localName + "]", node);
    }
    return decorator;
}

       對於BeanDefinitionDecorator處理邏輯的查找,可以看到,其會根據節點的類型進行判斷,根據不同的情況獲取不同的BeanDefinitionDecorator處理對象。

3. 自定義子標簽

       對於自定義子標簽的使用,其與自定義標簽的使用非常相似,不過需要注意的是,根據對自定義屬性的源碼解析,我們知道自定義子標簽並不是自定義標簽,自定義子標簽只是起到對其父標簽所定義的bean的一種裝飾作用,因而自定義子標簽的處理邏輯定義與自定義標簽主要有兩點不同:①在NamespaceHandler.init()方法中注冊自定義子標簽的處理邏輯時需要使用registerBeanDefinitionDecorator(String, BeanDefinitionDecorator)方法;②自定義子標簽的處理邏輯需要實現的是BeanDefinitionDecorator接口。其余部分的使用都和自定義標簽一致。

4. 總結

       本文主要對自定義標簽,自定義屬性和自定義子標簽的使用方式和源碼實現進行了講解,有了對自定義標簽的理解,我們可以在Spring的xml文件中根據自己的需要實現自己的處理邏輯。另外需要說明的是,Spring源碼中也大量使用了自定義標簽,比如spring的AOP的定義,其標簽為<aspectj-autoproxy />。從另一個角度來看,我們前面兩篇文章對Spring的xml文件的解析進行了講解,可以知道,Spring默認只會處理import、alias、bean和beans四種標簽,對於其余的標簽,如我們所熟知的事務處理標簽,這些都是使用自定義標簽實現的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM