spring注解之@ComponentScan、@ComponentScans


 一、思考

  1. @ComponentScan注解是做什么的?
  2. basePackages的方式和basePackageClasses的方式有什么區別?你建議用哪個?為什么?
  3. useDefaultFilters有什么用?
  4. 常見的過濾器有哪些類型?說說你知道的幾個
  5. @ComponentScan是在哪個類中處理的?說一下大概的解析過程?

二、背景介紹

2種注冊bean的方式:

  1. xml中bean元素的方式
  2. @Bean注解標注方法的方式

通常情況下,項目中大部分類都需要交給spring去管理,按照上面這2種方式,代碼量還是挺大的。
為了更方便bean的注冊,Spring提供了批量的方式注冊bean,方便大量bean批量注冊,spring中的@ComponentScan就是干這個事情的。

1、@ComponentScan

@ComponentScan用於批量注冊bean

這個注解會讓spring去掃描某些包及其子包中所有的類,然后將滿足一定條件的類作為bean注冊到spring容器容器中。

該注解默認會掃描該類所在的包下所有的配置類,相當於之前的 <context:component-scan>

具體需要掃描哪些包?以及這些包中的類滿足什么條件時被注冊到容器中,這些都可以通過這個注解中的參數動態配置。

先來看一下這個注解的定義:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class) //@1
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 "**/*.class";

    boolean useDefaultFilters() default true;

    Filter[] includeFilters() default {};

    Filter[] excludeFilters() default {};

    boolean lazyInit() default false;
}

定義上可以看出此注解可以用在任何類型上面,不過我們通常將其用在類上面。

常用參數:

value:指定需要掃描的包,如:com.javacode2018

basePackages:作用同value;value和basePackages不能同時存在設置,可二選一

basePackageClasses:指定一些類,spring容器會掃描這些類所在的包及其子包中的類

nameGenerator:自定義bean名稱生成器

resourcePattern:需要掃描包中的那些資源,默認是:**/*.class,即會掃描指定包中所有的class文件

useDefaultFilters:對掃描的類是否啟用默認過濾器,默認為true

includeFilters:過濾器:用來配置被掃描出來的那些類會被作為組件注冊到容器中

excludeFilters:過濾器,和includeFilters作用剛好相反,用來對掃描的類進行排除的,被排除的類不會被注冊到容器中

lazyInit:是否延遲初始化被注冊的bean

@1:@Repeatable(ComponentScans.class),這個注解可以同時使用多個

@ComponentScan工作的過程:

  1. Spring會掃描指定的包,且會遞歸下面子包,得到一批類的數組

  2. 然后這些類會經過上面的各種過濾器,最后剩下的類會被注冊到容器中

所以玩這個注解,主要關注2個問題:

第一個:需要掃描哪些包?通過value、backPackages、basePackageClasses這3個參數來控制

第二:過濾器有哪些?通過useDefaultFilters、includeFilters、excludeFilters這3個參數來控制過濾器

這兩個問題搞清楚了,就可以確定哪些類會被注冊到容器中。

默認情況下,任何參數都不設置的情況下,此時,會將@ComponentScan修飾的類所在的包作為掃描包;默認情況下useDefaultFilters為true這個為true的時候,spring容器內部會使用默認過濾器,規則是:凡是類上有@Repository、@Service、@Controller、@Component這幾個注解中的任何一個的,那么這個類就會被作為bean注冊到spring容器中,所以默認情況下,只需在類上加上這幾個注解中的任何一個,這些類就會自動交給spring容器來管理了。

2、@Component、@Repository、@Service、@Controller

這幾個注解都是spring提供的。

先說一下@Component這個注解,看一下其定義:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}

從定義中可以看出,這個注解可以用在任何類型上面。

通常情況下將這個注解用在類上面,標注這個類為一個組件,默認情況下,被掃描的時候會被作為bean注冊到容器中。

value參數:被注冊為bean的時候,用來指定bean的名稱,如果不指定,默認為類名首字母小寫。如:類UserService對應的beanname為userService

再來看看@Repository源碼如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {

    @AliasFor(annotation = Component.class)
    String value() default "";

}

Repository上面有@Component注解。

value參數上面有@AliasFor(annotation = Component.class),設置value參數的時候,也相當於給@Component注解中的value設置值。

其他兩個注解@Service、@Controller源碼和@Repository源碼類似。

這4個注解本質上是沒有任何差別,都可以用在類上面,表示這個類被spring容器掃描的時候,可以作為一個bean組件注冊到spring容器中。

spring容器中對這4個注解的解析並沒有進行區分,統一采用@Component注解的方式進行解析,所以這幾個注解之間可以相互替換。

spring提供這4個注解,是為了讓系統更清晰,通常情況下,系統是分層結構的,多數系統一般分為controller層、service層、dao層

@controller通常用來標注controller層組件,@service注解標注service層的組件,@Repository標注dao層的組件,這樣可以讓整個系統的結構更清晰,當看到這些注解的時候,會和清晰的知道屬於哪個層,對於spring來說,將這3個注解替換成@Component注解,對系統沒有任何影響,產生的效果是一樣的

下面通過案例來感受@ComponentScan各種用法。

3、案例1:任何參數未設置

UserController

package com.javacode2018.lesson001.demo22.test1.controller;

import org.springframework.stereotype.Controller;

@Controller
public class UserController {
}

UserService

package com.javacode2018.lesson001.demo22.test1.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {
}

UserDao

package com.javacode2018.lesson001.demo22.test1.dao;

import org.springframework.stereotype.Repository;

@Repository
public class UserDao {
}

UserModel

package com.javacode2018.lesson001.demo22.test1;

import org.springframework.stereotype.Component;

@Component
public class UserModel {
}

上面幾個類中,分別使用了4種注解。

@CompontentScan修飾的類

package com.javacode2018.lesson001.demo22.test1;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class ScanBean1 {
}

測試用例

package com.javacode2018.lesson001.demo22;

import com.javacode2018.lesson001.demo22.test1.ScanBean1;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ComponentScanTest {

    @Test
    public void test1() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean1.class);
        for (String beanName : context.getBeanDefinitionNames()) {
            System.out.println(beanName + "->" + context.getBean(beanName));
        }
    }
}

@1:使用AnnotationConfigApplicationContext作為ioc容器,將ScanBean作為參數傳入。

默認會掃描ScanBean類所在的包中的所有類,類上有@Component、@Repository、@Service、@Controller任何一個注解的都會被注冊到容器中

運行輸出

部分輸出如下:

userModel->com.javacode2018.lesson001.demo22.test1.UserModel@595b007d
userController->com.javacode2018.lesson001.demo22.test1.controller.UserController@72d1ad2e
userDao->com.javacode2018.lesson001.demo22.test1.dao.UserDao@2d7275fc
userService->com.javacode2018.lesson001.demo22.test1.service.UserService@399f45b1

注意最后4行這幾個bean,都被注冊成功了。

4、案例2:指定需要掃描的包

指定需要掃毛哪些包,可以通過value或者basePackage來配置,二者選其一,都配置運行會報錯,下面我們通過value來配置。

ScanBean2

package com.javacode2018.lesson001.demo22.test2;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan({
        "com.javacode2018.lesson001.demo22.test1.controller",
        "com.javacode2018.lesson001.demo22.test1.service"
})
public class ScanBean2 {
}

上面指定了2需要掃描的包,這兩個包中有2個類。

測試用例

ComponentScanTest中新增個方法

@Test
public void test2() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean2.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(beanName + "->" + context.getBean(beanName));
    }
}

運行輸出

截取了關鍵幾行如下:

userController->com.javacode2018.lesson001.demo22.test1.controller.UserController@dd8ba08
userService->com.javacode2018.lesson001.demo22.test1.service.UserService@245b4bdc

可以看出只有controller包和service包中的2個類被注冊為bean了。

注意

指定包名的方式掃描存在的一個隱患,若包被重名了,會導致掃描會失效,一般情況下面我們使用basePackageClasses的方式來指定需要掃描的包,這個參數可以指定一些類型,默認會掃描這些類所在的包及其子包中所有的類,這種方式可以有效避免這種問題。

下面來看一下basePackageClasses的方式。

5、案例:basePackageClasses指定掃描范圍

我們可以在需要掃描的包中定義一個標記的接口或者類,他們的唯一的作用是作為basePackageClasses的值,其他沒有任何用途。

下面我們定義這樣一個接口

package com.javacode2018.lesson001.demo22.test6.beans;

public interface ScanClass {
}

再來定義2個類,用@Component注解標記

package com.javacode2018.lesson001.demo22.test6.beans;

import org.springframework.stereotype.Component;

@Component
public class Service1 {
}
package com.javacode2018.lesson001.demo22.test6.beans;

import org.springframework.stereotype.Component;

@Component
public class Service2 {
}

來一個@CompontentScan標記的類

package com.javacode2018.lesson001.demo22.test6;

import com.javacode2018.lesson001.demo22.test6.beans.ScanClass;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackageClasses = ScanClass.class)
public class ScanBean6 {
}

測試用例

ComponentScanTest中新增個方法

@Test
public void test6() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean6.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(beanName + "->" + context.getBean(beanName));
    }
}

運行輸出

service1->com.javacode2018.lesson001.demo22.test6.beans.Service1@79924b
service2->com.javacode2018.lesson001.demo22.test6.beans.Service2@7b9a4292

6、includeFilters的使用

用法

再來看一下includeFilters這個參數的定義:

Filter[] includeFilters() default {};

是一個Filter類型的數組,多個Filter之間為或者關系,即滿足任意一個就可以了,看一下Filter的代碼:

@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {

    FilterType type() default FilterType.ANNOTATION;

    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    String[] pattern() default {};

}

可以看出Filter也是一個注解,參數:

type:過濾器的類型,是個枚舉類型,5種類型

ANNOTATION:通過注解的方式來篩選候選者,即判斷候選者是否有指定的注解

ASSIGNABLE_TYPE:通過指定的類型來篩選候選者,即判斷候選者是否是指定的類型

ASPECTJ:ASPECTJ表達式方式,即判斷候選者是否匹配ASPECTJ表達式

REGEX:正則表達式方式,即判斷候選者的完整名稱是否和正則表達式匹配

CUSTOM:用戶自定義過濾器來篩選候選者,對候選者的篩選交給用戶自己來判斷

value:和參數classes效果一樣,二選一

classes:3種情況如下

當type=FilterType.ANNOTATION時,通過classes參數可以指定一些注解,用來判斷被掃描的類上是否有classes參數指定的注解

當type=FilterType.ASSIGNABLE_TYPE時,通過classes參數可以指定一些類型,用來判斷被掃描的類是否是classes參數指定的類型

當type=FilterType.CUSTOM時,表示這個過濾器是用戶自定義的,classes參數就是用來指定用戶自定義的過濾器,自定義的過濾器需要實現org.springframework.core.type.filter.TypeFilter接口

pattern:2種情況如下

當type=FilterType.ASPECTJ時,通過pattern來指定需要匹配的ASPECTJ表達式的值

當type=FilterType.REGEX時,通過pattern來自正則表達式的值

7、案例:掃描包含注解的類

需求

我們自定義一個注解,讓標注有這些注解的類自動注冊到容器中

代碼實現

下面的代碼都在com.javacode2018.lesson001.demo22.test3包中。

定義一個注解

package com.javacode2018.lesson001.demo22.test3;

import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBean {
}

創建一個類,使用這個注解標注

package com.javacode2018.lesson001.demo22.test3;

@MyBean public class Service1 {
}

再來一個類,使用spring中的`@Compontent`標注

package com.javacode2018.lesson001.demo22.test3;

import org.springframework.stereotype.Component;

@Component public class Service2 {
}

再來一個類,使用@CompontentScan標注

package com.javacode2018.lesson001.demo22.test3;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@ComponentScan(includeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyBean.class)
})
public class ScanBean3 {
}

上面指定了Filter的type為注解的類型,只要類上面有@MyBean注解的,都會被作為bean注冊到容器中。

測試用例

ComponentScanTest中新增個測試用例

@Test
public void test3() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean3.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(beanName + "->" + context.getBean(beanName));
    }
}

運行輸出,截取了主要的幾行

service1->com.javacode2018.lesson001.demo22.test3.Service1@6b81ce95
service2->com.javacode2018.lesson001.demo22.test3.Service2@2a798d51

Service1上標注了@MyBean注解,被注冊到容器了,但是Service2沒有標注@MyBean啊,怎么也被注冊到容器了?

原因:Service2上標注了@Compontent注解,而@CompontentScan注解中的useDefaultFilters默認是true,表示也會啟用默認的過濾器,而默認的過濾器會將標注有@Component、@Repository、@Service、@Controller這幾個注解的類也注冊到容器中

如果我們只想將標注有@MyBean注解的bean注冊到容器,需要將默認過濾器關閉,即:useDefaultFilters=false,我們修改一下ScanBean3的代碼如下:

@ComponentScan(
        useDefaultFilters = false, //不啟用默認過濾器
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyBean.class)
        })
public class ScanBean3 {
}

再次運行test3輸出:

service1->com.javacode2018.lesson001.demo22.test3.Service1@294425a7

擴展:自定義注解支持定義bean名稱

上面的自定義的@MyBean注解,是無法指定bean的名稱的,可以對這個注解做一下改造,加個value參數來指定bean的名稱,如下:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component //@1
public @interface MyBean {
   @AliasFor(annotation = Component.class) //@2
    String value() default ""; //@3
}

重點在於@1和@2這2個地方的代碼,通過上面的參數可以間接給@Component注解中的value設置值。

這塊用到了@AliasFor注解,對這塊不了解的,可以去看一下:java注解詳解及spring對注解的增強

修改一下Service1的代碼:

@MyBean("service1Bean")
public class Service1 {
}

運行test3用例輸出:

service1Bean->com.javacode2018.lesson001.demo22.test3.Service1@222545dc

此時bean名稱就變成了service1Bean

8、案例:包含指定類型的類

下面的代碼都位於com.javacode2018.lesson001.demo22.test4包中。

來個接口

package com.javacode2018.lesson001.demo22.test4;

public interface IService {
}

讓spring來進行掃描,類型滿足IService的都將其注冊到容器中。

來2個實現類

package com.javacode2018.lesson001.demo22.test4;

public class Service1 implements IService {
}
package com.javacode2018.lesson001.demo22.test4;

public class Service2 implements IService {
}

來一個@CompontentScan標注的類

package com.javacode2018.lesson001.demo22.test4;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@ComponentScan(
        useDefaultFilters = false, //不啟用默認過濾器
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IService.class) //@1
        })
public class ScanBean4 {
}

@1:被掃描的類滿足IService.class.isAssignableFrom(被掃描的類)條件的都會被注冊到spring容器中

來個測試用例

ComponentScanTest中新增個測試用例

@Test
public void test4() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean4.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(beanName + "->" + context.getBean(beanName));
    }
}

運行輸出

service1->com.javacode2018.lesson001.demo22.test4.Service1@6379eb
service2->com.javacode2018.lesson001.demo22.test4.Service2@294425a7

9、自定義Filter

用法

有時候我們需要用到自定義的過濾器,使用自定義過濾器的步驟:

1.設置@Filter中type的類型為:FilterType.CUSTOM
2.自定義過濾器類,需要實現接口:org.springframework.core.type.filter.TypeFilter
3.設置@Filter中的classses為自定義的過濾器類型

來看一下TypeFilter這個接口的定義:

是一個函數式接口,包含一個match方法,方法返回boolean類型,有2個參數,都是接口類型的,下面介紹一下這2個接口。

MetadataReader接口

類元數據讀取器,可以讀取一個類上的任意信息,如類上面的注解信息、類的磁盤路徑信息、類的class對象的各種信息,spring進行了封裝,提供了各種方便使用的方法。

看一下這個接口的定義:

public interface MetadataReader {

    /**
     * 返回類文件的資源引用
     */
    Resource getResource();

    /**
     * 返回一個ClassMetadata對象,可以通過這個讀想獲取類的一些元數據信息,如類的class對象、是否是接口、是否有注解、是否是抽象類、父類名稱、接口名稱、內部包含的之類列表等等,可以去看一下源碼
     */
    ClassMetadata getClassMetadata();

    /**
     * 獲取類上所有的注解信息
     */
    AnnotationMetadata getAnnotationMetadata();

}

MetadataReaderFactory接口

類元數據讀取器工廠,可以通過這個類獲取任意一個類的MetadataReader對象。

源碼:

public interface MetadataReaderFactory {

    /**
     * 返回給定類名的MetadataReader對象
     */
    MetadataReader getMetadataReader(String className) throws IOException;

    /**
     * 返回指定資源的MetadataReader對象
     */
    MetadataReader getMetadataReader(Resource resource) throws IOException;

}

10、自定義Filter案例

需求

我們來個自定義的Filter,判斷被掃描的類如果是IService接口類型的,就讓其注冊到容器中。

代碼實現

來個自定義的TypeFilter類:

package com.javacode2018.lesson001.demo22.test5;

import com.javacode2018.lesson001.demo22.test4.IService;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;

public class MyFilter implements TypeFilter {
    /**
     * @param metadataReader
     * @param metadataReaderFactory
     * @return
     * @throws IOException
     */
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        Class curClass = null;
        try {
            //當前被掃描的類
            curClass = Class.forName(metadataReader.getClassMetadata().getClassName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        //判斷curClass是否是IService類型
        boolean result = IService.class.isAssignableFrom(curClass);
        return result;
    }
}

來一個@CompontentScan標注的類

package com.javacode2018.lesson001.demo22.test5;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@ComponentScan(
        basePackages = {"com.javacode2018.lesson001.demo22.test4"},
        useDefaultFilters = false, //不啟用默認過濾器
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = MyFilter.class) //@1
        })
public class ScanBean5 {
}

@1:type為FilterType.CUSTOM,表示Filter是用戶自定義的,classes為自定義的過濾器

再來個測試用例

ComponentScanTest中新增個測試用例

@Test
public void test5() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean5.class);
    for (String beanName : context.getBeanDefinitionNames()) {
        System.out.println(beanName + "->" + context.getBean(beanName));
    }
}

運行輸出

service1->com.javacode2018.lesson001.demo22.test4.Service1@4cc451f2
service2->com.javacode2018.lesson001.demo22.test4.Service2@6379eb

11、excludeFilters

配置排除的過濾器,滿足這些過濾器的類不會被注冊到容器中,用法上面和includeFilters用一樣

12、@ComponentScan重復使用

從這個注解的定義上可以看出這個注解可以同時使用多個,如:

@ComponentScan(basePackageClasses = ScanClass.class)
@ComponentScan(
        useDefaultFilters = false, //不啟用默認過濾器
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IService.class)
        })
public class ScanBean7 {
}

還有一種寫法,使用@ComponentScans的方式:

@ComponentScans({
        @ComponentScan(basePackageClasses = ScanClass.class),
        @ComponentScan(
                useDefaultFilters = false, //不啟用默認過濾器
                includeFilters = {
                        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IService.class)
                })})
public class ScanBean7 {
}

13、Spring中這塊的源碼

@CompontentScan注解是被下面這個類處理的

org.springframework.context.annotation.ConfigurationClassPostProcessor

這個類非常非常關鍵,主要用戶bean的注冊,@Configuration,@Bean注解也是被這個類處理的。

還有下面這些注解:

@PropertySource
@Import
@ImportResource
@Compontent

以上這些注解都是被ConfigurationClassPostProcessor這個類處理的,內部會遞歸處理這些注解,完成bean的注冊。

以@CompontentScan來說一下過程,第一次掃描之后會得到一批需要注冊的類,然后會對這些需要注冊的類進行遍歷,判斷是否有上面任意一個注解,如果有,會將這個類交給ConfigurationClassPostProcessor繼續處理,直到遞歸完成所有bean的注冊。

想成為高手,這個類是必看的。

14、總結

  1. @ComponentScan用於批量注冊bean,spring會按照這個注解的配置,遞歸掃描指定包中的所有類,將滿足條件的類批量注冊到spring容器中

  2. 可以通過value、basePackages、basePackageClasses 這幾個參數來配置包的掃描范圍

  3. 可以通過useDefaultFilters、includeFilters、excludeFilters這幾個參數來配置類的過濾器,被過濾器處理之后剩下的類會被注冊到容器中

  4. 指定包名的方式配置掃描范圍存在隱患,包名被重命名之后,會導致掃描實現,所以一般我們在需要掃描的包中可以創建一個標記的接口或者類,作為basePackageClasses的值,通過這個來控制包的掃描范圍

  5. @CompontScan注解會被ConfigurationClassPostProcessor類遞歸處理,最終得到所有需要注冊的類。

參考:https://mp.weixin.qq.com/s/mVdARYO5N-ZgU4e0QdAgRg

 


免責聲明!

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



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