10.spring系列- bean的批量注册@ComponentScan

1.@ComponentScan注解是做什么的?
2.basePackages的方式和basePackageClasses的方式有什么区别?你建议用哪个?为什么?
3.useDefaultFilters有什么用?
4.常见的过滤器有哪些类型?
5.@ComponentScan是在哪个类中处理的?说一下大概的解析过程?

@ComponentScan

@ComponentScan用于批量注册bean,这个注解会让spring去扫描某些包及其子包中所有的类,然后将满足一定条件的类作为bean注册到spring容器容器中。

具体需要扫描哪些包?以及这些包中的类满足什么条件时被注册到容器中,这些都可以通过这个注解中的参数动态配置。

先来看一下这个注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class) //@1
public @interface ComponentScan {
	//指定需要扫描的包
    @AliasFor("basePackages")
    String[] value() default {};
	//作用同value
    @AliasFor("value")
    String[] basePackages() default {};
	//指定一些类,spring容器会扫描这些类所在的包及其子包中的类
    Class<?>[] basePackageClasses() default {};
	//自定义bean名称生成器
    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;
	//过滤器:用来配置被扫描出来的那些类会被作为组件注册到容器中
    Filter[] includeFilters() default {};
	//过滤器,和includeFilters作用刚好相反,用来对扫描的类进行排除的,被排除的类不会被注册到容器中
    Filter[] excludeFilters() default {};
	//是否延迟初始化被注册的bean
    boolean lazyInit() default false;
}

@ComponentScan 工作的过程:

  1. spring会扫描指定的包,且会递归下面子包,得到一批类的数组
  2. 然后这些类会经过上面的各种过滤器,最后剩下的类会被注册到容器中

使用这个注解,主要关注两个问题:

  1. 需要扫描哪些包?通过value,backPackages,basePackageClasses这3个参数来控制
  2. 过滤器有哪些?通过useDefaultFilters,includeFilters,excludeFilters这个3个参数来控制

默认情况下,任何参数都不设置的情况下,会将@ComponentScan修饰的类所在的包作为扫描包。默认情况下useDefaultFilters为ture,spring容器内部会使用默认的过滤器,规则是:凡是类上有@Repository、@Service、@Controller、@Component这几个注解中的任何一个的,那么这个类就会被作为bean注册到spring容器中。

案例1:默认配置

整体的结构:
在这里插入图片描述

对应包下生成不同的类:

//controller包下
@Controller
public class UserController {
}
//dao包下
@Repository
public interface UserDao {
}
//dao包下
@Repository
public class UserDao {
}
//service包下和其包下impl包实现类
public interface UserService {
}
@Service
public class UserServiceImpl implements UserService {
}
//model包下
@Component
public class User {
}
//最外层包下
@ComponentScan
public class ScanBean1 {
}

我们看到注解我们使用的有@Controller,@Service,@Repository,@Component。按照@ComponentScan扫描的默认规则:扫描其所在类,所在包,以及其子包下被这几个注解标注的类都会被注册到spring容器中。

测试:

@Test
    public void testComponentScan(){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean1.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String beanName : beanDefinitionNames) {
            String[] aliases = context.getAliases(beanName);
            System.out.println(String.format("bean名称:%s,别名:%s,bean对象:%s",
                    beanName,
                    Arrays.asList(aliases),
                    context.getBean(beanName)));
        }
    }

看运行结果:

bean名称:scanBean1,别名:[],bean对象:com.spring.componentScan.ScanBean1@338fc1d8
bean名称:userController,别名:[],bean对象:com.spring.componentScan.controller.UserController@4722ef0c
bean名称:userDao,别名:[],bean对象:com.spring.componentScan.dao.UserDao@48e1f6c7
bean名称:user,别名:[],bean对象:com.spring.componentScan.model.User@55cb6996
bean名称:userServiceImpl,别名:[],bean对象:com.spring.componentScan.service.impl.UserServiceImpl@1807e3f6

案例2:指定需要扫描的包

指定扫描的包,我们通过value或者basePackage来配置,二选一。

@ComponentScan({
        "com.spring.componentScan.controller",
        "com.spring.componentScan.dao"
})
public class ScanBean2 {
}

运行结果:

bean名称:scanBean2,别名:[],bean对象:com.spring.componentScan.ScanBean2@6f204a1a
bean名称:userController,别名:[],bean对象:com.spring.componentScan.controller.UserController@2de56eb2
bean名称:userDao,别名:[],bean对象:com.spring.componentScan.dao.UserDao@5f8e8a9d

案例3:basePackageClasses指定扫描范围

在config包下,我们建个Service包,下面有两个类:

@Service
public class Service1 {
}
@Service
public class Service2 {
}

config下有个接口类:

public interface ScanBeanInterface {
}

在componentScan包下新建ScanBean3:

@ComponentScan(basePackageClasses = {
        ScanBeanInterface.class
})
public class ScanBean3 {
}

测试:

@Test
    public void testComponentScan3() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean3.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String beanName : beanDefinitionNames) {
            String[] aliases = context.getAliases(beanName);
            System.out.println(String.format("bean名称:%s,别名:%s,bean对象:%s",
                    beanName,
                    Arrays.asList(aliases),
                    context.getBean(beanName)));
        }
    }

运行结果:

bean名称:scanBean3,别名:[],bean对象:com.spring.componentScan.ScanBean3@eadd4fb
bean名称:service1,别名:[],bean对象:com.spring.componentScan.config.Service.Service1@740fb309
bean名称:service2,别名:[],bean对象:com.spring.componentScan.config.Service.Service2@7bd7d6d6

案例4:includeFilters的使用

看一个includeFilters的定义:

Filter[] includeFilters() default {};
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {

    FilterType type() default FilterType.ANNOTATION;

    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    String[] pattern() default {};

}

可以看出Filter也是一个注解,参数:

type:过滤器的类型,是个枚举类型,5种类型

ANNOTATION:通过注解的方式来筛选候选者,即判断候选者是否有指定的注解

ASSIGNABLE_TYPE:通过指定的类型来筛选候选者,即判断候选者是否是指定的类型

ASPECTJ:ASPECTJ表达式方式,即判断候选者是否匹配ASPECTJ表达式

REGEX:正则表达式方式,即判断候选者的完整名称是否和正则表达式匹配

CUSTOM:用户自定义过滤器来筛选候选者,对候选者的筛选交给用户自己来判断

value:和参数classes效果一样,二选一

classes:3种情况如下

当type=FilterType.ANNOTATION时,通过classes参数可以指定一些注解,用来判断被扫描的类上是否有classes参数指定的注解

当type=FilterType.ASSIGNABLE_TYPE时,通过classes参数可以指定一些类型,用来判断被扫描的类是否是classes参数指定的类型

当type=FilterType.CUSTOM时,表示这个过滤器是用户自定义的,classes参数就是用来指定用户自定义的过滤器,自定义的过滤器需要实现org.springframework.core.type.filter.TypeFilter接口

pattern:2种情况如下

当type=FilterType.ASPECTJ时,通过pattern来指定需要匹配的ASPECTJ表达式的值

当type=FilterType.REGEX时,通过pattern来自正则表达式的值

需求,扫描包含指定注解的类

新建一个包,在其下面建几个类

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBean {
}
//使用我们自定义的注解,当有这个注解的时候,我们将其注册到spring容器中
@MyBean
public class Service1 {
}

@Component
public class Service2 {
}
//使用Filter过滤器,注意:需要把ComponentScan的默认过滤器设置为false,否则会有默认过滤器,会把@Component、@Repository、@Service、@Controller这几个注解的类也注册到容器中
@ComponentScan(useDefaultFilters = false,
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyBean.class)
        })
public class ScanBean {
}

测试:

@Test
    public void testComponentScan4() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String beanName : beanDefinitionNames) {
            String[] aliases = context.getAliases(beanName);
            System.out.println(String.format("bean名称:%s,别名:%s,bean对象:%s",
                    beanName,
                    Arrays.asList(aliases),
                    context.getBean(beanName)));
        }
    }

结果:

bean名称:scanBean,别名:[],bean对象:com.spring.componentScan1.ScanBean@2a8448fa
bean名称:service1,别名:[],bean对象:com.spring.componentScan1.Service1@6f204a1a

扩展:自定义注解支持定义bean名称

我们对Mybean进行修改,使用@AliasFor

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component //添加一个@Component注解
public @interface MyBean {
    //添加一个AliasFor注解和vaue
    @AliasFor(annotation = Component.class)
    String value() default "";
}

service1进行修改:

//定义bean名
@MyBean("serviceBean")
public class Service1 {
}

运行测试结果:

bean名称:scanBean,别名:[],bean对象:com.spring.componentScan1.ScanBean@26abb146
bean名称:serviceBean,别名:[],bean对象:com.spring.componentScan1.Service1@72c8e7b

案例:注册指定类型的类

新建包,在新包下建类

public interface IService {
}
public class ServiceA implements IService {
}
public class ServiceB implements IService {
}
@Service
public class ServiceC {
}
@ComponentScan(
        useDefaultFilters = false, //不启用默认过滤器
        includeFilters = {
        		//我们只注册实现了IService接口的类
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IService.class)
        }
)
public class ScanBean4 {
}

测试方法:

@Test
    public void testComponentScan5() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean4.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String beanName : beanDefinitionNames) {
            String[] aliases = context.getAliases(beanName);
            System.out.println(String.format("bean名称:%s,别名:%s,bean对象:%s",
                    beanName,
                    Arrays.asList(aliases),
                    context.getBean(beanName)));
        }
    }

测试结果:

bean名称:scanBean4,别名:[],bean对象:com.spring.componentScan2.ScanBean4@1807e3f6
bean名称:serviceA,别名:[],bean对象:com.spring.componentScan2.ServiceA@480d3575
bean名称:serviceB,别名:[],bean对象:com.spring.componentScan2.ServiceB@f1da57d

尽管ServiceC有标记@Service,但是没有注册到容器中。

自定义Filter

用法

  1. 设置@Filter中type的类型为:FilterType.CUSTOM
  2. 自定义过滤器类,需要实现接口:TypeFilter
  3. 设置@Filter中的class为自定义的过滤器类型

看下TypeFilter这个接口:

@FunctionalInterface
public interface TypeFilter {
    boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
            throws IOException;
}

MetadataReader接口

类元数据读取器,可以读取一个类上的任意信息,如类上面的注解信息、类的磁盘路径信息、类的class对象的各种信息,spring进行了封装,提供了各种方便使用的方法:

public interface MetadataReader {

    /**
     * 返回类文件的资源引用
     */
    Resource getResource();

    /**
     * 返回一个ClassMetadata对象,可以通过这个读想获取类的一些元数据信息,如类的class对象、是否是接口、是否有注解、是否是抽象类、父类名称、接口名称、内部包含的之类列表等等,可以去看一下源码
     */
    ClassMetadata getClassMetadata();

    /**
     * 获取类上所有的注解信息
     */
    AnnotationMetadata getAnnotationMetadata();

}

MetadataReaderFactory接口

类元数据读取器工厂,可以通过这个类获取任意一个类的MetadataReader对象:

public interface MetadataReaderFactory {

    /**
     * 返回给定类名的MetadataReader对象
     */
    MetadataReader getMetadataReader(String className) throws IOException;

    /**
     * 返回指定资源的MetadataReader对象
     */
    MetadataReader getMetadataReader(Resource resource) throws IOException;

}

案例

1.自定义TypeFilter类:

public class MyFilter implements TypeFilter {

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        //获取类名
        String className = metadataReader.getClassMetadata().getClassName();
        try {
            //获取类信息
            Class<?> name = Class.forName(className);
            //判断这个类是不是IService类型的
            boolean assignable = IService.class.isAssignableFrom(name);
            return assignable;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return false;
    }
}

2.添加类:

public interface IService {
}
public class ServiceB implements IService {
}
@Service
public class ServiceA {
}

3.使用自定义过滤器:

@ComponentScan(useDefaultFilters = false,
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = MyFilter.class)
        })
public class ScanBean5 {
}

测试方法:

@Test
    public void testComponentScan6() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanBean5.class);
        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String beanName : beanDefinitionNames) {
            String[] aliases = context.getAliases(beanName);
            System.out.println(String.format("bean名称:%s,别名:%s,bean对象:%s",
                    beanName,
                    Arrays.asList(aliases),
                    context.getBean(beanName)));
        }
    }

运行结果:

bean名称:scanBean5,别名:[],bean对象:com.spring.componentScan3.ScanBean5@480d3575
bean名称:serviceB,别名:[],bean对象:com.spring.componentScan3.ServiceB@f1da57d

excludeFilters用法和includeFilters一样

@CompontentScan注解是被下面这个类处理的

org.springframework.context.annotation.ConfigurationClassPostProcessor

很重要,这个类建议阅读,好好研究。

总结

  1. @ComponentScan用于批量注册bean,spring会按照这个注解的配置,递归扫描指定包中的所有类,将满足条件的类批量注册到spring容器中
  2. 可以通过value、basePackages、basePackageClasses 这几个参数来配置包的扫描范围
  3. 可以通过useDefaultFilters、includeFilters、excludeFilters这几个参数来配置类的过滤器,被过滤器处理之后剩下的类会被注册到容器中
  4. 指定包名的方式配置扫描范围存在隐患,包名被重命名之后,会导致扫描实现,所以一般我们在需要扫描的包中可以创建一个标记的接口或者类,作为basePackageClasses的值,通过这个来控制包的扫描范围
  5. @CompontScan注解会被ConfigurationClassPostProcessor类递归处理,最终得到所有需要注册的类。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值