Spring自定義標籤解析與實現

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

1. 自定義標籤

1.1 使用方式

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

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文件中爲其聲明對象的配置:spring

<?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各個屬性的定義。咱們首先看下自定義標籤各屬性的定義:app

<?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聲明:maven

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

       MyNameSpaceHandler只是註冊了apple的標籤的處理邏輯,真正的轉換邏輯在AppleBeanDefinitionParser中。這裏註冊的apple必須與Spring的applicationContext.xml文件中myapple:apple標籤後的apple保持一致,不然將找不到相應的處理邏輯。以下是AppleBeanDefinitionParser的處理邏輯:ide

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所表明的類的類型。以下是咱們的入口程序,用於檢查當前的自定義標籤是否正常工做的:ui

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());
  }
}

       運行結果以下:this

123, Asia

1.2 實現方式

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

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四種標籤,對於其他的標籤,如咱們所熟知的事務處理標籤,這些都是使用自定義標籤實現的。

相關文章
相關標籤/搜索