Spring Core Container 源碼分析七:註冊 Bean Definitions

前言

本來覺得,Spring 經過解析 bean 的配置,生成並註冊 bean defintions 的過程不太複雜,比較簡單,不用單獨開闢一篇博文來說述;可是當在分析前面兩個章節有關 @Autowired、@Component、@Service 註解的注入機制的時候,發現,若是沒有對有關 bean defintions 的解析和註冊機制完全弄明白,則很難弄清楚 annotation 在 Spring 容器中的底層運行機制;因此,本篇博文做者將試圖去弄清楚 Spring 容器內部是如何去解析 bean 配置並生成和註冊 bean definitions 的相關主流程;java

備註,本文是做者的原創做品,轉載請註明出處。node

bean definition 是什麼?

➥ bean definitions 是什麼?spring

其實很簡單,就是 Java 中的 POJO,用來描述 bean 配置中的 element 元素的,好比,咱們有以下的一個簡單的配置編程

beans.xml緩存

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
   
   <context:component-scan base-package="org.shangyang" />  
   
   <bean name="jane" class="org.shangyang.spring.container.Person">
       <property name="name" value="Jane Doe"/>
   </bean>
   
</beans>

能夠看到,上面有三個 elementapp

  1. _<beans/>_, root element
  2. _<context:component/>_, component-scan element
  3. _<bean/>_, bean element

在配置文件 beans.xml 被 Spring 解析的過程當中,每個 element 將會被解析爲一個 bean definition 對象緩存在 Spring 容器中;ide

➥ 須要被描述爲 bean definitions 的配置對象主要分爲以下幾大類,源碼分析

  1. xml-based,解析 xml beans 的狀況;
  2. 使用 @Autowired、@Required 註解注入 beans 的解析狀況;
    須要特殊處理並解析的元素 <context:annotation-config/>
  3. 使用 @Component、@Service、@Repository,@Beans 註解注入 beans 的解析狀況;
    須要特殊處理並解析的元素 <context:annotation-scan/>

➥ 最開始個人確是這麼認識 bean definitions 的,可是當我分析完有關 bean definitions 的相關邏輯和源碼之後,對其認識有了昇華,參考寫在最後post

源碼分析

最好的分析源碼的方式,就是經過高屋建瓴,逐個擊破的方式;首先經過流程圖得到它的藍圖(頂層設計圖),而後再根據藍圖上的點逐個擊破;最後才能達到融會貫通,成竹在胸的境界;因此,這裏做者用這樣的方式帶你深刻剖析 Spring 容器裏面的核心點,以及相關主流程究竟是如何運做的。測試

測試用例

爲了一次性把上述源碼分析所描述有的狀況闡述清楚,咱們繼續使用 Spring Core Container 源碼分析六:@Service 中使用的測試用例;惟一作的修改是,再使用一個特殊的 element xmlns:p 來配置 john,這樣能夠進一步去調試自定義 Spring 配置標籤是如何實現的;

beans.xml

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
   
   <context:component-scan base-package="org.shangyang" />  
   
    <bean name="john"
          class="org.shangyang.spring.container.Person"
          p:name="John Doe"
          p:spouse-ref="jane"/>
    
    <bean name="jane" class="org.shangyang.spring.container.Person">
        <property name="name" value="Jane Doe"/>
    </bean>
   
   <bean name="niba" class="org.shangyang.spring.container.Dog">
      <property name="name" value="Niba" />
   </bean>
       
</beans>

流程分析

整個流程是從解析 bean definitions 流程開始的,對應的入口是主流程的 _step 1.1.1.2 obtainFreshBeanFactory_;

入口流程

  1. 首選初始化獲得 BeanFactory 實例 DefaultListableBeanFactory,用來註冊解析配置後生成的 bean definitions;
  2. 而後經過 XmlBeanDefinitionReader 解析 Spring XML 配置文件
    根據用戶指定的 XML 文件路徑 location,進行解析而且獲得 Resource[] 對象,具體參考 step 1.1.3.3.1.1 getResource(location) 步驟;這裏,對其如何經過 location 獲得 Resource[] 對象作進一步分析,看源碼,

    PathMatchingResourcePatternResolver.java

    public Resource[] getResources(String locationPattern) throws IOException {
       Assert.notNull(locationPattern, "Location pattern must not be null");
       if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
          // a class path resource (multiple resources for same name possible)
          if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
             // a class path resource pattern
             return findPathMatchingResources(locationPattern);
          }
          else {
             // all class path resources with the given name
             return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
          }
       }
       else {
          // Only look for a pattern after a prefix here
          // (to not get fooled by a pattern symbol in a strange prefix).
          int prefixEnd = locationPattern.indexOf(":") + 1;
          if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
             // a file pattern
             return findPathMatchingResources(locationPattern);
          }
          else {
             // a single resource with the given name
             return new Resource[] {getResourceLoader().getResource(locationPattern)};
          }
       }
    }

    這裏的解析過程主要分爲兩種狀況進行解析,一種是前綴是 classpath: 的狀況,一種是普通的狀況,正如咱們當前所使用的測試用例的狀況,既是 new ClassPathXmlApplicationContext("beans.xml") 的狀況,這裏不打算在這裏繼續深挖;

  3. 以測試用例 beans.xml 爲例,從 #2 解析獲得的 Resource 實例 resource 對應的是 beans.xml 的配置信息,從 step 1.1.3.3.1.2 loadBeanDefinitions 開始將會對 resource 既是 beans.xml 中的元素依次進行解析;首先生成對應 beans.xml 的 org.w3c.Document 對象實例 document_,見 _step 1.1.3.3.1.2.2.1_,其次獲得解析 _document 對象的 BeanDefinitionDocumentReader 實例 documentReader_,將當前的 Resource 對象封裝爲 XmlReaderContext 實例 _xmlReaderContext_,最後經過 _documentReader 開始正式解析 document 對象獲得 bean definitions 並將其註冊到當前的 beanFactory 實例中,該步驟見 step 1.1.3.3.1.2.2.2.3

當完成上述三個步驟之後,將進入 register bean definitions process 流程

register bean definitions process

➥ 首先,重要的兩件事情是,

  • document 對象中得到了 Root 實例 root_,見 _step 1.2
    看一個 root 元素,長什麼樣的

    <beans xmlns="http://www.springframework.org/schema/beans"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns:p="http://www.springframework.org/schema/p"
         xmlns:context="http://www.springframework.org/schema/context"
         xsi:schemaLocation="http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans.xsd
             http://www.springframework.org/schema/context
             http://www.springframework.org/schema/context/spring-context.xsd">
     </beans>

    就是一個 xml 配置文件中的最頂層元素 <beans/>

  • 而後初始化獲得 documentReader 實例的解析對象既 this.delegate<BeanDefinitionParserDelegate>_,後面針對 _element 元素的解析將會使用到它;

➥ 後續,當前面的工做準備好了之後,來看看是如何解析 element 的?

首先,判斷 root 元素的 namespace 對應的是否是 default namespace,若不是,將進入 _step 1.3.3.3: parse custom element_;這裏咱們關注常規流程,既是當 root 元素的 namespace 是 default namespace 的流程;

遍歷 root 元素下的全部 element,

  1. 若 element 的 namespace 是 default namespace,將進入 parse default element 流程
    好比當前 element 是普通的 <bean/>
  2. 若 element 的 namespace 不是 default namespace,將進入 parse custom element 流程
    好比當前 element 是 <context:annotation-config/> 或者是 <context:component-scan/>

parse default element process

能夠看到,該流程中包含四個子流程,依次處理不一樣的 element 元素的狀況,其它三種都是比較特殊的狀況,咱們這裏,主要關注「解析 <bean/>" 元素的流程

解析 bean element 流程

這裏,爲了可以儘可能的展現出解析 <bean/> 元素的流程中的邏輯,我將使用一個比較特殊的 <bean/> 來梳理此部分的流程;

<bean name="john"
          class="com.example.Person"
          p:name="John Doe"
          p:spouse-ref="jane"/>

<bean/> 元素使用了 namespace xmlns:p="http://www.springframework.org/schema/p"

➥ 首先,經過 BeanDefintionParserDelegate 對象解析該 element,獲得一個 BeanDefinitionHolder 對象 bdHolder 實例;該解析過程當中會依次去解析 bean id, bean name, 以及相關的 scope, init, autowired model 等等屬性;見 step 1.1

➥ 其次,對 bean definition 進行相關的修飾操做,見 step 1.2

常規步驟

  1. 遍歷當前 element 中的全部 attributes,依次獲得 atttribute node
  2. 取得 node 所對應的 namespace URI,並判斷該 namespace 是不是 custom namespace,若是是 custom namespace,那麼正式進入對該 attribute node 的修飾過程,以下所述;

attribute node 的修飾過程

假設,咱們當前的 attribute node 爲 _p:spouse-ref="jane"_,看看該屬性是如何被解析的,

  1. 首先,經過 node namespace 獲得對應的 NamespaceHandler 實例 handler
    經過 xmlns:p="http://www.springframework.org/schema/p" 獲得的 NamespaceHandler 爲 SimplePropertyNamespaceHandler 對象;
  2. 其次,調用 SimplePropertyNamespaceHandler 對象對當前的元素進行解析;
    能夠看到,前面的解析並無什麼特殊的,從元素 p:spouse-ref="jane" 中解析獲得 propery name: _spouse-ref_,property value: _jane_;可是後續解析,比較特殊,須要處理 REF_SUFFIX 的狀況了,也就是當 property name 的後綴爲 -ref 的狀況,表示該 attribute 是一個 ref-bean 屬性,其屬性值引用的是其它的 bean 實例,因此呢,這裏將其 property value 封裝爲了一個 RuntimeBeanReference 對象實例,表示未來在解析該 property value 爲 Java Object 的時候,須要去初始化其引用的 bean 實例 _jane_,而後注入到當前的 property value 中;
  3. 最後,將解析後獲得的 bean definition 封裝在 bean definition holder 對象中進行返回;

➥ 最後,註冊 bean definition;

step 1.3.2 register.registerBeanDefinition(beanName, beanDefinition)_,_register 就是當前的 bean factory 實例,經過將 bean namebean definition 以鍵值對的方式在當前的 bean factory 中進行註冊;這樣,咱們就能夠經過 bean 的名字,獲得其對應的 bean definition 對象了;

➥ 寫在該小節最後,

咱們也能夠自定義某個 element 或者 element attribute,而且定義與之相關的 namespace 和 namespace handler,這樣,就可使得 Spring 容器解析自定義的元素;相似於 dubbo 配置中所使用的 <dubbo /> 自定義元素那樣;

parse custom element process

此步驟對應 register bean definitions process 步驟中的 step 1.3.3.2

該小節我將試圖使用一個經常使用的 custom element: <context:component-scan/> 來梳理整個流程;

  1. 首先獲得與 <context:component-scan /> 元素相關的 namespace uri: http://www.springframework.or...,見 _step 1.1
  2. 經過 #1 獲得的 namespace uri 解析獲得相應的 NamespaceHandler,這裏獲得的是 ContextNamespaceHandler_;見 _step 1.2
    step 1.2.1 getHandlerMappings() 返回了全部內置的 namespace uri 與 namespace handler 所一一對應的鍵值對;
  3. 使用 #2 返回的 NamespaceHandler 既 ContextNamespaceHandler 進行 parse 操做,見 _step 1.3_,參考子流程 parse element by ContextNamespaceHandler,注意,之因此這裏單獨使用一個子流程來介紹,是由於使用 ContextNamespaceHandler 來解析只是其中的一種解析狀況,未來考慮分析更多的子流程狀況;
parse element by ContextNamespaceHandler

繼續 parse custom element process 章節中所使用到的例子,<context:component-scan/> 來分析該流程,

➥ 在開始分析以前,看看 component-scan 元素長什麼樣,

注意,_component-scan_ element 自己包含 annotation-config attribute;

➥ 流程分析

首先,根據 element name: component-scan 找到對應的 BeanDefinitionParser,在 ContextNamespaceHandler 初始化的時候,便初始化設置好 8 對內置的 element nameparsers 的鍵值對;這裏,根據名字 component-scan 找到對應的 parser ComponentScanBeanDefinitionParser 對象;

其次,使用 ComponentScanBeanDefinitionParser 對象開始解析工做,

  1. 首先,解析 <context:component-scan base-package="org.shangyang"/> 獲得 basePcakges String[] 對象;
  2. 其次,初始化獲得 ClassPathBeanDefinitiionScanner 對象實例 scanner_,而後調用 _scanner.doScan 方法進入 do scan 流程,該流程中將會遍歷 base package 中所包含的全部 .class 文件,解析之,並生成相應的 bean definitions;另外在這個流程中,還要注意的是,最後會將 bean definitions 在當前的 bean factory 對象中進行註冊;
  3. 最後,這一步是從 step 1.2.4 開始,主要處理的邏輯爲,當 element 含有 annotation-config 屬性的時候,將會註冊一系列的 post-processors-bean-definitions
do scan 流程

這裏主要介紹上一個小節中 #2 步驟中所提到的 do scan 流程步驟,對應 parse element by ContextNamespaceHandler 流程圖中的 _step 1.2.3 scanner.doScan_;

➥ 先來看看 step 1.2.3.1 findCandidateComponent(basePackage)

ClassPathScanningCandidateComponentProvider.java (已刪除大量不相干代碼)

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
   Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
   try {
      String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;
      
      //1. 從當前用戶自定的 classpath 子路徑中,經過 regex 查詢到全部的所匹配的 resources;要特別注意的是,
      //   這裏爲何不直接經過 Class Loader 去獲取 classes 來進行判斷? 由於這樣的話就至關因而加載了 Class Type,而 Class Type 的加載過程是經過 Spring 容器嚴格控制的,是不容許隨隨便便加載的
      //   因此,取而代之,使用一個 File Resource 去讀取相關的字節碼,從字節碼中去解析........
      Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath); 
      boolean traceEnabled = logger.isTraceEnabled();
      boolean debugEnabled = logger.isDebugEnabled();
      
      //2. 依次遍歷用戶定義的 bean Class 對象
      for (Resource resource : resources) {
         if (traceEnabled) {
            logger.trace("Scanning " + resource);
         }
         
         if (resource.isReadable()) {
            try {
               // 將從字節碼中獲取到的相關 annotation(@Service) 以及 FileSystemResource 對象保存在 metadataReader 當中; 
               MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource); 
               if (isCandidateComponent(metadataReader)) {
                  ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                  sbd.setResource(resource);
                  sbd.setSource(resource);
                  if (isCandidateComponent(sbd)) {
                     candidates.add(sbd);
                  }
                  ...
               }
               ...
            }
            ...
         }
      }
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
   }
   return candidates;
}
  1. 代碼第 10 行

    Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);

    這一步經過遞歸搜索 base package 目錄下的全部 .class 文件,並將其字節碼封裝成 Resource[] 對象;上面的註釋解釋得很是清楚了,這裏封裝的是 .class 文件的字節碼,而非 class type;除了註解中所描述的,這裏再引伸說明下,這裏爲何不直接加載其 Class Type 還有一個緣由就是當 Spring 在加載 Class Type 的時候,頗有可能在該 Class Type 上配置了 AOP,經過 ASM 字節碼技術去修改原有的字節碼之後,再加入 Class Loader 中;因此,之類不能直接去解析 Class Type,而只能經過字節碼的方式去解析;

    這一步一樣告誡咱們,在使用 Spring 容器來開發應用的時候,開發者不要隨隨便便的自行加載 Class Type 到容器中,由於有可能在加載 Class Type 以前須要經過 Spring 容器的 ASM AOP 進行字節碼的修改之後再加載;

  2. 代碼第 23 行

    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);

    解析當前的 .class 字節碼,解析出對應的 annotation,好比 @Service,並將其協同 FileSystemResource 對象一同保存到 metadataReader 對象中;

  3. 代碼第 24 行

    protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
       for (TypeFilter tf : this.excludeFilters) {
          if (tf.match(metadataReader, this.metadataReaderFactory)) {
             return false;
          }
       }
       for (TypeFilter tf : this.includeFilters) { // includedFilters 包含三類 annotation,1. @Component 2. @ManagedBean 3. @Named
          if (tf.match(metadataReader, this.metadataReaderFactory)) {
             return isConditionMatch(metadataReader);
          }
       }
       return false;
    }

    既是從當前的 metadataReader 中去判斷是否存在 1. @Component 2. @ManagedBean 3. @Named 三種註解中的一種,若是是,則進入下面的流程

  4. 代碼 25 - 29 行,將符合 #3 標準的 annotation 封裝爲 ScannedGenericBeanDefinition annotation-bean-definition,並加入 candidates 返回

    if (isCandidateComponent(metadataReader)) {
       ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
       sbd.setResource(resource);
       sbd.setSource(resource);
       if (isCandidateComponent(sbd)) {
          candidates.add(sbd);
       }
       ...
    }

➥ 依次處理並註冊返回的 candidates

該步驟從流程圖 parse element by ContextNamespaceHandler 中的 step 1.2.3.2 開始,主要作了以下幾件事情,

  1. 設置 candiate (既 annotation bean definition) 的 scope
  2. 經過 AnnotationBeanNameGenerator 生成 bean name,由於經過 @Component、@Service 註解的方式注入的 bean 每每沒有配置 bean name,因此每每須要經過程序的方式自行生成相應的 bean name,看看內部的源碼,如何生成 bean name 的,

    /**
*/

@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {

if (definition instanceof AnnotatedBeanDefinition) {
     String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition); // 處理諸如 @Service("dogService") 的狀況
     if (StringUtils.hasText(beanName)) {
        // Explicit bean name found.
        return beanName;
     }
  }
  // Fallback: generate a unique default bean name. 裏面的實現邏輯就是經過將 Class Name 的首字母大寫編程小寫,而後返回;
  return buildDefaultBeanName(definition, registry);

}

一般狀況下,是將類名的首字母進行小寫並返回;對應 _step 1.2.2.3.3_
3. 設置 annotation bean definition 的默認值,參考 _step 1.2.4_
4. 設置 scoped proxy 到當前的 annotation bean definition
5. 最後,將 annotation bean definition 註冊到當前的 bean factory

###### 註冊 post-processor-bean-definitions

該步驟從流程圖 [parse element by ContextNamespaceHandler](#parse-element-by-ContextNamespaceHandler) 的 _step 1.2.4.2 registerAnnotationConfigProcessors_ 開始,將會依次註冊由以下 post-processor class 對象所對應的 post-processor-bean-definitions,

+ ConfigurationClassPostProcessor.class
+ AutowiredAnnotationBeanPostProcessor.class
+ RequiredAnnotationBeanPostProcessor.class
+ CommonAnnotationBeanPostProcessor.class
+ 經過 PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME 發射獲得的 class
+ EventListenerMethodProcessor.class
+ DefaultEventListenerFactory.class

注意,這裏都是經過 Class 對象註冊的,並不是註冊的實例化對象,下面,咱們來簡單分析一下注冊相關的源碼,以註冊 _AutowiredAnnotationBeanPostProcessor_ post-processor-bean-definition 爲例子,

_AnnotationConfigUtils#registerAnnotationConfigProcessors_

if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
// 將 AutowiredAnnotationBeanPostProcessor.class 封裝爲 bean definition
RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}

上面的步驟將 AutowiredAnnotationBeanPostProcessor.class 封裝爲 bean definition;

_AnnotationConfigUtils.registerPostProcessor_

private static BeanDefinitionHolder registerPostProcessor(

BeanDefinitionRegistry registry, RootBeanDefinition definition, String beanName) {

definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(beanName, definition); // 註冊 bean definition
return new BeanDefinitionHolder(definition, beanName);
}

這一步將 _AutowiredAnnotationBeanPostProcessor_ 所對應的 bean definition 注入了當前的 bean factory 當中;

_AutowiredAnnotationBeanPostProcessor_ 提供了 @Autowired 註解注入機制的實現,詳情參考 [AutowiredAnnotationBeanPostProcessor](/2017/04/05/spring-core-container-sourcecode-analysis-annotation-autowired/#AutowiredAnnotationBeanPostProcessor) 章節;
## 寫在最後

經過上述的分析,能夠清晰的看到,bean definition 的做用是什麼,就是經過 bean definition 中的描述去限定經過 Class Type 實例化獲得 instance 的業務規則,咱們看看由 [do scan 流程](#do-scan-流程) 所生成的 annotation-bean-definition<ScannedGenericBeanDefinition> 對象,

{% asset_img debug-scanned-generic-bean-definition.png %}

能夠看到,當咱們在後續要根據該 annotation-bean-definition 獲得一個 DogService 實例的時候,所要遵循的業務規則,以下所示,

Generic bean: class [org.shangyang.spring.container.DogService];
scope=;
abstract=false;
lazyInit=false;
autowireMode=0;
dependencyCheck=0;
autowireCandidate=true;
primary=false;
factoryBeanName=null;
factoryMethodName=null;
initMethodName=null;
destroyMethodName=null;
defined in file [/Users/mac/workspace/spring/framework/sourcecode-analysis/spring-core-container/spring-sourcecode-test/target/classes/org/shangyang/spring/container/DogService.class]

不過,要注意,這裏所獲得的 ScannedGenericBeanDefinition 實例,一樣沒有真正去加載 _org.shangyang.spring.container.DogService_ Class Type 到容器中,而只是將 class name `字符串`賦值給了 ScannedGenericBeanDefinition.beanClass,言外之意,未來在加載 Class Type 到容器中的時候,或許與實例化 instance 同樣也要根據 bean definitions 中的規則來限定其加載行爲,目前我所可以想到的與其相關的就是 ASM 字節碼技術,能夠在 bean definition 中定義 ASM 字節碼修改規則,來控制相關 Class Type 的加載行爲;

# References
相關文章
相關標籤/搜索