一、思考
- @ComponentScan注解是做什么的?
- basePackages的方式和basePackageClasses的方式有什么區別?你建議用哪個?為什么?
- useDefaultFilters有什么用?
- 常見的過濾器有哪些類型?說說你知道的幾個
- @ComponentScan是在哪個類中處理的?說一下大概的解析過程?
二、背景介紹
2種注冊bean的方式:
- xml中bean元素的方式
- @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工作的過程:
-
Spring會掃描指定的包,且會遞歸下面子包,得到一批類的數組
-
然后這些類會經過上面的各種過濾器,最后剩下的類會被注冊到容器中
所以玩這個注解,主要關注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、總結
-
@ComponentScan用於批量注冊bean,spring會按照這個注解的配置,遞歸掃描指定包中的所有類,將滿足條件的類批量注冊到spring容器中
-
可以通過value、basePackages、basePackageClasses 這幾個參數來配置包的掃描范圍
-
可以通過useDefaultFilters、includeFilters、excludeFilters這幾個參數來配置類的過濾器,被過濾器處理之后剩下的類會被注冊到容器中
-
指定包名的方式配置掃描范圍存在隱患,包名被重命名之后,會導致掃描實現,所以一般我們在需要掃描的包中可以創建一個標記的接口或者類,作為basePackageClasses的值,通過這個來控制包的掃描范圍
-
@CompontScan注解會被ConfigurationClassPostProcessor類遞歸處理,最終得到所有需要注冊的類。
參考:https://mp.weixin.qq.com/s/mVdARYO5N-ZgU4e0QdAgRg