Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

Spring详解(七)----基于注解的方式装配Bean

1、注解装配Bean介绍

通过前面的学习,我们已经知道如何通过XML的方式去装配Bean了,但是我们在实际的开发中,为了简化开发,更多的时候会使用到注解(annotation)的方式来装配Bean。因为注解可以大量减少繁琐的XML配置,并且注解的功能更为强大,它既能实现XML的功能,也提供了自动装配的功能,更加有利于开发,这就是“约定优于配置”原则,简称CoC(Convention over Configuration)。Spring提供了两种方式让Spring IOC容器发现Bean:

  • 组件扫描:通过定义资源的方式,让Spring IOC容器扫描资源所在的包,从而装配Bean。
  • 自动装配:通过注解自动找到依赖关系中所需要的Bean,即通过@Autowired或@Resource自动注入Bean对象。

所以在后面的学习中都会以注解为主。下面来学习下组件扫描和使用注解进行自动装配。

2、使用注解装配Bean

Spring提供了对Annotation(注解)技术的全面支持。Spring中定义了一系列的注解,常用的注解如表所示:

注解名称
描述
@Component 作用在类上的注解,可以使用此注解来描述Spring中的Bean,但是它是一个泛华的概念,仅仅表示一个组件,可以作用在任何层次。白话文描述:当某个类上用该注解修饰时,表示Spring 会把这个类扫描成一个Bean实例,等价于XML方式中定义的:<bean id="user" class="com.thr.spring.pojo.User">,此时可以直接简写成@Component(value = "user") 或者 @Component("user"),甚至直接写成@Component,如果不写括号里面的内容,默认以类名的首字母小写的形式作为 id 配置到容器中。
@Repository 通常用于对访问层DAO实现类进行标注,其功能与@Component相同,只是名字不同。
@Service 通常用于对业务层Service实现类进行标注,其功能与@Component相同,只是名字不同。
@Controller 通常用于对控制层Controller实现类进行标注,其功能与@Component相同,只是名字不同。
@Autowired 用于对Bean的属性变量、属性的setter()方法即构造方法进行标注,配合对应的注解处理完成Bean的自动装配工作。默认按照Bean的类型进行装配,说简单点就自动注入另一个对象,相当于<property name="" ref=""/>
@Resource 其作用于@Autowired一样,区别在于@Autowired默认按照Bean类型装配,而@Resource默认按照Bean实例名称进行装配。@Resource中有两个重要的属性:name和type。Spring将那么属性解析为Bean实例名称,type进行为Bean实例类型。若指定了name属性,则按照实例名称进行装配;若指定了type属性,则按照Bean类型进行装配;若都无法匹配,则抛出NoSuchBeanDefinitionException异常。
@Qualifier 与@Autowired注解配合使用,会将默认的按Bean类型装配修改为按Bean的实例名称进行装配,Bean的实例名称有@Qualifier注解的参数指定。
@Primary 可以作用在类上,也可以配合@Bean作用在方法上,表示优先使用该注解标志的Bean。
@Value 相当于<property name="" value=""/>,这个注解表示注入一个值,但是这里只是一个简单值,如果是注入一个对象得用另一个注解(@Autowired 或者@Resource )。

使用注解装配Bean简单举例,来看之前的User类,并用@Component进行装配(或者@Repository、@Service、@Controller):

/**
 * 用户实体类 用@Component注解将User类标注为一Bean
 *
 * @author tanghaorong
 */
@Data
@Component(value = "user")
public class User {
    @Value(value = "2020")
    private Integer userId;
    @Value(value = "小唐")
    private String userName;
    @Value(value = "20")
    private Integer userAge;
    @Value(value = "123456")
    private String userPwd;
    @Value(value = "中国北京")
    private String userAddress;

    /**
     * 装配对象属性这里下面介绍--使用注解自动装配
     */
    private GirlFriend girlFriend;
}

GirlFriend实体对象:

/**
 * GirlFriend实体对象
 */
@Data
@Component
public class GirlFriend {
    private String girlName;
    private Integer girlAge;
    private String girlHeight;
}

然后在applicationContext.xml配置文件中引入组件扫描器(它的作用就是扫描哪里使用了@Component注解):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- bean definitions here -->

    <!-- 组件自动扫描,指定注解扫描包路径 -->
    <!-- base-package放的是包名,有多个包名中间用逗号隔开 -->
    <context:component-scan base-package="com.thr.spring.pojo"/>

</beans>

注意:这里是要引入context的命名空间(idea会自动引入的):

image

测试代码:

/**
 * Spring测试代码
 *
 * @author tanghaorong
 */
public class SpringRunTest {
    public static void main(String[] args) {
        //1.初始化Spring容器,加载配置文件
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        //2.通过容器获取实例,getBean()方法中的参数是bean标签中的id
        User user = applicationContext.getBean("user", User.class);
        //3.调用实例中的属性
        System.out.println(user);
    }
}

运行测试代码,查看控制台打印结果:

image

3、扫描组件注解@ComponentScan

上面我们在XML配置文件中配置了组件扫描: <context:component-scan base-package="com.thr.spring.pojo"/>,它的作用是自动扫描指定包路径下的组件,即:扫描指定包路径下用@Component、@Controller、@Service和@Repository注解修饰的类,存在就将其装配为Bean放入IOC容器中,我们也可以不在applicationContext.xml配置文件中配置组件扫描,而是直接使用注解扫描组件,所以我们将xml配置文件的配置去掉:

image

那么接下来要怎么做呢?这个时候我们需要重新创建一个类并且在类上添加@ComponentScan注解即可,意思是告诉 Spring 容器怎么扫描,就是指定扫描哪个包,如下:

package com.thr.spring.config;

import org.springframework.context.annotation.ComponentScan;

/**
 * 组件扫描注解
 */
@ComponentScan(value = "com.thr.spring.*")
public class ScanBeanConfig {
}

代码目录结构:

image

上面的代码非常简单,但是需要注意的是:@ComponentScan注解如果不指定扫描哪个包的话,默认是扫描当前作用类的包路径 (这里是com.thr.spring.config),如果不在则扫描失败。然后就可以使用 Spring IOC 容器的实现类 AnnotationConfigApplicationContext 去生成Bean实例了,代码如下所示:

注意:AnnotationConfigApplicationContext 的参数必须是使用了@ComponentScan注解的那个类的Class对象,即将 ScanBeanConfig 作为参数传入。这样默认会扫描 ScanBeanConfig 类所在的包中的所有类,凡是类上有@Component、@Repository、@Service、@Controller任何一个注解的都会被注册到容器中。

/**
 * Spring测试代码
 *
 * @author tanghaorong
 */
public class SpringRunTest {
    public static void main(String[] args) {
        //1.初始化Spring容器,通过注解加载
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanBeanConfig.class);
        //2.通过容器获取实例
        User user = applicationContext.getBean("user", User.class);
        //3.调用实例中的属性
        System.out.println(user);
    }
}

这里可以看到使用了 AnnotationConfigApplicationContext 类去初始化 Spring IoC 容器,我们将@ComponentScan注解作用在 ScanBeanConfig 类上,所以它的配置项是 ScanBeanConfig类,这样 Spring IoC 就会根据注解的配置去解析对应的资源,来生成 IoC 容器了。但是这就有个弊端,一般来说我们的资源不会全部放在同一个包下,而对于 @ComponentScan 注解,它只是扫描所在包的 Java 类,这就意味着要进行全局扫描,这可怎么办呢?还好这个注解它有两个属性配置项basePackages和basePackageClasses:

  • value:等价于basePackages。
  • basePackages:表示扫描指定的包路径,可以是多个。
  • basePackageClasses:表示扫描这些类所在的包及其子包中的类,可以配置多个。

这样我们就不用关心配置类是否和被扫描资源在同一个包下的问题了。然后我们来重构之前写的 ScanBeanConfig类来验证上面两个属性配置项,首先我们将ScanBeanConfig类移到另一个包下如com.thr.config,代码如下。

package com.thr.spring.config;

import com.thr.spring.pojo.User;
import org.springframework.context.annotation.ComponentScan;

/**
 * 组件扫描注解(三选一)
 */
// @ComponentScan(basePackages = "com.thr.spring.*")
// @ComponentScan(value = "com.thr.spring.*")
@ComponentScan(basePackageClasses = User.class)
public class ScanBeanConfig {
}

如果有多个包或类,我们用大括号包起来然后在大括号里面用逗号隔开,简单举例:

@ComponentScan(basePackages = {"package1","package3","package4"})
@ComponentScan(basePackageClasses ={Class1.class,Class2.class,Class3.class})

这样 Spring 容器就能将一个类装配成Bean了。

4、使用@Autowired注解自动装配

上面提到使用@Value注解只能装配普通值,是不能装配对象的,所以下面我们来介绍使用注解自动装配对象,需要使用到@Autowired注解:

@Autowired:它默认是按byType进行匹配,可以用于修饰类成员变量(字段)、Setter 方法、构造函数,甚至普通方法,但是前提是方法必须有至少一个参数。

我们在实际的开发中基本都会使用注解来对对象属性完成自动装配,因为这样可以减少配置的复杂度,所以@Autowired非常的重要!

[1]、作用于类的成员变量(字段 | Field)

注意:在IDEA编辑器中使用@Autowired作用于字段 (Field) 的时,IDEA会给出一个提示:Field injection is not recommended(意思是不再推荐使用字段注入),但是习惯了作用于字段,所以不必管它,如果你感觉不爽的话可以按照如下操作隐藏这个提示:

setting-->Editor-->inspections-->Spring-->Spring Core-->Code-->Filed injection warning去掉右边的小勾勾,Apply-->OK即可。具体为啥不推荐可以去百度一下。

然后创建的User类:

@Data
@Component(value = "user")
public class User {
    @Value(value = "2020")
    private Integer userId;
    @Value(value = "小唐")
    private String userName;
    @Value(value = "20")
    private Integer userAge;
    @Value(value = "123456")
    private String userPwd;
    @Value(value = "中国北京")
    private String userAddress;

    //这里使用@Autowired注解自动注入
    @Autowired
    private GirlFriend girlFriend;
}

初始化用于注入的GirlFriend类:

@Data
@Component
public class GirlFriend {
    @Value("王美丽")
    private String girlName;
    @Value("18")
    private int girlAge;
    @Value("170")
    private String girlHeight;
}

测试代码如下:

/**
 * Spring测试代码
 *
 * @author tanghaorong
 */
@ComponentScan(value = "com.thr.spring.*")
public class SpringRunTest1 {
    public static void main(String[] args) {
        //1.初始化Spring容器,通过注解加载
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringRunTest1.class);
        //2.通过容器获取实例
        User user = applicationContext.getBean("user", User.class);
        //3.调用实例中的属性
        System.out.println(user);
    }
}

运行测试代码,查看控制台打印的结果:

image


[2]、作用于Setter方法

作用于Setter方法和作用于构造函数。这两种方式实现的效果和上面的效果是一模一样的。

image

[3] 作用于构造函数

注意:如果已经使用注解完成了初始化工作,那么则不能再创建该参数的构造方法了,比如我们使用了@Value注解初始化userName属性,那么则就不能再创建userName属性的构造方法了。

image



补充1:@Autowired注解中有个属性required,这个属性是一个boolean类型,为true(默认)表示注入bean的时候该bean必须存在,不然就会注入失败,但程序不报错 。为 false 表示注入bean的时候如果bean存在,就注入成功,如果没有就忽略跳过,启动时不会报错!但是不能直接使用,因为bean为NULL!

例如我将GirlFriend类的@Component注解给注释掉,并且把User类中的@Autowired注解的属性required设置为false。

image

image

通过运行的结果可以发现注入失败了,但是不会报错,只是返回为null。

image


补充2:@Autowired注解并不是完全按照byType进行匹配。而是默认先按byType进行匹配,如果发现找到多个bean,则又按照byName方式进行匹配,如果还有多个,则报出异常。动手能力强的可以自己去实践一遍,我自己是去验证过的。

5、@Autowired自动装配的歧义性

由于@Autowired注解是根据类型来自动装配的,所以肯定会存在有多个相同类型的bean,而Spring IOC容器却不知要选择哪一个的情况,此时就产生了歧义性,那么我们怎么来解决呢?Spring中给我们提供了@Primary和@Qualifier这两个注解。

  • @Primary:可以作用在类上,也可以配合@Bean作用在方法上,表示优先使用该注解标志的Bean。实际开发中不实用,所以就不介绍了。
  • @Qualifier:表示当容器中存在多个相同类型的Bean时,使用这个注解可以根据Bean的名字来选择注入哪个Bean,推荐使用这种方式。

image

6、与@Autowired类似的注解@Resource

@Resource 注解相当于@Autowired,它们两个都是用来实现依赖注入的。只是@AutoWried默认按byType自动注入,而@Resource默认按byName自动注入。而且@Resource只能处理setter注入(包括字段)。@Resource有两个重要属性,分别是name和type,其中name属性相当于@Qualifier,type相当于根据类型配置。Spring 将 name 属性解析为bean的名字,而type属性则被解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,如果使用type属性则使用byType的自动注入策略。如果都没有指定,则通过反射机制使用byName自动注入策略。表面上我们说@Resource默认按byName自动注入,其实如果按名称查找不到匹配的bean时,最后会按byType进行自动注入,@Resource依赖注入时查找bean的规则如下:

  • 如果不指定name属性,也不指定type属性,则自动按byName方式进行查找。如果没有找到符合的bean,则回退为一个原始类型进行进行查找,如果找到就注入。
  • 只是指定了@Resource注解的name属性,则只能按name后的名字去bean元素里查找有与之相等的name属性的bean,如果找不到则会抛出异常。
  • 只指定@Resource注解的type属性,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常。
  • 既指定了@Resource的name属性又指定了type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。

补充:但是我好像听说这个注解在Java11中被删除了,也不知道是不是真的,如果是真的还是慎用!然后我去查了一下JDK11的官方文档,确实JDK11将javax.annotation这个包移除了,如果想继续使用可以通过maven或者其他方式导入。

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>

如果这个依赖无法使用,可以去maven仓库自行查找。

7、@Autowired和@Resource的区别

相同点:

  • 二者均可以用来注入bean,都可以用在字段上或者方法上

不同点:

  • @Autowired是属于Spring框架的,是 Spring 提供的注解;而@Resource属于J2EE,是 JDK 提供的注解。
  • @Autowired默认按byType进行装配,可以结合@Qualifier使用按名称装配,如果发现找到多个bean,则又按照byName方式进行匹配,如果还有多个,则报出异常。
  • @Resource默认按byName进行装配,名称可以通过name属性进行指定,如果没有指定name属性,则默认采用字段名进行查找,当找不到与名称匹配的bean时才按byType进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。

小结:@Autowired是按照先按 byType 后按 byName 进行匹配,@Resources是按照先按 byName 后按 byType进行匹配。

8、@Named/@Inject(了解)

这两个注解的是JSR-330的一部分,而Spring 是支持JSR-330的。这些注解在使用上和Spring的注解一样,只是想要导入额外的相关jar包。如下:

<!-- https://mvnrepository.com/artifact/javax.inject/javax.inject -->
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>
  • @Named 用来替代@Component 声明一个Bean
  • @Inject 用来替代@Autowired来执行注入

实际上我们很少会使用这样的注解,只需知道有这个东西即可。

9、补充:@ComponentScan和@ComponentScans详解

这是@ComponentScan的官方介绍:

image

翻译一下的大致意思是:扫描组件注解可以与 @Configuration 类一起使用,并且该注解提供了与 Spring XML 的 <context:component-scan> 一样的功能,所以@ComponentScan是用于扫描注册Bean的一个注解,它会根据配置扫描路径下被@Component或者被其标注了的注解标注的类,比如@Controller、@Service、@Repository和@Configuration等。同时我们可以指定 basePackageClasses 或 basePackages(或其别名值)来定义要扫描的特定包,常用的方式是basePackages方式。 如果未定义特定的包,则会从声明此注解的类的包中进行扫描。

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

    /**
     * 扫描包路径
     * @ComponentScan(value = "spring.annotation.componentscan")
     */
    @AliasFor("basePackages")
    String[] value() default {};

    /**
     * 扫描包路径
     */
    @AliasFor("value")
    String[] basePackages() default {};

    /**
     * 指定扫描类所在的包
     * @ComponentScan(basePackageClasses = {User.class, UserService.class})
     */
    Class<?>[] basePackageClasses() default {};

    /**
     * 命名注册的Bean,可以自定义实现命名Bean,
     * 1、@ComponentScan(value = "spring.annotation.componentscan",nameGenerator = MyBeanNameGenerator.class)
     * MyBeanNameGenerator.class 需要实现 BeanNameGenerator 接口,所有实现BeanNameGenerator 接口的实现类都会被调用
     * 2、使用 AnnotationConfigApplicationContext 的 setBeanNameGenerator方法注入一个BeanNameGenerator
     * BeanNameGenerator beanNameGenerator = (definition,registry)-> String.valueOf(new Random().nextInt(1000));
     * AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
     * annotationConfigApplicationContext.setBeanNameGenerator(beanNameGenerator);
     * annotationConfigApplicationContext.register(MainConfig2.class);
     * annotationConfigApplicationContext.refresh();
     * 第一种方式只会重命名@ComponentScan扫描到的注解类
     * 第二种只有是初始化的注解类就会被重命名
     * 列如第一种方式不会重命名 @Configuration 注解的bean名称,而第二种就会重命名 @Configuration 注解的Bean名称
     */
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    /**
     * 用于解析@Scope注解,可通过 AnnotationConfigApplicationContext 的 setScopeMetadataResolver 方法重新设定处理类
     * ScopeMetadataResolver scopeMetadataResolver = definition -> new ScopeMetadata();  这里只是new了一个对象作为演示,没有做实际的逻辑操作
     * AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
     * annotationConfigApplicationContext.setScopeMetadataResolver(scopeMetadataResolver);

     * annotationConfigApplicationContext.register(MainConfig2.class);
     * annotationConfigApplicationContext.refresh();
     * 也可以通过@ComponentScan 的 scopeResolver 属性设置
     *@ComponentScan(value = "spring.annotation.componentscan",scopeResolver = MyAnnotationScopeMetadataResolver.class)
     */
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

    /**
     * 用来设置类的代理模式
     */
    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;


    /**
     * 扫描路径 如 resourcePattern = **/*.class" 使用 includeFilters 和 excludeFilters 会更灵活
     */
    String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

    /**
     * 指示是否应启用对带有{@code @Component},{@ code @Repository},
     * {@ code @Service}或{@code @Controller}注释的类的自动检测。
     */
    boolean useDefaultFilters() default true;

    /**
     * 对被扫描的包或类进行过滤,若符合条件,不论组件上是否有注解,Bean对象都将被创建
     * @ComponentScan(value = "spring.annotation.componentscan",includeFilters = {
     *     @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class}),
     *     @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {SchoolDao.class}),
     *     @ComponentScan.Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class}),
     *     @ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "spring.annotation..*"),
     *     @ComponentScan.Filter(type = FilterType.REGEX, pattern = "^[A-Za-z.]+Dao$")
     * },useDefaultFilters = false)
     * useDefaultFilters 必须设为 false
     */
    Filter[] includeFilters() default {};

    /**
     * 指定哪些类型不适合进行组件扫描。
     * 用法同 includeFilters 一样
     */
    Filter[] excludeFilters() default {};

    /**
     * 指定是否应注册扫描的Bean以进行延迟初始化。
     * @ComponentScan(value = "spring.annotation.componentscan",lazyInit = true)
     */
    boolean lazyInit() default false;


    /**
     * 用于 includeFilters 或 excludeFilters 的类型筛选器
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    @interface Filter {

        /**
         * 要使用的过滤器类型,默认为 ANNOTATION 注解类型
         * @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class, Service.class})
         */
        FilterType type() default FilterType.ANNOTATION;

        /**
         * 过滤器的参数,参数必须为class数组,单个参数可以不加大括号
         * 只能用于 ANNOTATION 、ASSIGNABLE_TYPE 、CUSTOM 这三个类型
         * @ComponentScan.Filter(type = FilterType.ANNOTATION, value = {Controller.class, Service.class})
         * @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {SchoolDao.class})
         * @ComponentScan.Filter(type = FilterType.CUSTOM, classes = {MyTypeFilter.class})
         */
        @AliasFor("classes")
        Class<?>[] value() default {};

        /**
         * 作用同上面的 value 相同
         * ANNOTATION 参数为注解类,如  Controller.class, Service.class, Repository.class
         * ASSIGNABLE_TYPE 参数为类,如 SchoolDao.class
         * CUSTOM  参数为实现 TypeFilter 接口的类 ,如 MyTypeFilter.class
         * MyTypeFilter 同时还能实现 EnvironmentAware,BeanFactoryAware,BeanClassLoaderAware,ResourceLoaderAware 这四个接口
         * EnvironmentAware
         * 此方法用来接收 Environment 数据 ,主要为程序的运行环境,Environment 接口继承自 PropertyResolver 接口,详细内容在下方
         * @Override
         * public void setEnvironment(Environment environment) {
         *    String property = environment.getProperty("os.name");
         * }
         *
         * BeanFactoryAware
         * BeanFactory Bean容器的根接口,用于操作容器,如获取bean的别名、类型、实例、是否单例的数据
         * @Override
         * public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
         *     Object bean = beanFactory.getBean("BeanName")
         * }
         *
         * BeanClassLoaderAware
         * ClassLoader 是类加载器,在此方法里只能获取资源和设置加载器状态
         * @Override
         * public void setBeanClassLoader(ClassLoader classLoader) {
         *     ClassLoader parent = classLoader.getParent();
         * }
         *
         * ResourceLoaderAware
         * ResourceLoader 用于获取类加载器和根据路径获取资源
         * public void setResourceLoader(ResourceLoader resourceLoader) {
         *     ClassLoader classLoader = resourceLoader.getClassLoader();
         * }
         */
        @AliasFor("value")
        Class<?>[] classes() default {};

        /**
         * 这个参数是 classes 或 value 的替代参数,主要用于 ASPECTJ 类型和  REGEX 类型
         * ASPECTJ  为 ASPECTJ 表达式
         * @ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "spring.annotation..*")
         * REGEX  参数为 正则表达式
         * @ComponentScan.Filter(type = FilterType.REGEX, pattern = "^[A-Za-z.]+Dao$")
         */
        String[] pattern() default {};
    }
}

从源码的定义上可以看出此注解可以用在任何类型上面,不过我们通常将其用在类上面。常用参数介绍:

  • value:指定需要扫描的包,如:com.thr.spring.*
  • basePackages:作用同value;value和basePackages不能同时存在设置,可二选一,表示要扫描的路径,如果为空,解析的时候会解析被@ComponentScan标注类的包路径
  • basePackageClasses:指定一些类,Spring容器会扫描这些类所在的包及其子包中的类,与basePackages互斥
  • nameGenerator:自定义bean名称生成器,在解析注册BeanDefinition的时候用到
  • scopeResolver:类定义上的@Scope注解解析器,如果没有该注解默认单例
  • scopedProxy:扫描到@Component组件是是否生成代理以及生成代理方式
  • resourcePattern:扫描路径时规则,默认是:**/*.class,即会扫描指定包中所有的class文件
  • useDefaultFilters:对扫描的类是否启用默认过滤器,默认为true,扫描@Component标注的类以及衍生注解标注的类,如果为false则不扫描,需要自己指定includeFilters
  • includeFilters:自定义包含过滤器,用来配置被扫描出来的那些类会被作为组件注册到容器中,如果@Component扫描不到或者不能满足,则可以使用自定义扫描过滤器
  • excludeFilters:自定义排除过滤器,和includeFilters作用刚好相反,用来对扫描的类进行排除的,被排除的类不会被注册到容器中
  • lazyInit:表示扫描注册BeanDefinition后是否延迟初始化,默认false

TIPS:@ComponentScan工作的过程:Spring会扫描指定的包,且会递归下面子包,得到一批类的数组然后这些类会经过上面的各种过滤器,最后剩下的类会被注册到容器中所以玩这个注解,主要关注2个问题:第一个:需要扫描哪些包?通过 value、backPackages、basePackageClasses 这3个参数来控制;第二:过滤器有哪些?通过 useDefaultFilters、includeFilters、excludeFilters 这3个参数来控制过滤器,这两个问题搞清楚了,就可以确定哪些类会被注册到容器中。默认情况下,任何参数都不设置的情况下,此时,会将@ComponentScan修饰的类所在的包作为扫描包;默认情况下useDefaultFilters为true,这个为true的时候,Spring容器内部会使用默认过滤器,规则是:凡是类上有 @Repository、@Service、@Controller、@Component 这几个注解中的任何一个的,那么这个类就会被作为Bean注册到Spring容器中,所以默认情况下,只需在类上加上这几个注解中的任何一个,这些类就会自动交给spring容器来管理了。

而我们平时用的最多的就是basePackages,以及做一些定制化扫描时会用到includeFilters和excludeFilters,所以下面来看下。

9.1、value、basePackages和basePackageClasses的使用

  • value:指定需要扫描的包,如:com.thr.spring.*
  • basePackages:作用同value;value和basePackages不能同时存在设置,可二选一,表示要扫描的路径,如果为空,解析的时候会解析被@ComponentScan标注类的包路径
  • basePackageClasses:指定一些类,Spring容器会扫描这些类所在的包及其子包中的类,与basePackages互斥

这些都是常规用法比较简单,所以不多说了,直接复用一下上面的代码ScanBeanConfig类,代码如下。

package com.thr.spring.config;

import com.thr.spring.pojo.User;
import org.springframework.context.annotation.ComponentScan;

/**
 * 组件扫描注解(三选一)
 */
// @ComponentScan(basePackages = "com.thr.spring.*")
// @ComponentScan(value = "com.thr.spring.*")
@ComponentScan(basePackageClasses = User.class)
public class ScanBeanConfig {
}

如果有多个包或类,我们用大括号包起来然后在大括号里面用逗号隔开,简单举例:

@ComponentScan(basePackages = {"package1","package3","package4"})
@ComponentScan(basePackageClasses ={Class1.class,Class2.class,Class3.class})

9.2、includeFilters的使用

先来看一下includeFilters这个参数的定义:

Filter[] includeFilters() default {};

它是一个 Filter 类型的数组,多个Filter之间为或者关系,即满足任意一个就可以了,看一下 Filter 的代码:

@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来自正则表达式的值

案例1:扫描包含注解的类

我们自定义一个注解,让标注有这些注解的类自动注册到容器中,代码实现如下:首先在 com.thr.spring.annotation 包中自定义一个注解

package com.thr.spring.annotation;

import java.lang.annotation.*;

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyBean {
}

然后创建一个类,使用上面创建的这个注解进行标注。

package com.thr.spring.service;

import com.thr.spring.annotation.MyBean;

@MyBean
public class Service1 {
}

再来一个类,处理使用Spring中的 @Compontent 标注。

package com.thr.spring.service;

import org.springframework.stereotype.Component;

@Component
public class Service2 {
}

最后再来一个扫描配置类,使用@CompontentScan标注。

package com.thr.spring.config;

import com.thr.spring.annotation.MyBean;
import com.thr.spring.pojo.User;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;

@ComponentScan(value = "com.thr.spring.*",
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyBean.class)
        })
public class ScanBeanConfig1 {
}

上面扫描了com.thr.spring包及其所有子包的类,并且额外指定了Filter的type为注解的类型,所以只要类上面有 @MyBean 注解的,都会被作为bean注册到容器中。

测试代码如下:

/**
 * Spring测试代码
 *
 * @author tanghaorong
 */
public class SpringRunTest1 {
    public static void main(String[] args) {
        //1.初始化Spring容器,通过注解加载
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanBeanConfig1.class);
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            System.out.println(beanName + "->" + applicationContext.getBean(beanName));
        }
    }
}

运行输出结果:

image

从运行结果可以发现:Service1上标注了 @MyBean 注解,被注册到容器了,但是 Service2 上没有标注 @MyBean 啊,怎么也被注册到容器了?并且可以发现User、Girlfriend、ScanBeanConfig1也都注册到容器了。

原因:Service2、User、Girlfriend上标注了 @Compontent 注解,而@CompontentScan注解中的 useDefaultFilters默认是 true ,表示也会启用默认的过滤器,而默认的过滤器会将标注有 @Component、@Repository、@Service、@Controller 这几个注解的类也注册到容器中。如果我们只想将标注有 @MyBean 注解的bean注册到容器,需要将默认过滤器关闭,即:useDefaultFilters=false,可以修改一下ScanBeanConfig1的代码如下:

@ComponentScan(value = "com.thr.spring.*",
        useDefaultFilters = false, //不启用默认过滤器
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyBean.class)
        })
public class ScanBeanConfig1 {
}

然后再次运行的到的输出结果为:

image


案例2:扫描包含指定类型的类

下面定义一个接口,让Spring来进行扫描,类型满足IService的都将其注册到容器中。

public interface IService {
}

创建两个上面IService接口实现类:

public class Service1 implements IService {
}
public class Service2 implements IService {
}

随后在来一个@CompontentScan标注的配置类,扫描配置类的意思是:被扫描的类满足 IService.class.isAssignableFrom(被扫描的类) 条件的都会被注册到Spring容器中。

@ComponentScan(value = "com.thr.spring.*",
        useDefaultFilters = false, //不启用默认过滤器
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = IService.class)
        })
public class ScanBeanConfig2 {
}

测试代码:

/**
 * Spring测试代码
 *
 * @author tanghaorong
 */
public class SpringRunTest2 {
    public static void main(String[] args) {
        //1.初始化Spring容器,通过注解加载
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanBeanConfig2.class);
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            System.out.println(beanName + "->" + applicationContext.getBean(beanName));
        }
    }
}

运行输出:

image


案例3:自定义Filter

首先来看看与自定义Filter有关的参数和相关的类,使用自定义过滤器的步骤:

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

来看一下 TypeFilter 这个接口的定义

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

可以发现它是一个函数式接口,包含一个match方法,方法返回boolean类型,有2个参数,都是接口类型的,下面介绍一下这2个接口。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;
}

自定义Filter案例

我们来个自定义的Filter,判断被扫描的类如果是 IService 接口类型的,就让其注册到容器中。

代码实现:在包com.thr.spring.filter下创建一个自定义的TypeFilter类

public class MyFilter implements TypeFilter {

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory
            metadataReaderFactory) throws IOException {
        Class<?> curClass = null;
        try {
            //当前被扫描的类
            curClass = Class.forName(metadataReader.getClassMetadata().getClassName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        //判断curClass是否是IService类型
        assert curClass != null;
        return IService.class.isAssignableFrom(curClass);
    }
}

随后在来一个@CompontentScan标注的配置类,注意:type为FilterType.CUSTOM,表示Filter是用户自定义的,classes为自定义的过滤器

@ComponentScan(value = "com.thr.spring.*",
        useDefaultFilters = false, //不启用默认过滤器
        includeFilters = {
                @ComponentScan.Filter(type = FilterType.CUSTOM, classes = MyFilter.class)
        })
public class ScanBeanConfig3 {
}

测试代码:

/**
 * Spring测试代码
 *
 * @author tanghaorong
 */
public class SpringRunTest3 {
    public static void main(String[] args) {
        //1.初始化Spring容器,通过注解加载
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ScanBeanConfig3.class);
        for (String beanName : applicationContext.getBeanDefinitionNames()) {
            System.out.println(beanName + "->" + applicationContext.getBean(beanName));
        }
    }
}

运行输出:

image

9.3、excludeFilters的使用

excludeFilters参数用于配置排除的过滤器,满足这些过滤器的类不会被注册到容器中,用法上面和includeFilters用一样,所以就不演示了,可以参考includeFilters的用法。


9.4、@ComponentScans的使用

@ComponentScans注解可以一次声明多个 @ComponentScan 注释。也可以与 Java 8 对可重复注释的支持结合使用,在该方法中,可以简单地在同一方法上多次声明 @ComponentScan,从而隐式生成此容器注释。

我们先看 @ComponentScan 注解的源码,如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

}

上面 @ComponentScan 注解源码上面使用了 @Repeatable 注解,表示该注解可以被 @ComponentScans 作为数组使用。

再来看看 @ComponentScans 注解源码,如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ComponentScans {
    ComponentScan[] value();
}

该注解的 value() 属性是一个 @ComponentScan 注解数组。


@ComponentScans案例

下面实例在 @ComponentScans 注解中声明了两个 @ComponentScan 注解,如下:

@ComponentScans({
        @ComponentScan(basePackages = {
                "com.thr.spring.controller1"
        }, includeFilters = {
                // 仅仅使用了 @RestController 注解声明的类
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {Controller.class})
        }, useDefaultFilters = false),
        @ComponentScan(basePackages = {
                "com.thr.spring.controller2"
        }, excludeFilters = {
                // 过滤使用了 @MyAnnotation 注解的类
                @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {MyBean.class})
        })
})
public class MyConfig {

}

上面实例中,第一个 @ComponentScan 注解将扫描 com.thr.spring.controller1 包及子包下面声明了 @Controller 注解的类;第二个 @ComponentScan 注解将过滤 com.thr.spring.controller2 包及子包下声明了 @MyBean 注解的类。

posted @ 2020-12-19 20:08  唐浩荣  阅读(2324)  评论(1编辑  收藏  举报