Spring高級之註解@ComponentScan詳解(超詳細)

定義/作用

@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項目地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章