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项目地址

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
@ComponentScan是Spring框架中的注解之一,用于指定要扫描的包及其子包,以自动注册Spring容器中的Bean。它的作用是告诉Spring在指定的包中查找带有@Component注解(或其他特定注解)的类,并将其实例化为Bean,并加入到Spring容器中统一管理。 @ComponentScan可以用在@Configuration注解的类上,也可以用在普通的@Component注解的类上。当用在@Configuration注解的类上时,它会扫描指定包下的所有类并注册为Bean;当用在普通的@Component注解的类上时,它会扫描指定包下的所有类并将其作为普通Bean进行注册。 @ComponentScan可以接收一个或多个参数,常见的参数如下: - basePackages:指定要扫描的包路径,可以使用字符串数组指定多个包。例如:@ComponentScan(basePackages = {"com.example.package1", "com.example.package2"}) - basePackageClasses:指定要扫描的类所在的包,Spring会根据这些类所在的包路径进行扫描。例如:@ComponentScan(basePackageClasses = {Class1.class, Class2.class}) - includeFilters:指定要包含的过滤条件,只有符合条件的类才会被注册为Bean。例如:@ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class)) - excludeFilters:指定要排除的过滤条件,符合条件的类不会被注册为Bean。例如:@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = MyExcludeClass.class)) 除了上述常见的参数外,还可以使用其他参数来进一步细化扫描的范围和条件。通过灵活配置@ComponentScan注解,我们可以方便地进行自动扫描和注册Bean,减少手动配置的工作量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值