定義/作用
@ComponentScan註解用於實現spring主鍵的註解掃描,會掃描特定包內的類上的註解。
源碼(對屬性進行一些簡介,會在後文中詳細講解每個屬性):
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE}) //只能作用在類上,一般作用在配置類上。
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
@AliasFor("basePackages")
//與basePackages屬性互爲別名,作用一樣
String[] value() default {};
@AliasFor("value")
//掃描的基礎包。
String[] basePackages() default {};
//掃描的類,會掃描該類所在包及其子包的組件。
Class<?>[] basePackageClasses() default {};
//bean id 生成器,定義了一套生成bean id 的規則。
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
//這個作者暫時也沒搞得太明白,不過這個用的非常少。就不解釋了。
Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;
//這個作者暫時也沒搞得太明白,不過這個用的非常少。就不解釋了。
ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;
//定義掃描的規則。
String resourcePattern() default "**/*.class";
//是否使用默認的過濾器,默認true
boolean useDefaultFilters() default true;
//包含過濾器。
ComponentScan.Filter[] includeFilters() default {};
//排除過濾器
ComponentScan.Filter[] excludeFilters() default {};
//是否延遲加載,默認false,也就是默認是bean是立即加載到容器中。
boolean lazyInit() default false;
//內部註解,是定義掃描規則的註解。
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Filter {
FilterType type() default FilterType.ANNOTATION;
@AliasFor("classes")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
String[] pattern() default {};
}
}
屬性詳解:
String[] basePackages() default {} 或 String[] value() default {};;
該屬性配置的是spring要掃描的基礎包,定義了之後,spring默認會掃描該包及其子包下的相應註解生成bean組件。
該屬性是一個數組,也就是可以定義多個基礎包。
當該屬性不設置的時候,默認掃描該配置類所在的包及其子包下的組件。
Class<?>[] basePackageClasses() default {};
配置要掃描的類的class對象,默認會掃描該類所在的包及其子包下的組件。
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;
配置bean Id的生成規則,默認是如果組件註解@Component、@Controller、@Service、@Repository、@Named、ManagedBean註解都沒有顯示配置組件id時,就會把類名第一位轉化爲小寫作爲該組件的id。如果不同包中有相同名字的類,在掃描時就會報錯。
我們可以通過配置nameGenerator屬性自定義我們的組件id生成器。
nameGenerator有個實現類AnnotationBeanNameGenerator是基於註解開發時的註解bean名稱生成器。
我們通過集成它,然後覆蓋其中的方法實現規則的重寫。
以下是一個自定義的組件id生成器,將使用類的全路徑作爲組件的id。
/**
* @author YeHaocong
* @decription 自定義基於註解開發的組件id生成器,規則是如果組件註解沒有顯示定義組件的id的話,就用類的全限定名作爲組件id
*
*/
public class CustomAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
/**
* 重寫方法
* @param definition
* @return
*/
@Override
protected String buildDefaultBeanName(BeanDefinition definition) {
//獲取類全限定名
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
//直接返回。
return beanClassName;
}
}
@Configuration
//自定義nameGenerator
@ComponentScan(basePackages = "com.componentscan",nameGenerator = CustomAnnotationBeanNameGenerator.class)
public class Config {
}
/**
* @author YeHaocong
* @decription 測試類
*
*/
public class testComponentScan {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
@Test
public void testComponentScan(){
UserService userService = (UserService) context.getBean("com.componentscan.service.impl.UserServiceImpl");
userService.addUser();
System.out.println("com.componentscan.service.impl.UserServiceImpl組件存在嗎:" + context.containsBean("com.componentscan.service.impl.UserServiceImpl"));
System.out.println("userServiceImpl組件存在嗎:" + context.containsBean("userServiceImpl"));
}
}
結果:
可以看出規則被改變了。
AnnotationBeanNameGenerator 類源碼:
public class AnnotationBeanNameGenerator implements BeanNameGenerator {
private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component";
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
if (definition instanceof AnnotatedBeanDefinition) {
//獲取註解上配置的組件id
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
if (StringUtils.hasText(beanName)) {
// Explicit bean name found.
//組件註解上有設置組件id並且組件id不爲空字符串時,直接返回使用該組件id
return beanName;
}
}
// Fallback: generate a unique default bean name.
//沒有設置。使用默認的組件id
return buildDefaultBeanName(definition, registry);
}
/**
* 獲取組件註解上設置的組件id
*/
@Nullable
protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {
AnnotationMetadata amd = annotatedDef.getMetadata();
//獲取類上的註解
Set<String> types = amd.getAnnotationTypes();
String beanName = null;
//遍歷註解
for (String type : types) {
//獲取註解上的屬性
AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type);
//檢查,註解上的屬性不爲null,
// 並且 組件註解類型是 org.springframework.stereotype.Component註解 ||
// 包含 org.springframework.stereotype.Component
//||javax.annotation.ManagedBean||javax.inject.Named
if (attributes != null && isStereotypeWithNameValue(type, amd.getMetaAnnotationTypes(type), attributes)) {
Object value = attributes.get("value");
if (value instanceof String) {
String strVal = (String) value;
if (StringUtils.hasLength(strVal)) {
if (beanName != null && !strVal.equals(beanName)) {
//一個類上可以使用多個組件註解,例如可以同時在某類上配置 Controller 和 Service註解,如果這些註解的
//配置的id不一樣,就會進入這裏拋出異常。
throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
"component names: '" + beanName + "' versus '" + strVal + "'");
}
beanName = strVal;
}
}
}
}
return beanName;
}
/**
* 可以重寫該方法,改變 組件的註解的類型
*/
protected boolean isStereotypeWithNameValue(String annotationType,
Set<String> metaAnnotationTypes, @Nullable Map<String, Object> attributes) {
//如果該註解類型是 org.springframework.stereotype.Component註解 || 包含 org.springframework.stereotype.Component
//||javax.annotation.ManagedBean||javax.inject.Named 並且attributes不爲空,並且 value key不爲空就返回true。
boolean isStereotype = annotationType.equals(COMPONENT_ANNOTATION_CLASSNAME) ||
metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) ||
annotationType.equals("javax.annotation.ManagedBean") ||
annotationType.equals("javax.inject.Named");
return (isStereotype && attributes != null && attributes.containsKey("value"));
}
/**
*
*/
protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
return buildDefaultBeanName(definition);
}
/**
* 默認id規則。可以重寫該方法修改規則
*/
protected String buildDefaultBeanName(BeanDefinition definition) {
//獲取類全限定名
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
//把包名去掉,僅剩下類名
String shortClassName = ClassUtils.getShortName(beanClassName);
//把類名第一個字符轉小寫。
return Introspector.decapitalize(shortClassName);
}
}
String resourcePattern() default “**/*.class”;
該屬性設置在基礎包的前提下,掃描的路徑**表示任意層級,所以默認是從基礎包開始任意層級的class文件。
如果修改成
*/*.class 就不會掃描子包,只會掃描當前包的class文件。
**/Serivce.class 只會掃描當前包下的Serivce.class結尾的文件。
lazyInit
設置是否把掃描到的組件延遲實例化,默認爲false,表示立即實例化。如果設置爲true,容器會在第一次getBean時實例化該組件。
boolean useDefaultFilters() default true;
useDefaultFilters屬性表示是否啓用默認的過濾器,默認過濾器是指被@Component註解 註解的註解,比如Controller、Service、Repository、Component。也有其他的@Named等。默認是true,開啓。也就是我們可以自定義組件註解,只要用@Component註解在自定義註解上面,spring默認會掃描到。
ComponentScan.Filter[] includeFilters() default {};
該屬性配置掃描包含過濾器,只包含、不排除,也就是說,例如useDefaultFilters默認爲true,默認會對@Compoent等註解進行掃描。然後使用includeFilters屬性對自定義@MyComponent註解進行包含,那麼,spring仍然會對@Compoent等註解進行掃描,對@MyComponent註解的包含不會導致原有的註解的排除。
includeFilters只包含新的掃描規則,不會排除已有的掃描規則。
規則不僅僅是註解,還有其他,後文會解釋。
這是沒有使用該屬性的情況下掃描MyComponent註解。不存在。
//配置類
@Configuration
//自定義nameGenerator
@ComponentScan(basePackages = "com.componentscan",nameGenerator = CustomAnnotationBeanNameGenerator.class)
public class Config {
}
//組件1,用@Component註解
@Component
public class UserConfig {
public void config(){
System.out.println("UserConfig==>config");
}
}
//組件2,用myComponent自定義註解。
@MyComponent
public class UserServiceImpl implements UserService{
public void addUser() {
System.out.println("添加用戶");
}
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyComponent {
String value() default "";
}
//測試類:
public class testComponentScan {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
@Test
public void testComponentScan(){
System.out.println("com.componentscan.service.impl.UserServiceImpl組件存在嗎:" + context.containsBean("com.componentscan.service.impl.UserServiceImpl"));
System.out.println("com.componentscan.config.UserConfig組件存在嗎:" + context.containsBean("com.componentscan.config.UserConfig"));
}
}
說明:用MyComponent註解的組件不存在,用@Component註解的類存在,spring掃描了@Component註解,沒有掃描@Component註解。
下面是使用使用includeFilters屬性:
@Configuration
//自定義nameGenerator
@ComponentScan(basePackages = "com.componentscan",nameGenerator = CustomAnnotationBeanNameGenerator.class,
includeFilters = {@ComponentScan.Filter(value = MyComponent.class)})
public class Config {
}
結果:
說明:因爲包含了@MyComponent註解,所以使用@MyComponent註解的組件會被掃描並實例化,但是也不排除默認會掃描的註解,比如@Component。
ComponentScan.Filter[] excludeFilters() default {};
此註解要配置不掃描的規則,比如排除了@Component註解後,用該註解的類不會被spring實例化爲組件。
注意:excludeFilters的優先級比includeFilters高,例如,兩個屬性分別配置了同樣的規則,excludeFilters屬性的配置會生效,spring會對該規則進行排除。
ComponentScan.Filter
該註解時ComponentScan註解的內部註解。定義ComponentScan掃描的規則。
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Filter {
//規則類型
FilterType type() default FilterType.ANNOTATION;
//規則的類
@AliasFor("classes")
Class<?>[] value() default {};
//與value屬性互爲別名,一致。
@AliasFor("value")
Class<?>[] classes() default {};
//規則的表達式
String[] pattern() default {};
}
//規則類型
public enum FilterType {
//基於註解類型的規則
ANNOTATION,
//基於類型的規則
ASSIGNABLE_TYPE,
//基於ASPECTJ表達式
ASPECTJ,
//基於正則表達式的規則
REGEX,
//自定義規則
CUSTOM;
private FilterType() {
}
}
自定義規則Demo
場景介紹:
一個汽車銷售集團,在成立之初,只在北京銷售汽車,我們的汽車研發後只在北京部署上線。但是隨着業務發展,現在全國各地區均有銷售大區,總部在北京,但是在不同地區的很多業務員業務都不盡相同。比如,其中一個區別是:
- 在華北地區銷售一臺普通轎車的績效算5,提成1%,銷售一臺豪華級SUV轎車的績效算8,提成1.5%。
- 在華南地區銷售一臺普通轎車的績效算4,提成0.8%,銷售一臺豪華級SUV轎車的績效算10,提成2%。
這時,我們如果針對不同的地區對對項目源碼進行刪減替換,加入一些if/else,會顯示十分的簡單粗暴,此時將有區域區別的業務類抽取成一個個接口,然後針對不同的區域提供不同的實現,用配置文件配置具體註冊哪些區域實現到容器中。然後用自定義的TypeFilter就可以實現註冊指定區域的組件到容器中。
項目目錄結構:
過濾器代碼:
/**
* @author YeHaocong
* @decription 自定義類型過濾器
*
*/
public class CustomComponentScanFilterType extends AbstractTypeHierarchyTraversingFilter {
//路徑校驗對象
private PathMatcher pathMatcher;
//地區名稱
private String regionName;
//只掃描符合該表達式該包
private static String PACKAGE_PATTERN = ClassUtils.convertClassNameToResourcePath("com.chy.carsale.service.*.*");
protected CustomComponentScanFilterType() throws IOException {
//這兩個參數考慮到是否要考慮父類和接口的信息,這裏設置爲false,都不考慮。
super(false, false);
pathMatcher = new AntPathMatcher();
try {
//載入配置文件,創建一個Properties對象
Properties props = PropertiesLoaderUtils.loadAllProperties("region/region.properties");
//獲取配置文件配置的鍵爲 region.name的值
regionName = props.getProperty("region.name");
if (regionName == null || regionName.isEmpty()){
throw new RuntimeException("配置文件region/region.properties 的region.name 不存在");
}
}
catch (RuntimeException e){
throw e;
}
}
/**
* 因爲該過濾器是排除過濾器,所以如果排除該類時,要返回true。掃描該類時,要返回false。
* @param className
* @return
*/
@Override
protected boolean matchClassName(String className) {
//判斷包名是否符合PACKAGE_PATTERN設置的表達式。
boolean isMatchPackage = isMatchPackage(className);
//不符合情況,返回true,不掃描該類。
if (!isMatchPackage){
return true;
}
try {
//根據全限定名獲取類的class對象。
Class type = ClassUtils.forName(className, CustomComponentScanFilterType.class.getClassLoader());
//獲取該類上的Region註解
Region region = (Region) type.getAnnotation(Region.class);
if (region == null)
//如果Region註解不存在,返回true,不掃描該類
return true;
//獲取region註解上的value屬性值。
String value = region.value();
if (regionName.equals(value)){
//與配置文件配置的地區值一樣時,返回false,掃描該類。
return false;
}
//返回true,不掃描該類
return true;
} catch (ClassNotFoundException e) {
throw new RuntimeException();
}
}
private boolean isMatchPackage(String className) {
//將類全路徑轉爲文件路徑格式
String path = ClassUtils.convertClassNameToResourcePath(className);
boolean match = pathMatcher.match(PACKAGE_PATTERN,path);
return match;
}
}
使用:
demo項目地址:
demo項目地址