Spring注解驱动开发(一):使用@ComponentScan自定义扫描规则和组件注入规则

发布于:2023-01-20 ⋅ 阅读:(13) ⋅ 点赞:(0) ⋅ 评论:(0)

1. 背景知识

在使用 Spring 进行项目开发时,我们会向 Spring Ioc 容器中批量注入 xxxDao,xxxService,xxxController 之类的对象
为了实现批量注入功能,我们会在对应的业务类上面标注 @Repository,@Service,@Controller 注解,会在通用组件上面标注 @Componet 注解,之后再配置一定的包扫描规则来进行组件注入

在未进行正文之前,我先带你了解一下@Component注解以及对应的派生注解

/**
 * .......
 * @author Mark Fisher
 * @since 2.5
 * @see Repository
 * @see Service
 * @see Controller
 * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {

	/** 设置组件名称 */
	String value() default "";

}

⭐️Tip:这里我也分享你几个看源码的小技巧:

  1. 多看注释:很多优秀的开源框架都配有及其简洁凝练的注释,一方面它概括了类,方法以及字段所表示的含义,另一方面,它对提升你的英文阅读能力和注释编写能力有很大的帮助
  2. 看类的发布版本和关联类信息:类的发布版本可以很好的让你了解整个框架的发展过程,让你从整体对框架形成感性认知;相关类信息会有助于你对此类的功能的理解更加清晰

言归正传,O(∩_∩)O哈哈~,这个注解的作用就是当开启包扫描的时候,把该注解标注的类作为Ioc容器注入的候选对象(这意味着即使开启包扫描功能,该注解标注的类未必会被注入容器)
@Component 注解有一个属性值,传入的会作为标注类在Ioc容器中的 “标识”(在之后的Ioc源码分析文章我会带你理解它的妙用)

​❓ 从上面看来,有@Componet 注解就够了,那为什么要设计 @Repository,@Service,@Controller 这些注解呢?
在我看来,这三个注解主要用于业务类使用,它们自身带有业务意义,这里我们拿@Controller 注解为例,看看它的源码


/**
 * Indicates that an annotated class is a "Controller" (e.g. a web controller).
 *
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 2.5
 * @see Component
 * @see org.springframework.web.bind.annotation.RequestMapping
 * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {

	@AliasFor(annotation = Component.class)
	String value() default "";

}

🐳 根据我前面说的看源码的技巧,我们分析一下这个注解

  1. 从注释上看到了,@Controller 这个注解意味着它所标注的类是一个 “Controller”,也就是一个控制器(如果你不明白控制器是什么意思,你可以把它理解为 负责接收用户发起的请求,并把处理后的结果返回给用户的容器(建议你了解一下Spring MVC) 就好了)
  2. 从 @Controller 的声明中可以看出它是 @Componet 注解的派生类,这意味着开启包扫描时,标注了@Controller 的类也会被作为候选对象
  3. @Controller的 属性值也只有一个,并且这个属性值是继承自 @Component
    ⭐️ Tips: @AliasFor注解有两个功能 : 让同一注解中的属性互为别名或者继承父类的属性

好了,经过我上面的介绍,你应该对这四个注解有了基本的了解,接下来开始我们的正文吧!

2. 预先准备代码

为了方便内容的介绍,在这里我先给出一些测试代码,你可以将这些代码粘贴到自己的项目中进行测试(注意,这里的测试代码可能不符合实际业务的编码规范,仅供测试使用,请见谅)
实体类(domain) :

/**
 * @author NanCheng
 * @version 1.0
 * @date 2022/8/12 19:03
 */
public class Person {
    private Integer id;
    private String name;
    private String sex;

    public Person() {
    }

    public Person(Integer id, String name, String sex) {
        this.id = id;
        this.name = name;
        this.sex = sex;
    }
	// ...... 省略get,set 方法,请自行补全
    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                '}';
    }
}

数据操作类(dao):

@Repository
public class PersonDao {
}

业务逻辑类(service):

@Service
public class PersonService {
}

控制器类(controller):

@Controller
public class PersonController {
}

好啦,我们的基本业务类写好了,下面开始使用包扫描吧

3. 使用XML配置包扫描

包扫描最经典的配置方式就是在 Spring 配置文件中配置包的扫描。
当我们要开启包扫描时,需要在Spring的XML配置文件中的beans节点中引入context 标签,如下所示

<?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
       http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.zhang.blog"/>

    <bean id="person" class="org.zhang.blog.ioc.domain.Person">
        <property name="id" value="1" />
        <property name="name" value="jack"/>
    </bean>
</beans>

⭐️ 如果你想引入更多 XML 模式,你可以参考下面的官网链接: Spring XML Schema

注意这里我们扫描的包是: org.zhang.blog,我的包结构如下图所示:
在这里插入图片描述
接下来我们编写测试代码进行测试吧:

/**
 * @author NanCheng
 * @version 1.0
 * @date 2022/8/12 20:00
 */
public class IcoMain {
    public static void main(String[] args) {
    	// 读取对应XML文件的配置
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:META-INF/dependency-inject.xml");
        // 获取容器中 Beans 的名字
        String[] definitionNames = applicationContext.getBeanDefinitionNames();
        for (String name : definitionNames) {
            System.out.println(name);
        }
    }
}

在这里插入图片描述
从图中可以看出我们定义的业务组件都被成功扫描,并注入Ioc容器中
❓ 我们并未指定 @Componet 的 value 属性,那么这些组件在容器中是怎样命名的呢?
从上图我们可以看出,这些组件默认命名规则是将类名首字母小写

3. 使用 @ComponentScan 注解配置包扫描

上面,我们通过在 Srping XML 文件中配置包扫描实现了组件批量扫描的功能,接下来我们将利用 @ComponentScan 实现包扫描功能
我们先写一个类并在上面标注上 @ComponentScan 注解

/**
 * @author NanCheng
 * @version 1.0
 * @date 2022/7/23 6:15
 * <p>
 * 配置类 + 包扫描
 */
@ComponentScan(value = "org.zhang.blog")
public class CustomerConfig {

    /**
     * 未指定Bean名称,利用方法名作为BeanName
     * @return
     */
    @Bean
    public Person person() {
        // 构造器注入
        return new Person(1, "jack", "sex");
    }
}

这里我们定义了配置类CustomerConfig ,并且在它上面标注了@ComponentScan 注解,我们下编写测试类测试一下我们的注解是否生效了,此处只需要将原来的XML配置上下文环境转换为注解配置上下文环境即可,其它代码并不需要改变:

/**
 * @author NanCheng
 * @version 1.0
 * @date 2022/8/12 20:00
 */
public class IcoMain {
    public static void main(String[] args) {
        // 将此处注释掉
//        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:META-INF/dependency-inject.xml");

        // 换成注解配置上下文环境
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CustomerConfig.class);

        // 获取容器中所有 Bean 的名字
        String[] definitionNames = context.getBeanDefinitionNames();
        for (String name : definitionNames) {
            System.out.println(name);
        }
    }
}

在这里插入图片描述
从结果我们可以看出,对应的业务组件也被扫描放入到了 Ioc 容器中

其实从上面可以看出,两种配置都很简单,你可以根据自己的喜好选择,但我要提示你的就是:以后你大概率会使用Spring Boot 进行业务开发,在那里你几乎不会写Spring 的XML文件,所以注解扫描还是必须要掌握的

4. @ComponentScan 注解介绍

上面我们学会了如何使用 @ComponentScan 进行包扫描,但是它的功能不止于此,你还可以在此之上进行一些定制化操作,我们先从这个注解的源码开始讲解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
	// basePackages 的一个别名
	@AliasFor("basePackages")
	String[] value() default {};
	
	@AliasFor("value")
	String[] basePackages() default {};
	
	Class<?>[] basePackageClasses() default {};

	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

	ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

	String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

	boolean useDefaultFilters() default true;

	Filter[] includeFilters() default {};

	Filter[] excludeFilters() default {};
	
	boolean lazyInit() default false;

	@Retention(RetentionPolicy.RUNTIME)
	@Target({})
	@interface Filter {
		// 过滤类型
		FilterType type() default FilterType.ANNOTATION;
		// classes别名
		@AliasFor("classes")
		Class<?>[] value() default {};

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

		String[] pattern() default {};

	}
}

这次源码中有很多属性,你依然可以借助注释了解大概内容,但是我在这里会给你介绍几个重要的属性,其余的属性你可以自行探索

4.1 basePackages

basePackages,这个属性就是设置我们所扫描的包名的集合,与我们之前设置的value 属性互为别名,我们可以看以下的设置,结合我的Demo结构理解以下它的作用:
这里我设置了扫描的包的字符串(这里是一个字符串数组)

// 只需要将上面案例的此处做修改即可
@ComponentScan(value = {"org.zhang.blog.ioc.dao","org.zhang.blog.ioc.controller"})
public class CustomerConfig {

    /**
     * 未指定Bean名称,利用方法名作为BeanName
     *
     * @return
     */
    @Bean
    public Person person() {
        // 构造器注入
        return new Person(1, "jack", "sex");
    }
}

最终扫描到的类信息如下图所示:
在这里插入图片描述
从结果可以看出, 由于我们这次扫描的包路径不包括 org,zhang.blog.ioc.service,原来的personService 并未被扫描
好啦,从上面的案例我相信你能理解 basePackages 属性的意义和用法了

4.2 basePackageClasses

这个属性在实际项目中用的很少,是因为它借助的一个实际意义的类,对该类所在的包进行扫描,说起来可能很难懂,我通过代码让你理解它存在的意义:
在文章开始我定义了一个标注 @Repository 注解的 UserDao ,这里我再定义一个没有任何意义的类 UselessDao,具体代码如下所示:

public class UselessDao {
}

它只是一个简单的类,并没有标注任何组件扫描相关的注解,只是和 UserDao 放在同一个包下面,现在改造我们的@ComponetScan 注解的属性值为:

//@ComponentScan(value = {"org.zhang.blog.ioc.dao","org.zhang.blog.ioc.controller"})
// 改成如下模样
@ComponentScan(basePackageClasses = {UselessDao.class})
public class CustomerConfig {

    /**
     * 未指定Bean名称,利用方法名作为BeanName
     *
     * @return
     */
    @Bean
    public Person person() {
        // 构造器注入
        return new Person(1, "jack", "sex");
    }
}

让我们看一下最终哪些类被扫描到了:
在这里插入图片描述
这次我们发现只扫描到了 personDao ,这下你能明白 basePackageClasses 的意义了吗
⭐️ 但是我个人感觉这个属性有时候显得很鸡肋,小伙伴们你们觉得呢(O(∩_∩)O哈哈~)

还有一个重要的点来自@ComponetScan的注释: 当basePackages(value)和basePackageClasses都设置为空时(未指定状态),默认扫描规则是当前注解标注的类所在的包,这个规则在Spring Boot 主启动类上有很明显的体现,使用Spring Boot 的小伙伴们注意到了吗

4.3 useDefaultFilters + includeFilters

我们可以使用 @ComponentScan 注解中的 includeFilters属性来指定进行包扫描时,只包含哪些类,但是使用这种规则时需要将 useDefaultFilters 设置为false

Filter[] includeFilters() default {};

这个属性值是一个Filter 数组,那么Filter 是什么呢?

	@Retention(RetentionPolicy.RUNTIME)
	@Target({})
	@interface Filter {

		/**
		 * The type of filter to use.
		 * <p>Default is {@link FilterType#ANNOTATION}.
		 * @see #classes
		 * @see #pattern
		 */
		FilterType type() default FilterType.ANNOTATION;

		/**
		 * Alias for {@link #classes}.
		 * @see #classes
		 */
		@AliasFor("classes")
		Class<?>[] value() default {};
		
		@AliasFor("value")
		Class<?>[] classes() default {};
		
		String[] pattern() default {};

	}

Filter 其实是 @ComponetScan 的一个内部类,利用它可以根据指定的规则筛选出想扫描的类,其中的 type 属性决定了过滤规则,value 决定了对哪些类进行过滤
我们先看一下 type 属性,其对应的类型是 FilterType,我们进入其内部看一眼

public enum FilterType {

	/**
	 * Filter candidates marked with a given annotation.
	 * @see org.springframework.core.type.filter.AnnotationTypeFilter
	 */
	ANNOTATION,

	/**
	 * Filter candidates assignable to a given type.
	 * @see org.springframework.core.type.filter.AssignableTypeFilter
	 */
	ASSIGNABLE_TYPE,

	/**
	 * Filter candidates matching a given AspectJ type pattern expression.
	 * @see org.springframework.core.type.filter.AspectJTypeFilter
	 */
	ASPECTJ,

	/**
	 * Filter candidates matching a given regex pattern.
	 * @see org.springframework.core.type.filter.RegexPatternTypeFilter
	 */
	REGEX,

	/** Filter candidates using a given custom
	 * {@link org.springframework.core.type.filter.TypeFilter} implementation.
	 */
	CUSTOM
}

从上面的代码我们可以看出过滤规则还是很多的,你甚至可以通过设置FIlter 的type 为 FilterType.CUSTOM,来自定义规则。

这里我就不展开介绍了,有兴趣的小伙伴可以深入研究,这里我展示一下如何通过注解进行扫描类的过滤

@ComponentScan(value = {"org.zhang.blog"},
	useDefaultFilters = false, includeFilters = 
	{@ComponentScan.Filter(type = FilterType.ANNOTATION,
	classes = {Service.class, Controller.class})})
public class CustomerConfig {

    /**
     * 未指定Bean名称,利用方法名作为BeanName
     *
     * @return
     */
    @Bean
    public Person person() {
        // 构造器注入
        return new Person(1, "jack", "sex");
    }
}

你可以从代码上看到我所配置的过滤规则,我利用注解过滤,只扫描标注了 Service 和Controller 注解的类,最终扫描结果如下图所示:
在这里插入图片描述
到这里,小伙伴们应该了解这个属性的用法了吧

4.4 excludeFilters

这是最后一个重要的属性,它同样是一个FIlter 数组,可以用来根据指定规则排除一些类型

Filter[] excludeFilters() default {};

我们还是以注解类型进行演示

@ComponentScan(value = {"org.zhang.blog"},
 excludeFilters = {@ComponentScan.Filter(type =
 FilterType.ANNOTATION,
 classes = {Service.class, Controller.class})})
public class CustomerConfig {

    /**
     * 未指定Bean名称,利用方法名作为BeanName
     *
     * @return
     */
    @Bean
    public Person person() {
        // 构造器注入
        return new Person(1, "jack", "sex");
    }
}

你可以从代码上看到我所配置的排除过滤规则,我利用注解过滤,排除了标注 Service 和Controller 注解的类,最终扫描结果如下图所示:
在这里插入图片描述
你可以发现 PersonService 和 PersonController 并未被扫描到
这次,你明白这个属性的用法了吧。

最后,我建议你在项目里要注意尽量避免同时使用 excludeFiltersincludeFilters,以免出现歧义

4.5 可重复注解

如果你对 Java 8 的新特性了解的话,你一定听说过 @Repeatable 注解,你再回头看一眼 @ComponetScan 注解,你会发现它被标注为了可重复注解(收集该注解的类为 @ComponentScans),那么你完全可以在类上标注一个@ComponentScans,在里面配置多个 @ComponentScan

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

我这里只是给你做一个演示,你可以自己编写代码尝试一下


@ComponentScans(value = 
{@ComponentScan(basePackageClasses =
 {UselessDao.class}),
@ComponentScan(value =
 {"org.zhang.blog.ioc.dao","org.zhang.blog.ioc.controller"}})
public class CustomerConfig {

    /**
     * 未指定Bean名称,利用方法名作为BeanName
     *
     * @return
     */
    @Bean
    public Person person() {
        // 构造器注入
        return new Person(1, "jack", "sex");
    }
}

5. 总结

这篇文章,我向你传输了以下知识点

  1. 介绍了@Componet 注解以及它的派生注解的基本用法
  2. 介绍了我在看源码时的几个小技巧
  3. 介绍了包扫描的两种方式
  4. 介绍了@ComponetScan 注解的属性及用法
  5. 提了一下 @Repeatable 注解的使用,这种用法在很多地方有体现,希望你有时间多了解一下

最后,我希望你看完本篇文章后,能够使用@ComponentScan 注解进行定制化的扫描和组件批量注入功能,希望你能有所进步,也希望你能给我的文章点个赞,原创不易