Spring-@ComponentScan分析

514 阅读14分钟

Spring-@ComponentScan分析

简述

ComponentScan注解可以指定Spring Bean 扫描的基本路径。通过它指定的路径,可以加载这个路径下面所有的符合规则的Bean(比如说@Component注解标注的类,或者ManagedBean,Named)。将他们加载到Spring中,他的功能等同于Spring xml配置文件中的<context:component-scan>标签

此外,在他基本的功能上,增加了资源的匹配操作,bean名字的生成,scope注解的解析,是否使用默认的filter。(这个filter就是用来过滤哪些Bean是可以加载的,那些是不需要加载的),一般都是这么做的,先加载所有,然后在利用filter来做过滤。找到合适的资源。这个套路在Springboot的自动装配中也是有的,比如Springboot中的AutoConfigurationImportFilter,他主要是用在AutoConfigurationImportSelector中的。做自动装配的过滤的。

例子

自定义注解@MyAn注解,被@MyAn标注的bean会被添加到Spring中,可以通过ApplicationContext.getBean()获取到。

自定义注解

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

配置类

@Configuration(proxyBeanMethods = false)
@ComponentScan(includeFilters = {@Filter(type = FilterType.ANNOTATION, value = {MyAn.class})},
		useDefaultFilters = true)
public class ScanConfig {
}

实体的bean

@MyAn
public class TestBeanA
{
}

@Component
public class TestBeanB {

}

主启动类

public class TestComponentScan {
	public static void main(String[] args) {
		try {
			AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class);
			TestBeanA bean = context.getBean(TestBeanA.class);
			System.out.println(bean);
			TestBeanB bean2 = context.getBean(TestBeanB.class);
			System.out.println(bean2);
		}catch (Exception e){
			e.printStackTrace();
		}
	}
}

整体的包结结构

image-20220116161729494.png

这里分两个包的目的是为了等会测试指定扫描包的路径。

结果

image-20220116161943581.png

两个baen都可以扫描到。

从上面可以看到,分了两个包,a和b,下面各有一个Bean,TestBeanA是@MyAn标注,TestBeanB是@Component修饰,ScanConfig是配置类,被@Configuration修饰,并且在上面标注了@ComponentScan注解,在上面利用includeFilters属性来指定过滤器,@MyAn修饰的Bean为符合条件的Bean,并且指定了过滤器的类型(FilterType.ANNOTATION)

分析

分析主要是开始从配置类的解析操作开始,还记得之前的 Spring——配置类解析过程吗,在这里面简单的提了一嘴@ComponentScan,并没有做具体的分析,本文着重将关于@ComponentScan的内容,但这个前提是配置类解析大体要知道一点,如果不知道,建议先看完配置类解析。

直接开始,从ConfigurationClassParser#doProcessConfigurationClass方法开始

image-20220116164535607.png 相关的代码就是上面涉及的内容,主要分为两步:

  1. 获取配置类上@ComponentScan注解的属性。
  2. 循环遍历,利用ComponentScanAnnotationParser做加载操作。

获取属性没有什么可说的,从解析加载开始(ComponentScanAnnotationParser#parse方法开始),ComponentScanAnnotationParser里面就是用来解析@ComponentScan注解的。

在parse方法里面使用ClassPathBeanDefinitionScanner来做扫描。ClassPathBeanDefinitionScanner就是利用Context.scan的时候,内部就是利用的他,不过在context里面只是提供了几个方法来配置ClassPathBeanDefinitionScanner的几个属性,但并不是全部。

ComponentScanAnnotationParser#parse方法中下面也分为两部:

  1. 创建ClassPathBeanDefinitionScanner,从获取到的@ComponentScan注解中获取属性,配置ClassPathBeanDefinitionScanner。
  2. 利用ClassPathBeanDefinitionScanner来做加载和解析。

创建ClassPathBeanDefinitionScanner,配置属性

ClassPathBeanDefinitionScanner是一个Bean的定义信息的扫描器,他可以探测Bean在classpath上,并且会将对应的bean(满足条件)的bean注册到给定的BeanFactory中,合格的bean,满足条件的bean是通过配置filter来判断的,默认的filter会探测(@Component,@Repository,@Service,@Controller)。同样也支持@ManagedBean和@Named注解。

创建

构造函数

image-20220116200716518.png

在构造方法里指定,是否要使用默认的filter。默认的filter就是@Component,@ManagedBean,@Named这几个注解。

注册默认的filter,filter的类型说明

image-20220116201833389.png spring中默认的在scan的时候能被加载到Spring中的Bean,默认的注解是@Component,@ManagedBean,@Name

可以看到,在默认的注册逻辑里面,会构建AnnotationTypeFilter,并且会将他添加到includeFilters属性中。之后在用。那么下面就先看看过滤器的逻辑

过滤器说明

过滤器要实现TypeFilter接口,主要是给MetadataReader来用的, 是为了判断哪些Bean是合格的。是可以加载到Spring中的。

MetadataReader是用来访问类的元数据的,主要是通过ASM来访问字节码,获取类的元数据,在MetadataReader里面,用了门面设计模式。在TypeFilter里面,就可以通过MedataReader获取class的元信息,比如,类名字,实现的接口,父类,注解,等等信息来做匹配和判断。

image-20220116205820458.png

这里就不很详细的分析AbstractTypeHierarchyTraversingFilter和他的子类里面的逻辑了。只是大概的说说

首先在匹配的时候,会先调用matchSelf方法(这个逻辑也有点像快速失败的思想,如果这里直接匹配成功了,就不会有后续的操作了,后续的操作是为了会通过ASM来访问字节码)

之后在会matchClassName,匹配方法的名字。

之后会通过标志位控制(AbstractTypeHierarchyTraversingFilter#considerInherited),是否要判断父类,调用matchSuperClass,匹配父类的名字,如果这个方法返回了null,就会递归的在走一遍匹配的方法,这一次的匹配的主题就是父类了。

通过标志位置(considerInterfaces)来控制是否要匹配接口,调用matchInterface匹配接口的名字,如果这个方法返回了null,就会递归再走一遍。

AbstractTypeHierarchyTraversingFilter匹配的逻辑基本说清楚了,AssignableTypeFilterAnnotationTypeFilter就是在上面说的几个步骤里面通过注解和类型来做匹配,在没有什么特殊的了。


到这里,ClassPathBeanDefinitionScanner的创建已经说清楚了,下面看他的配置。相关的代码在ComponentScanAnnotationParser#parse(AnnotationAttributes,String)里面,下面的

配置

主题流程:拿到@ComponentScan注解里面的属性,对应的配置ClassPathBeanDefinitionScanner

代码没啥可说的,我在这说一些主要的几个关键点。

ComponentScan的includeFilters和excludeFilters属性

首先要是知道,includeFilters和excludeFilters都是TypeFilter,只不过他俩有不同的含义,反应在代码里面就是属于不同的集合(ClassPathScanningCandidateComponentProvider#includeFilters和excludeFilters)。

基本的就能串起来了,注解上提供了执行filter,这里读取到,实例化之后,添加到scanner里面去。主线就是这。只不过,Spring提供了一些几个常见的filter,避免我们还得写TypeFilter,他们对应的枚举值分别是:

public enum FilterType {

   // 匹配注解
    // 对应的filter是AnnotationTypeFilter
	ANNOTATION,

 // 匹配类型,对应的filter是AssignableTypeFilter
	ASSIGNABLE_TYPE,

	// 匹配给定的aspectJ表达式
    //filter是AspectJTypeFilter
	ASPECTJ,

    // 匹配给定的正则表达式
    //filter对应的是RegexPatternTypeFilter
	REGEX,

	 // 自定义
    // 需要实现TypeFilter接口
	CUSTOM

}

实例化TypeFilter和通过不同的类型,通过不同的参数来构建不同的TypeFilter对应的代码在ComponentScanAnnotationParser#typeFiltersFor。他会对不同的类型的Filter,有不同的参数的要求。

上面说的指定Filter通过ComponentScan,反应在编码上如下所示

image-20220116215124517.png

要注意,includeFilters是一个@Filter的数组。

此外,ComponentScanAnnotationParser还会注册一个excludeFilters来排除配置类标注了@ComponentScan的类,简单来说,就是排除了配置类,因为配置类在一开始的时候就注册进去了。

ComponentScan的basePackages和basePackageClasses

关于这个就直接看代码吧,可清楚了。

image-20220116215331182.png

会先从basePackages获取值,如果没有会从basePackageClasses(他是一个class的数组)中获取,拿的是basePackageClasses所在的包,如果没有指定,就从当前声明的类所在的包中扫描。(就是标注@ComponentScan)注解所在包。这也就解释了,为啥Springboot中@SpringApplication标注的类要写在最外面了,因为写在最外面,扫描的路径就是主启动类所在的包已经下层包里面的class对象了,那么有个问题,能不能将Springboot的主启动类写在里面,是可以的,但是得手动指定@ComponentScan的扫描路径,否则在主启动类所在包外层的包里面的class就不会被扫描到

ComponentScan的basePackages和resourcePattern

他是扫描的后缀,默认是

image-20220116220107437.png

完整的扫描路径如下所示

image-20220118135621212.png

图中的resourcePattern就是@ComponentScan注解指定的后缀。

利用ClassPathBeanDefinitionScanner做加载和解析

上面配置了创建和配置了ClassPathBeanDefinitionScanner之后,这里做的就是扫描,将class文件加载到内存,通过MetadataReader来访问class文件, 通过filter来做过滤,之后封装为BeanDefintion注册到Spring中。

总体的思路就是上面说的。对应的代码在 ClassPathScanningCandidateComponentProvider#scanCandidateComponents,对了ClassPathScanningCandidateComponentProvider是ClassPathBeanDefinitionScanner的父类。

这里的代码分为四个部分:(相关的代码在ClassPathBeanDefinitionScanner#doScan

  1. 扫描加载class文件到内存。

    确定扫描路径,将class文件加载到内存中(对应的其实就是Resource对象),这里的加载其实就是读取class文件,用Resource对象来包装一下

  2. 读取class文件信息,封装为MetaData。

    因为要获取类的信息,比如类的名称,类的注解,类实现的接口,父类,等等这样的信息,所以,就得通过一个方式来获取,在Spring中,他的名字叫做MetadataReader,这也是Spring中的老套路了,用Resource表示资源,

    通过MetadataReader的接口,可以得到ClassMetadataAnnotationMetadataResource,他们三分别对应的是类的元信息(接口,父类,是否是final,是否是abstract,是否是注解,是否是接口等等,这类的信息),类里面注解的信息,class文件。

    MetadataReader里面是通过创建SimpleAnnotationMetadataReadingVisitor(ASM)来直接访问字节码来获取类的信息的。我对字节码也是一知半解,就不再这里说了。对了,还得说一个点,创建MetadataReader是通过简单工厂设计模式来做的。

  3. 通过filter做过滤。

    在获取到MetadataReader后,后续的操作就可以通过MetadataReader来访问了,通过他就可以访问类的元信息,这个时候,将MetadataReader传递给TypeFilter来决定哪些class能被加载到Spring中,那些不行,对应的代码在 ClassPathScanningCandidateComponentProvider#isCandidateComponent。其实就是调用上面说的excludeFiltersincludeFilters

  4. 封装为BeanDefinition,注册到Spring中。

    到这一步,已经能确定是要注册到Spring中的Bean了。

    创建ScannedGenericBeanDefinition,将MetadataReader传递进去,这样就拿到BeanDefintion了,之后通过ScopeMetadataResolver来处理@Scope注解,生产Bean的名字,处理Bean标注的Spring的那些注解(@Lazy,@DependsOn等等,对应的代码在AnnotationConfigUtils#processCommonDefinitionAnnotations),通过BeanNameGenerator来生成Bean的名字,之后,将这一个完整的BeanDefintion注册到Spring中。

    这里还有一个点,扫描到的class如果是一个配置类的话,也就不能将他简单的注册进去,而是要解析配置类。

大体的思路就是上面说的这样,就不贴具体的代码了,下面围绕几个问题再来详细的说说。

问题

  1. 确定了扫描的路径,包括,前缀,后缀之后,他是怎么来扫描的?

  2. Bean名称是怎么生成的,具体的逻辑是什么?其实就是BeanNameGenerator的功能。

  3. MetaDataReader是怎么来读取Class文件的?

下面围绕这几个问题,展开说说

确定了扫描的路径,包括,前缀,后缀之后,他是怎么来扫描的?

具体的代码在PathMatchingResourcePatternResolver#getResources(String)里面。

通过上面的分析,指定了扫描的包的路径之后,后缀为采用默认的 **/*.class,完全的扫描的路径为( 扫描的包/**/*.class ),这种风格叫做Ant风格,在Spring中很常见,比如Spring xml启动的时候配置文件的位置,可以使用Ant风格来做的。

具体是下面的逻辑来做的。

  1. 确定根路径,其实就是上面说的指定的扫描的包。
  2. 确定匹配的全路径,用于之后做判断
  3. 递归拿到跟路径下面的所有的文件,在循环的时候将文件的路径传递给PathMatcher来判断是否匹配。

PathMatcher使用来做匹配的,他重要的一个实现类是AntPathMatcher,

image-20220118142151220.png

匹配了,就会将他添加到result里面,这样,result就是所有的结果了,就能拿到class文件了,还有,这里面觉不是这么简单,本质就是这,其中还有很多的判断,比如如果是文件系统怎么办,选择什么样的加载器,都是在这过程中需要用的。

Bean名称是怎么生成的,具体的逻辑是什么?其实就是BeanNameGenerator的功能。

这里说的主要就是BeanNameGenerator了,在ClassPathBeanDefinitionScanner里面默认的是AnnotationBeanNameGenerator。下面说的也是围绕着AnnotationBeanNameGenerator#generateBeanName来说的。

image-20220118144110394.png

基本的思想如下:

  1. 既然是要从注解中获取bean的名字,所以,就得拿到注解,拿到属性。
  2. 但是这注解,不能乱写,属性也不能乱写,否则拿什么去检索,所以得增加一个判断,判断当前类上面的这些注解,有没有之前说好的那几种(@Component或者Component作为元注解的注解,@ManagedBean,@Named),并且规定的属性值value有没有。
  3. 如果没有这样的注解,或者注解中没有指定,就得给一个默认值。

通过这里,就可以干一个事情,自定义注解,实现@Component的功能,

自定义注解,实现@Component的功能。

这个例子是基于一开始的例子来改的

  1. 自定义注解

    首先@Component得作为元注解,自定义的属性里面得有value,

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Component
    public @interface MyAn {
    	@AliasFor(annotation = Component.class,value = "value") //通过aliasFor,来做属性值的共享
    	String value() default "";
    }
    
  2. 配置类上@ComponentScan怎么写?

    需要指定TypeFIlter,类型为注解,value传递的是自定义的注解。

    @Configuration(proxyBeanMethods = false)
    @ComponentScan(includeFilters = {@Filter(type = FilterType.ANNOTATION, value = {MyAn.class})
    }
    )
    public class ScanConfig {
    }
    
  3. 测试

    public class TestComponentScan {
    	public static void main(String[] args) {
    		try {
    			AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class);
    			TestBeanA bean = (TestBeanA)context.getBean("testaaa");
    			System.out.println(bean);
    			TestBeanB bean2 = context.getBean(TestBeanB.class);
    			System.out.println(bean2);
    		}catch (Exception e){
    			e.printStackTrace();
    		}
    	}
    }
    
MetaDataReader是怎么来读取Class文件的?

看不懂😁,之前说了是通过ASM直接访问字节码,解析字节码获取信息的,我对字节码一知半解,刚才我以为我可以,不行,看不懂,看不懂。

具体的代码在SimpleMetadataReader的构造方法里面,重点看看SimpleAnnotationMetadataReadingVisitorClassReader吧。


到这里,@ComponentScan已经解析完了,下面结合上面的分析,自己随便搞搞。

举几个例子

  1. 匹配指定包下当前目录下面的class文件,别的不用匹配

    1. @ComponentScan注解怎么写?

      package compontscan;
      
      @Configuration(proxyBeanMethods = false)
      @ComponentScan(includeFilters = {@Filter(type = FilterType.ANNOTATION, value = {MyAn.class})
      },
      		resourcePattern = "*.class" //没有中间的**了,这里没有指定basePackage,那就用标注了@ComponentScan注解的类所在目录为basePackage,这里就是ScanConfig。也就是
                     // compontscan/*.class文件,之前的TestBeanA就扫描不到了
      )
      public class ScanConfig {
      }
      
    2. 主启动类和组织结构图

      public class TestComponentScan {
      	public static void main(String[] args) {
      		try {
      			AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ScanConfig.class);
      			TestBeanC bean = (TestBeanC)context.getBean("testc");
      			System.out.println(bean);
      		}catch (Exception e){
      			e.printStackTrace();
      		}
      	}
      }
      

image-20220118153717228.png

  1. 自定义bean的名字。

    自定义注解,注解指定名字,但是不像上面说的那样,用@Component作为元注解,这里是没有的。自定义NameGenerator,出现的结果就是,提供一个注解,可以指定bean的名字。下面的例子还是基于上面的例子来实现的。

    1. 自定义注解

      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.TYPE)
      @Documented
      public @interface MyAn {
      	String name() default "";
      }
      
    2. 自定义NameGenerator

      继承AnnotationBeanNameGenerator,重写isStereotypeWithNameValue,自己增加了判断MyAn的处理。这只是一个例子,也可以直接重写BeanNameGenerator#generateBeanName

      public class MyNameGenerator extends AnnotationBeanNameGenerator {
      	private static final String MY_AN = "common.MyAn"; 
      
      	@Override
      	protected boolean isStereotypeWithNameValue(String annotationType, Set<String> metaAnnotationTypes, Map<String, Object> attributes) {
      		boolean isStereotype = super.isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes);
      		return isStereotype ?
      				isStereotype :
      				isMyAnWithNameValue(annotationType, metaAnnotationTypes, attributes);
      	}
      
      	protected boolean isMyAnWithNameValue(String annotationType, Set<String> metaAnnotationTypes, Map<String, Object> attributes) {
      		boolean isStereotype = annotationType.equals(MY_AN) ||
      				metaAnnotationTypes.contains(MY_AN);
      		if ((isStereotype && attributes != null && attributes.containsKey("name"))) {
      			attributes.put("value",attributes.get("name"));
      			System.out.println(attributes.get("name"));
      			return true;
      		}
      		return false;
      	}
      }
      
      
    3. 配置类添加@ComponentScan注解

      通过 nameGenerator属性来指定。

      @Configuration(proxyBeanMethods = false)
      @ComponentScan(includeFilters = {@Filter(type = FilterType.ANNOTATION, value = {MyAn.class})
      },
      		resourcePattern = "*.class",
      		nameGenerator = MyNameGenerator.class
      )
      public class ScanConfig {
      }
      
    4. 自定义Bean

      @MyAn(name = "testc")
      public class TestBeanC
      {
      }
      
      
    5. 主启动类

image-20220118220611249.png

总结

@ComponentScan分析结束了,最直接,简单的思想是,加载指定路径下面的class文件,通过ASM来解析字节码,获取类的元信息,通过TypeFilter来判断那些class可以用,将符合的class文件解析为BeanDefinition(其中包括一些常用的注解),之后将他们注册到Spring中。

但是在指定扫描包的时候,支持ant风格的匹配,总体就是拿到指定扫描包下面的所有的文件,通过路径来判断是否匹配。

还有,在确定需要加载的class的时候,还得选择加载方式,比如,有的是文件系统的,有的是classpath下的,等等。

关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。