我100%確定,你對@ComponentScan注解的了解僅限于皮毛
環(huán)境:Spring Boot3.2.5
1. 簡介
在Spring中,@ComponentScan注解是一個強大且常用的工具,但很多人對其功能和使用方法可能只是一知半解。本文將深入探討@ComponentScan注解的各個方面,揭示其隱藏的功能和最佳實踐,幫助讀者徹底掌握這一重要注解。
@ComponentScan注解,用于自動檢測并注冊帶有@Component、@Service、@Repository和@Controller等注解的類為Spring Bean。雖然許多開發(fā)者都熟悉它的基本用法,但其背后的復雜性和靈活性卻往往被忽視。
本篇文章中我將詳細的介紹@ComponentScan的各種配置選項,包括如何指定基礎包、排除特定組件、自定義過濾器以及處理代理和作用域等配置項的使用。
首先,我們先準備以下要使用到的類
package com.pack.ioc.scan_component
@Component
public class A {}
package com.pack.ioc.scan_component.child
@Component
public class B {}
// 配置類
package com.pack.ioc.scan_component
@Configuration
@ComponentScan
public class AppConfig {
}
接下來,準備一個運行的Main類
public class ComponentScanTest {
public static void main(String[] args) {
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationCo
context.registerBean(AppConfig.class) ;
context.refresh() ;
// 打印當前容器中注冊的所有bean集合
System.out.println(Arrays.toString(context.getBeanDefinitionNames())) ;
}
}
}
在后續(xù)的每一個示例演示中都會基于上面定義的類進行,這過程中我們還會創(chuàng)建更多的類。
2. 實戰(zhàn)案例
2.1 基本使用
在上面準備的基本環(huán)境中,在AppConfig類是僅僅是添加了@ComponentScan注解,沒有配置任何的選項,運行程序輸出如下:
[..., com.pack.ioc.scan_component.AppConfig, a, b]
這里將我們自定義的類都打印了(這里省去Spring容器自動注冊bean)。根據(jù)運行結果看出,雖然A,B不再同一個包中,但是B所在的包是AppConfig所在的包的子包中,所以@ComponentScan會處理當前包及其子包中的類。
2.2 指定basePackages
@ComponentScan(basePackages = {"com.pack.ioc.scan_component.child"})
public class AppConfig {}
這里只是指定了一個子包,運行結果:
[..., com.pack.ioc.scan_component.AppConfig, b]
此時,A類不會包含在內(nèi)。
2.3 指定basePackageClasses
@ComponentScan(basePackageClasses = {B.class})
public class AppConfig {}
這里可以通過指定Class對象,這樣會根據(jù)該Class對象所在的包進行掃描,運行結果
[..., com.pack.ioc.scan_component.AppConfig, b]
Spring內(nèi)部會自動將你配置的所有Class對象所在的包,作為掃描路徑。
2.4 指定includeFilters
注:@ComponentScan注解還包括excludeFilters排除過濾屬性,這里我們只介紹includeFilters屬性。
由于includeFilters中配置的是@Filter,而該注解中定義了type屬性,該type屬性是FilterType枚舉類,其中包括以下值:
public enum FilterType {
ANNOTATION,
ASSIGNABLE_TYPE,
ASPECTJ,
REGEX,
CUSTOM
}
接下來,我們將依次介紹這些枚舉值。
- 過濾類型FilterType.ANNOTATION
在目標類上存在指定的注解。
首先,我們自定義一個注解,然后通過該注解標準類。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Pack {
}
// 使用該注解標注下面的類
@Pack
public class C {
}
在默認情況下,Spring容器是不會識別到C這個類的,通過如下配置后就不一樣了
@ComponentScan(includeFilters = {
@Filter(type = FilterType.ANNOTATION, classes = {Pack.class})
})
public class AppConfig {}
運行結果
[..., com.pack.ioc.scan_component.AppConfig, a, c, b]
Spring默認的@Component注解及我們自定義的@Pack注解都生效了。
- 過濾類型FilterType.ASPECTJ
與目標類匹配的AspectJ類型表達式。
首先,自定義如下類。
public class PackClass {
}
修改配置類
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.ASPECTJ,
pattern = {"com.pack..Pack*"}
)
})
public class AppConfig {}
com.pack..Pack*:com.pack包及子包下的以Pack開頭的類都將匹配。
運行結果
[..., com.pack.ioc.scan_component.AppConfig, a, b, packClass]
注:在本例中@ComponentScan掃描的包還是當前AppConfig所在的包及其子包。
- 過濾類型FilterType.ASSIGNABLE_TYPE
目標類可以是這里給定的類或接口。
首先,自定義DAO接口。
public interface DAO {
}
public class Person implements DAO {
}
修改配置類
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.ASSIGNABLE_TYPE, classes = {DAO.class}
)
})
public class AppConfig {}
只要我們定義的類實現(xiàn)了DAO接口都將匹配。
運行結果
[..., com.pack.ioc.scan_component.AppConfig, a, b, person]
通過classes你可以指定多個接口或者是父類。
- 過濾類型FilterType.REGEX
與目標組件的類名匹配的正則表達式。
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.REGEX,
pattern = {"com\\.pack\\..*.Pack.*"}
)
})
public class AppConfig {}
運行結果
[..., com.pack.ioc.scan_component.AppConfig, a, b, packClass]
注意:在寫正則表達式時 "." 需要進行轉(zhuǎn)義。
- 過濾類型FilterType.CUSTOM
org.springframework.core.type.TypeFilter接口的自定義實現(xiàn)。
public class PackTypeFilter implements TypeFilter {
@Override
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata() ;
return annotationMetadata.hasAnnotation(Pack.class.getName()) ;
}
}
在自定義的類型過濾中,我們判斷類上是否存在@Pack。
修改配置類
@ComponentScan(includeFilters = {
@Filter(
type = FilterType.CUSTOM,
classes = {PackTypeFilter.class}
)
})
public class AppConfig {}
這不僅包括了內(nèi)置的過濾器會生效(過濾@Component注解),同時我們自定義的過濾器也會起到作用。
以上針對每一種過濾類型講解完了,你是否有點暈,這里指定了type后,接下來是配置pattern還是classes屬性呢?大家記住如下組合即可。
類型 | 配置的屬性 |
ANNOTATION、ASSIGNABLE_TYPE、CUSTOM | classes |
ASPECTJ、REGEX | pattern |
只有這兩種組合沒有其它組合。
2.5 指定lazyInit
指定掃描到的類是否進行延遲初始化。
@Component
public class A implements InitializingBean {
@PostConstruct
public void init() {
System.err.println("A init...") ;
}
@Override
public void afterPropertiesSet() throws Exception {
System.err.println("A afterPropertiesSet...") ;
}
}
修改配置類
@ComponentScan(lazyInit = true)
public class AppConfig {}
運行結果
圖片
并沒有打印在A類中定義的初始化回調(diào)。
2.6 指定useDefaultFilters
指示是否應啟用對帶有@Component、@Repository、@Service或@Controller注解的類的自動檢測。
修改配置類
@ComponentScan(
useDefaultFilters = false,
includeFilters = {
@Filter(
type = FilterType.CUSTOM,
classes = {PackTypeFilter.class}
)
})
public class AppConfig {}
運行結果
[..., com.pack.ioc.scan_component.AppConfig, c]
我們定義的A,B兩個類都沒有打?。ㄟ@2個類是有@Component注解的)。這說明我們就關閉了默認的過濾器功能。
2.7 指定nameGenerator
通過指定該屬性,掃描到的組件將通過該值來生成對應的beanName。
自定義BeanNameGenerator
public class PackBeanNameGenerator extends AnnotationBeanNameGenerator {
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
String beanName = super.generateBeanName(definition, registry) ;
beanName = beanName.replaceFirst("^.", beanName.substring(0, 1).toUpperCase()) ;
return "pack" + beanName ;
}
}
在這里我們將所有的beanName都添加一個 pack 前綴。
修改配置類
@ComponentScan(nameGenerator = PackBeanNameGenerator.class)
public class AppConfig {}
運行結果
[..., com.pack.ioc.scan_component.AppConfig, packA, packB]
除配置類外,其它beanName都以pack開頭。
2.8 指定scopedProxy
指示是否應為檢測到的組件生成代理。
首先,修改A類如下:
@Scope
@Component
public class A implements InitializingBean {}
在類A上添加了@Scope作用域注解。注:要使得scopedProxy生效你必須對bean定義作用域(只要有該注解即可,而至于什么作用域無所謂)。
修改配置類
@ComponentScan(scopedProxy = ScopedProxyMode.TARGET_CLASS)
public class AppConfig {}
這里設置為代理目標類。
接下來,我們做如下的測試
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.registerBean(AppConfig.class) ;
context.refresh() ;
System.out.println(Arrays.toString(context.getBeanDefinitionNames())) ;
System.err.println(context.getBean(A.class).getClass().getName()) ;
}
運行結果
圖片
Bean A生成的是通過cglib代理后的對象。
2.9 指定scopeResolver
用于解析檢測到的組件的作用域。在默認情況下檢測的是@Scope定義的作用域。
特別說明:只有當@ComponentScan中的scopedProxy屬性設置為ScopedProxyMode.DEFAULT時,該屬性才會生效。
接下來,通過實例講解。
首先,我們自定義作用域注解(為了體現(xiàn)自定義自定義ScopeMetadataResolver的意義)。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PackScope {
@AliasFor("scopeName")
String value() default "";
@AliasFor("value")
String scopeName() default "";
ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
該自定義作用域注解與@Scope中定義的屬性一致。
自定義作用域解析器。
public class PackAnnotationScopeMetadataResolver extends AnnotationScopeMetadataResolver {
public PackAnnotationScopeMetadataResolver() {
setScopeAnnotationType(PackScope.class) ;
}
}
解析器中設置我們自定義的作用域注解。
修改的類A。
@PackScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class A implements InitializingBean {}
運行結果
自定義的作用域注解及自定義解析器生效了。