定义/作用
@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;
}
}
使用: