Spring IOC进阶-组件扫描高级

从 0 开始深入学习 Spring 专栏目录总览

前面在第 7 章中,咱介绍了最基础的注解驱动 IOC ,以及组件扫描的最基本使用方式。然而,组件扫描本身可是大有文章的,深入了解组件扫描中的各个细节,对了解这部分内容很有帮助。本章咱就来学习组件扫描中的一些高级使用和底层实现原理。

1. 包扫描的路径【掌握】

第 7 章中咱有讲过,@ComponentScan 注解可以指定包扫描的路径(而且还可以声明不止一个),它的写法是使用 @ComponentScan 的 value / basePackages 属性:

@Configuration
@ComponentScan("com.linkedbear.spring.annotation.e_basepackageclass.bean")
public class BasePackageClassConfiguration {
    
}

这种方式是最常用的,也是最推荐使用的。除此之外,还有一种声明方式,它使用的是类的 Class 字节码:

@Repeatable(ComponentScans.class)
public @interface ComponentScan {
	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

    // 注意这个
	Class<?>[] basePackageClasses() default {};

它的这个 basePackageClasses 属性,可以传入一组 Class 进去,它代表的意思,是扫描传入的这些 Class 所在包及子包下的所有组件

下面用一个简单的例子演示一下效果。

本小节源码位置:com.linkedbear.spring.annotation.e_basepackageclass

1.1 声明几个组件类+配置类

继续沿用之前注解驱动 IOC 的包吧,咱这里创建一个 e_basepackageclass 包,声明几个组件和配置类:

Spring IOC进阶-组件扫描高级

1.2 标注配置类的包扫描规则

配置类中,声明包扫描配置,咱先拿 DemoService 传进去:

@Configuration
@ComponentScan(basePackageClasses = DemoService.class)
public class BasePackageClassConfiguration {
    
}

1.3 测试运行

编写启动类,驱动 IOC 容器,并打印容器中所有的 Bean 的名称:

public class BasePackageClassApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BasePackageClassConfiguration.class);
        String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
        Stream.of(beanDefinitionNames).forEach(System.out::println);
    }
}

运行 main 方法,发现控制台中只打印了 DemoService 与 DemoDao :

org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
basePackageClassConfiguration
demoDao
demoService

说明它确实以 DemoService 所在的包为基准扫描了,不过没有扫描到 DemoComponent 。

1.4 加入DemoComponent

在 @ComponentScan 中,再加入 DemoComponent 的字节码:

@Configuration
@ComponentScan(basePackageClasses = {DemoService.class, DemoComponent.class})
public class BasePackageClassConfiguration {
    
}

重新运行 main 方法,控制台中多了 DemoComponent 与 InnerComponent 的打印,由此体现出 basePackageClasses 的作用。

2. 包扫描的过滤【掌握】

在实际开发的场景中,我们用包扫描拿到的组件不一定全部都需要,也或者只有一部分需要,这个时候就需要用到包扫描的过滤了。

如果小伙伴之前有用 SpringWebMvc 的 xml 配置开发 Web 应用的话,应该印象蛮深刻吧!spring-mvc.xml 中配置只能扫描 @Controller 注解,applicationContext.xml 中又要设置不扫描 @Controller 注解,这就是扫描过滤的规则设置。

本小节源码位置:com.linkedbear.spring.annotation.f_typefilter

下面咱先来创建一些组件类。

2.1 声明好多组件类+配置类

这次声明的更多了,可想而知接下来得有多少种过滤的规则哦(滑稽)

Spring IOC进阶-组件扫描高级

简单说一下这些组件的编写。

  • @Animal 是一个普通的注解,它可以标注在类上
    • Cat 、Dog 、Pikachu 是三个最简单的类,其中 Cat 和 Dog 上除了标注 @Component 注解外,还标注 @Animal
  • color 包下的 Color 是一个父类
    • 下面的红黄绿三个类均标注 @Component ,不过只有 Red 和 Yellow 继承 Color
  • bean 包的 DemoService 与 DemoDao 均是普通的类,且都没有标注任何注解

下面咱开始介绍过滤的几种方式。

2.2 按注解过滤包含

在 TypeFilterConfiguration 中,声明 @ComponentScan 注解,扫描整个 f_typefilter 包,之后在 @ComponentScan 注解中声明 includeFilters 属性,让它把含有 @Animal 注解的类带进来:

@Configuration
@ComponentScan(basePackages = "com.linkedbear.spring.annotation.f_typefilter",
               includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Animal.class))
public class TypeFilterConfiguration {
    
}

注意这里面的 @Filter 是 @ComponentScan 注解的内部类哦。

编写启动类,驱动 IOC 容器,并打印容器中所有 Bean 的名称,发现所有标注了模式注解的类全加载进来了:

typeFilterConfiguration
cat
dog
pikachu
green
red
yellow

这跟咱的预想似乎不是很一致:我想只让你把 @Animal 注解过的类带进来,你咋把这么一大堆都给我注册了呢?那是因为,@ComponentScan 注解中还有一个属性:useDefaultFilters ,它代表的是“是否启用默认的过滤规则”。咱之前也讲过了,默认规则就是扫那些以 @Component 注解为基准的模式注解。其实这个属性的文档注释也写的很明白了:

Indicates whether automatic detection of classes annotated with @Component @Repository, @Service, or @Controller should be enabled.

指示是否应启用对以 @Component 、@Repository 、@Service 或 @Controller 注解的类的自动检测。

合着人家默认就扫这几个注解的原因在这里啊,那我指定了我自己想过滤的规则你还不理呗?这是瞧谁不起啊?

莫急莫急,你声明了自己的过滤规则,不耽误人家的呀。换句话说,这些 include 的过滤规则之间互相不受影响,且不会互相排除:你包含的组件我也包含,那咱就一起加载;你不包含的我包含,那我也把它们带过来,而不是我拿过来了你又给我扔了。

也许这样理解起来更容易一些(有颜色的部分代表匹配规则):

Spring IOC进阶-组件扫描高级

2.3 按注解排除

这次咱反过来,刚才不是 include 了吗?这次咱换用 exclude :

@Configuration
@ComponentScan(basePackages = "com.linkedbear.spring.annotation.f_typefilter",
               excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Animal.class))
public class TypeFilterConfiguration {
    
}

重新运行 main 方法,可以发现 Cat 和 Dog 都不见了:

typeFilterConfiguration
pikachu
green
red
yellow

由此可以得出结论:排除型过滤器会排除掉其他过滤规则已经包含进来的 Bean 。

跟上面对比起来,很明显这种情况下包含的组件会少一些(只要是带 @Animal 的都不会被匹配):

Spring IOC进阶-组件扫描高级

2.3 按类型过滤

继续在 @ComponentScan 注解上添加过滤规则,这次咱把所有 Color 类型都包含进来:

@Configuration
@ComponentScan(basePackages = "com.linkedbear.spring.annotation.f_typefilter",
               includeFilters = {@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Color.class)},
               excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Animal.class)})
public class TypeFilterConfiguration {
    
}

重新运行 main 方法,发现这次连父类 Color 也给带进来了:(第 5 个)

typeFilterConfiguration
pikachu
color
green
red
yellow

如果把这个规则也挪到 exclude 中,则 color 、red 、yellow 都就没了:

typeFilterConfiguration
pikachu
green

2.4 正则表达式过滤

除了按注解过滤、按类型过滤,它内置的模式还有两种表达式的过滤规则,分别是 “切入点表达式过滤” 和 “正则表达式过滤” 。关于切入点表达式的概念咱放到 AOP 中再讲,这里先讲正则表达式的方式。

这次咱想通过正则表达式的方式,把那两个 Demo 开头的组件加载进来,正则表达式就可以这样编写:

@Configuration
@ComponentScan(basePackages = "com.linkedbear.spring.annotation.f_typefilter",
               includeFilters = {
                       @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.linkedbear.spring.annotation.f_typefilter.+Demo.+")
               },
               excludeFilters = {
                       @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Animal.class),
                       @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Color.class)
               })
public class TypeFilterConfiguration {
    
}

这样编写好后,重新运行 main 方法,DemoService 和 DemoDao 就会被注册到 IOC 容器了:

typeFilterConfiguration
pikachu
demoDao
demoService
green

2.5 自定义过滤

如果预设的几种模式都不能满足要求,那就需要用编程式过滤方式了,也就是自定义过滤规则。

先定个目标吧,这次编写自定义过滤后,咱们把 green 也过滤掉。

2.5.1 TypeFilter接口

编程式自定义过滤,需要编写过滤策略,实现 TypeFilter 接口。这个接口只有一个 match 方法:

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

这个 match 方法有两个参数,咱是一个也看不懂呀!好在文档注释中有描述,咱可以来参考一下:

  • metadataReader :the metadata reader for the target class
    • 通过这个 Reader ,可以读取到正在扫描的类的信息(包括类的信息、类上标注的注解等)
  • metadataReaderFactory :a factory for obtaining metadata readers for other classes (such as superclasses and interfaces)
    • 借助这个 Factory ,可以获取到其他类的 Reader ,进而获取到那些类的信息
    • 可以这样理解:借助 ReaderFactory 可以获取到 Reader ,借助 Reader 可以获取到指定类的信息

现在看不懂没关系,咱把上面的需求实现出来就好了嘛。

2.5.2 编写自定义过滤规则

MetadataReader 中有一个 getClassMetadata 方法,可以拿到正在扫描的类的基本信息,咱可以由此取到全限定类名,进而与咱需求中的 Green 类做匹配:

public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
         throws IOException {
    ClassMetadata classMetadata = metadataReader.getClassMetadata();
    return classMetadata.getClassName().equals(Green.class.getName());
}

返回 true ,则说明已经匹配上了。

2.5.3 添加过滤规则声明

TypeFilter 写完了,不要忘记加在 @ComponentScan 上:

@Configuration
@ComponentScan(basePackages = "com.linkedbear.spring.annotation.f_typefilter",
               includeFilters = {
                       @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.linkedbear.spring.annotation.f_typefilter.+Demo.+")
               },
               excludeFilters = {
                       @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Animal.class),
                       @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Color.class),
                       @ComponentScan.Filter(type = FilterType.CUSTOM, value = GreenTypeFilter.class)
               })
public class TypeFilterConfiguration {
    
}

重新运行启动类的 main 方法,可以发现 Green 也没了,自定义 TypeFilter 生效。

typeFilterConfiguration
pikachu
demoDao
demoService

更多的 MetadataReader 和 MetadataReaderFactory 的使用,小伙伴们可以自行探索,小册不过多列举。

2.5.4 metadata的概念

讲到这里了,咱先不着急往下走,停一停,咱讲讲 metadata 的概念。

回想一下 JavaSE 的反射,它是不是可以根据咱写好的类,获取到类的全限定名、属性、方法等信息呀。好,咱现在就建立起这么一个概念:咱定义的类,它叫什么名,它有哪些属性,哪些方法,这些信息,统统叫做元信息元信息会描述它的目标的属性和特征

在 SpringFramework 中,元信息大量出现在框架的底层设计中,不只是 metadata ,前面咱屡次见到的 definition ,也是元信息的体现。后面到了 IOC 高级部分,咱会整体的学习 SpringFramework 中的元信息、元定义设计,以及 BeanDefinition 的全解析。

3. 包扫描的其他特性【熟悉】

两个比较重头的特性咱说完之后,还有一些小零碎,咱也盘点盘点。

3.1 包扫描可以组合使用

小伙伴们在写 @ComponentScan 注解时一定有发现还有一个 @ComponentScans 注解吧!不过比较靠前的版本是看不到它的,它起源自 4.3 :

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

其实它就是一次性组合了一堆 @ComponentScan 注解而已了,没啥好说的。

3.2 包扫描的组件名称生成

咱在前面刚学习注解驱动时,就知道默认情况下生成的 bean 的名称是类名的首字母小写形式( Person → person ),可是它为啥就有这个规则呢?这个问题,也可以从 @ComponentScan 注解中找到。

在 @ComponentScan 注解的属性中,有一个 nameGenerator ,它的默认值是 BeanNameGenerator 。不过这个 BeanNameGenerator 是一个接口,从文档注释中不难找到实现类是 AnnotationBeanNameGenerator 。

3.2.1 BeanNameGenerator

从名称上就知道它是 Bean 的名字生成器了,它只有一个 generateBeanName 方法:

public interface BeanNameGenerator {
	String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry);
}

又出现 BeanDefinition 和 BeanDefinitionRegistry 了,可见元信息、元定义在底层真的太常见了!

不过咱先不要把精力放在这里,实现类才是重点。

3.2.2 AnnotationBeanNameGenerator的实现

找到 AnnotationBeanNameGenerator 的实现:

public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
    // 组件的注册方式是注解扫描的
    if (definition instanceof AnnotatedBeanDefinition) {
        // 尝试从注解中获取名称
        String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
        if (StringUtils.hasText(beanName)) {
            // Explicit bean name found.
            return beanName;
        }
    }
    // Fallback: generate a unique default bean name.
    // 如果没有获取到,则创建默认的名称
    return buildDefaultBeanName(definition, registry);
}

看这段源码的实现,整体的逻辑还是非常容易理解的:

  1. 只有注解扫描注册进来的 Bean 才会被处理( AnnotationBeanNameGenerator ,看类名  ̄へ ̄ )
  2. 既然是注解扫描进来的,那我就要看看有木有在注解中声明好了

    这种声明方式就是 @Component("person")

  3. 注解中找不到名,那好吧,我给你构造一个,不过这个名是按照我默认规则来的,你就别挑挑拣拣咯

上面从注解中获取的部分咱留到后面再看,这里咱只看 buildDefaultBeanName 的实现。

3.2.3 buildDefaultBeanName的实现

protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
    return buildDefaultBeanName(definition);
}

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);
}

一路走到最底下的方法中,它会根据组件类的全限定名,截取出短类名(如 com.linkedbear.Person → Person ),最后用一个叫 Introspector 的类,去生成 bean 的名称。那想必这个 Introspector.decapitalize 方法肯定就可以把类名的首字母转为小写咯,点进去发现确实如此:

public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
        Character.isUpperCase(name.charAt(0))){
        return name;
    }
    char chars[] = name.toCharArray();
    // 第一个字母转小写
    chars[0] = Character.toLowerCase(chars[0]);
    return new String(chars);
}

原理实现看完了,小伙伴们肯定有一个疑惑:Introspector 是个什么鬼哦??

3.2.4 Java的内省机制【扩展】

说到这个内省,或许好多小伙伴都没听说过。其实它是 JavaSE 中就有的,对 JavaBean 中属性的默认处理规则

回想一下咱写的所有模型类,包括 vo 类,是不是都是写好了属性,之后借助 IDE 生成 getter 和 setter ,或者借助 Lombok 的注解生成 getter 和 setter ?其实这个生成规则,就是利用了 Java 的内省机制。

**Java 的内省默认规定,所有属性的获取方法以 get 开头( boolean 类型以 is 开头),属性的设置方法以 set 开头。**根据这个规则,才有的默认的 getter 和 setter 方法。

Introspector 类是 Java 内省机制中最核心的类之一,它可以进行很多默认规则的处理(包括获取类属性的 get / set 方法,添加方法描述等),当然它也可以处理这种类名转 beanName 的操作。SpringFramework 深知这个设计之妙,就直接利用过来了。

有关更多的 Java 内省机制,小伙伴们可以搜索相关资料学习,小册不多展开讲解了(这部分适当了解即可)。

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring IOC进阶-组件扫描高级