在使用 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:这里我也分享你几个看源码的小技巧:
言归正传,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 "";
}
根据我前面说的看源码的技巧,我们分析一下这个注解
好了,经过我上面的介绍,你应该对这四个注解有了基本的了解,接下来开始我们的正文吧!
为了方便内容的介绍,在这里我先给出一些测试代码,你可以将这些代码粘贴到自己的项目中进行测试(注意,这里的测试代码可能不符合实际业务的编码规范,仅供测试使用,请见谅)
实体类(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 {
}
好啦,我们的基本业务类写好了,下面开始使用包扫描吧
包扫描最经典的配置方式就是在 Spring 配置文件中配置包的扫描。
当我们要开启包扫描时,需要在Spring的XML配置文件中的beans节点中引入context
标签,如下所示
<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 属性,那么这些组件在容器中是怎样命名的呢?
从上图我们可以看出,这些组件默认命名规则是将类名首字母小写
上面,我们通过在 Srping XML 文件中配置包扫描实现了组件批量扫描的功能,接下来我们将利用 @ComponentScan 实现包扫描功能
我们先写一个类并在上面标注上 @ComponentScan 注解
/**
* @author NanCheng
* @version 1.0
* @date 2022/7/23 6:15
*
* 配置类 + 包扫描
*/
@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文件,所以注解扫描还是必须要掌握的
上面我们学会了如何使用 @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 {};
}
}
这次源码中有很多属性,你依然可以借助注释了解大概内容,但是我在这里会给你介绍几个重要的属性,其余的属性你可以自行探索
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 属性的意义和用法了
这个属性在实际项目中用的很少,是因为它借助的一个实际意义的类,对该类所在的包进行扫描,说起来可能很难懂,我通过代码让你理解它存在的意义:
在文章开始我定义了一个标注 @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 的小伙伴们注意到了吗
我们可以使用 @ComponentScan 注解中的 includeFilters
属性来指定进行包扫描时,只包含哪些类,但是使用这种规则时需要将 useDefaultFilters
设置为false
Filter[] includeFilters() default {};
这个属性值是一个Filter 数组,那么Filter 是什么呢?
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface Filter {
/**
* The type of filter to use.
* 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 注解的类,最终扫描结果如下图所示:
到这里,小伙伴们应该了解这个属性的用法了吧
这是最后一个重要的属性,它同样是一个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 并未被扫描到
这次,你明白这个属性的用法了吧。
最后,我建议你在项目里要注意尽量避免同时使用 excludeFilters 和 includeFilters,以免出现歧义
如果你对 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");
}
}
这篇文章,我向你传输了以下知识点
最后,我希望你看完本篇文章后,能够使用@ComponentScan 注解进行定制化的扫描和组件批量注入功能,希望你能有所进步,也希望你能给我的文章点个赞,原创不易!