【Spring註解驅動開發】元件註冊-@ComponentScan-自動掃描元件&指定掃描規則

冰河團隊發表於2020-06-06

寫在前面

在實際專案中,我們更多的是使用Spring的包掃描功能對專案中的包進行掃描,凡是在指定的包或子包中的類上標註了@Repository、@Service、@Controller、@Component註解的類都會被掃描到,並將這個類注入到Spring容器中。Spring包掃描功能可以使用XML檔案進行配置,也可以直接使用@ComponentScan註解進行設定,使用@ComponentScan註解進行設定比使用XML檔案配置要簡單的多。

專案工程原始碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

使用XML檔案配置包掃描

我們可以在Spring的XML配置檔案中配置包的掃描,在配置包掃描時,需要在Spring的XML檔案中的beans節點中引入context標籤,如下所示。

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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/context/spring-context.xsd ">

接下來,我們就可以在XML檔案中定義要掃描的包了,如下所示。

<context:component-scan base-package="io.mykit.spring"/>

整個beans.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: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.xsd">

    <context:component-scan base-package="io.mykit.spring"/>

    <bean id = "person" class="io.mykit.spring.bean.Person">
        <property name="name" value="binghe"></property>
        <property name="age" value="18"></property>
    </bean>
</beans>

此時,只要在io.mykit.spring包下,或者io.mykit.spring的子包下標註了@Repository、@Service、@Controller、@Component註解的類都會被掃描到,並自動注入到Spring容器中。

此時,我們分別建立PersonDao、PersonService、和PersonController類,並在這三個類中分別新增@Repository、@Service、@Controller註解,如下所示。

  • PersonDao
package io.mykit.spring.plugins.register.dao;

import org.springframework.stereotype.Repository;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試的dao
 */
@Repository
public class PersonDao {
}
  • PersonService
package io.mykit.spring.plugins.register.service;

import org.springframework.stereotype.Service;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試的Service
 */
@Service
public class PersonService {
}
  • PersonController
package io.mykit.spring.plugins.register.controller;

import org.springframework.stereotype.Controller;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試的controller
 */
@Controller
public class PersonController {
}

接下來,我們在SpringBeanTest類中新建一個測試方法testComponentScanByXml()進行測試,如下所示。

@Test
public void testComponentScanByXml(){
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    String[] names = context.getBeanDefinitionNames();
    Arrays.stream(names).forEach(System.out::println);
}

執行測試用例,輸出的結果資訊如下所示。

personConfig
personController
personDao
personService
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
person

可以看到,除了輸出我們自己建立的bean名稱之外,也輸出了Spring內部使用的一些重要的bean名稱。

接下來,我們使用註解來完成這些功能。

使用註解配置包掃描

使用@ComponentScan註解之前我們先將beans.xml檔案中的下述配置註釋。

<context:component-scan base-package="io.mykit.spring"></context:component-scan>

註釋後如下所示。

<!--<context:component-scan base-package="io.mykit.spring"></context:component-scan>-->

使用@ComponentScan註解配置包掃描就非常Easy了!在我們的PersonConfig類上新增@ComponentScan註解,並將掃描的包指定為io.mykit.spring即可,整個的PersonConfig類如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author binghe
 * @version 1.0.0
 * @description 以註解的形式來配置Person
 */
@Configuration
@ComponentScan(value = "io.mykit.spring")
public class PersonConfig {

    @Bean("person")
    public Person person01(){
        return new Person("binghe001", 18);
    }
}

沒錯,就是這麼簡單,只需要在類上新增@ComponentScan(value = "io.mykit.spring")註解即可。

接下來,我們在SpringBeanTest類中新增testComponentScanByAnnotation()方法,如下所示。

@Test
public void testComponentScanByAnnotation(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig.class);
    String[] names = context.getBeanDefinitionNames();
    Arrays.stream(names).forEach(System.out::println);
}

執行testComponentScanByAnnotation()方法輸出的結果資訊如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
personController
personDao
personService
person

可以看到使用@ComponentScan註解同樣輸出了bean的名稱。

既然使用XML檔案和註解的方式都能夠將相應的類注入到Spring容器當中,那我們是使用XML檔案還是使用註解呢?我更傾向於使用註解,如果你確實喜歡使用XML檔案進行配置,也可以,哈哈,個人喜好嘛!好了,我們繼續。

關於@ComponentScan註解

我們點開ComponentScan註解類,如下所示。

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.type.filter.TypeFilter;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

	ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

	String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

	boolean useDefaultFilters() default true;

	Filter[] includeFilters() default {};

	Filter[] excludeFilters() default {};

	boolean lazyInit() default false;

	@Retention(RetentionPolicy.RUNTIME)
	@Target({})
	@interface Filter {
		FilterType type() default FilterType.ANNOTATION;
        
		@AliasFor("classes")
		Class<?>[] value() default {};
        
		@AliasFor("value")
		Class<?>[] classes() default {};
        
		String[] pattern() default {};
	}
}

這裡,我們著重來看ComponentScan類的兩個方法,如下所示。

Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};

includeFilters()方法表示Spring掃描的時候,只包含哪些註解,而excludeFilters()方法表示不包含哪些註解。兩個方法的返回值都是Filter[]陣列,在ComponentScan註解類的內部存在Filter註解類,大家可以看下上面的程式碼。

1.掃描時排除註解標註的類

例如,我們現在排除@Controller、@Service和@Repository註解,我們可以在PersonConfig類上通過@ComponentScan註解的excludeFilters()實現。例如,我們在PersonConfig類上新增了如下的註解。

@ComponentScan(value = "io.mykit.spring", excludeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class, Repository.class})
})

這樣,我們就使得Spring在掃描包的時候排除了使用@Controller、@Service和@Repository註解標註的類。執行SpringBeanTest類中的testComponentScanByAnnotation()方法,輸出的結果資訊如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
person

可以看到,輸出的結果資訊中不再輸出personController、personService和personDao說明Spring在進行包掃描時,忽略了@Controller、@Service和@Repository註解標註的類。

2.掃描時只包含註解標註的類

我們也可以使用ComponentScan註解類的includeFilters()來指定Spring在進行包掃描時,只包含哪些註解標註的類。

這裡需要注意的是,當我們使用includeFilters()來指定只包含哪些註解標註的類時,需要禁用預設的過濾規則。

例如,我們需要Spring在掃描時,只包含@Controller註解標註的類,可以在PersonConfig類上新增@ComponentScan註解,設定只包含@Controller註解標註的類,並禁用預設的過濾規則,如下所示。

@ComponentScan(value = "io.mykit.spring", includeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
}, useDefaultFilters = false)

此時,我們再次執行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果資訊如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
personController
person

可以看到,在輸出的結果中,只包含了@Controller註解標註的元件名稱,並沒有輸出@Service和@Repository註解標註的元件名稱。

注意:在使用includeFilters()來指定只包含哪些註解標註的類時,結果資訊中會一同輸出Spring內部的元件名稱。

3.重複註解

不知道小夥伴們有沒有注意到ComponentScan註解類上有一個如下所示的註解。

@Repeatable(ComponentScans.class)

我們先來看看@ComponentScans註解是個啥,如下所示。

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ComponentScans {
	ComponentScan[] value();
}

可以看到,在ComponentScans註解類中只宣告瞭一個返回ComponentScan[]陣列的value(),說到這裡,大家是不是就明白了,沒錯,這在Java8中是一個重複註解。

對於Java8不熟悉的小夥伴,可以到【Java8新特性】專欄檢視關於Java8新特性的文章。專欄地址小夥伴們可以猛戳下面的連結地址進行檢視:

https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&__biz=Mzg3MzE1NTIzNA==&scene=1&album_id=1325066823947321344#wechat_redirect

在Java8中表示@ComponentScan註解是一個重複註解,可以在一個類上重複使用這個註解,如下所示。

@Configuration
@ComponentScan(value = "io.mykit.spring", includeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
}, useDefaultFilters = false)
@ComponentScan(value = "io.mykit.spring", includeFilters = {
        @Filter(type = FilterType.ANNOTATION, classes = {Service.class})
}, useDefaultFilters = false)
public class PersonConfig {

    @Bean("person")
    public Person person01(){
        return new Person("binghe001", 18);
    }
}

執行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果資訊如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
personController
personService
person

可以看到,同時輸出了@Controller註解和@Service註解標註的元件名稱。

如果使用的是Java8之前的版本,我們就不能直接在類上寫多個@ComponentScan註解了。此時,我們可以在PersonConfig類上使用@ComponentScans註解,如下所示。

@ComponentScans(value = {
        @ComponentScan(value = "io.mykit.spring", includeFilters = {
                @Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
        }, useDefaultFilters = false),
        @ComponentScan(value = "io.mykit.spring", includeFilters = {
                @Filter(type = FilterType.ANNOTATION, classes = {Service.class})
        }, useDefaultFilters = false)
})

再次執行SpringBeanTest類的testComponentScanByAnnotation()方法,輸出的結果資訊如下所示。

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
personConfig
personController
personService
person

與使用多個@ComponentScan註解輸出的結果資訊相同。

總結:我們可以使用@ComponentScan註解來指定Spring掃描哪些包,可以使用excludeFilters()指定掃描時排除哪些元件,也可以使用includeFilters()指定掃描時只包含哪些元件。當使用includeFilters()指定只包含哪些元件時,需要禁用預設的過濾規則

好了,我們們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

專案工程原始碼已經提交到GitHub:https://github.com/sunshinelyz/spring-annotation

寫在最後

如果覺得文章對你有點幫助,請微信搜尋並關注「 冰河技術 」微信公眾號,跟冰河學習Spring註解驅動開發。公眾號回覆“spring註解”關鍵字,領取Spring註解驅動開發核心知識圖,讓Spring註解驅動開發不再迷茫。

相關文章