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();
}
}
}
整体的包结结构
这里分两个包的目的是为了等会测试指定扫描包的路径。
结果
两个baen都可以扫描到。
从上面可以看到,分了两个包,a和b,下面各有一个Bean,TestBeanA是@MyAn标注,TestBeanB是@Component修饰,ScanConfig是配置类,被@Configuration修饰,并且在上面标注了@ComponentScan注解,在上面利用includeFilters属性来指定过滤器,@MyAn修饰的Bean为符合条件的Bean,并且指定了过滤器的类型(FilterType.ANNOTATION)
分析
分析主要是开始从配置类的解析操作开始,还记得之前的 Spring——配置类解析过程吗,在这里面简单的提了一嘴@ComponentScan,并没有做具体的分析,本文着重将关于@ComponentScan的内容,但这个前提是配置类解析大体要知道一点,如果不知道,建议先看完配置类解析。
直接开始,从ConfigurationClassParser#doProcessConfigurationClass方法开始
相关的代码就是上面涉及的内容,主要分为两步:
- 获取配置类上@ComponentScan注解的属性。
- 循环遍历,利用
ComponentScanAnnotationParser做加载操作。
获取属性没有什么可说的,从解析加载开始(ComponentScanAnnotationParser#parse方法开始),ComponentScanAnnotationParser里面就是用来解析@ComponentScan注解的。
在parse方法里面使用ClassPathBeanDefinitionScanner来做扫描。ClassPathBeanDefinitionScanner就是利用Context.scan的时候,内部就是利用的他,不过在context里面只是提供了几个方法来配置ClassPathBeanDefinitionScanner的几个属性,但并不是全部。
在ComponentScanAnnotationParser#parse方法中下面也分为两部:
- 创建ClassPathBeanDefinitionScanner,从获取到的@ComponentScan注解中获取属性,配置ClassPathBeanDefinitionScanner。
- 利用ClassPathBeanDefinitionScanner来做加载和解析。
创建ClassPathBeanDefinitionScanner,配置属性
ClassPathBeanDefinitionScanner是一个Bean的定义信息的扫描器,他可以探测Bean在classpath上,并且会将对应的bean(满足条件)的bean注册到给定的BeanFactory中,合格的bean,满足条件的bean是通过配置filter来判断的,默认的filter会探测(@Component,@Repository,@Service,@Controller)。同样也支持@ManagedBean和@Named注解。
创建
构造函数
在构造方法里指定,是否要使用默认的filter。默认的filter就是@Component,@ManagedBean,@Named这几个注解。
注册默认的filter,filter的类型说明
spring中默认的在scan的时候能被加载到Spring中的Bean,默认的注解是
@Component,@ManagedBean,@Name。
可以看到,在默认的注册逻辑里面,会构建AnnotationTypeFilter,并且会将他添加到includeFilters属性中。之后在用。那么下面就先看看过滤器的逻辑
过滤器说明
过滤器要实现TypeFilter接口,主要是给MetadataReader来用的, 是为了判断哪些Bean是合格的。是可以加载到Spring中的。
MetadataReader是用来访问类的元数据的,主要是通过ASM来访问字节码,获取类的元数据,在MetadataReader里面,用了门面设计模式。在TypeFilter里面,就可以通过MedataReader获取class的元信息,比如,类名字,实现的接口,父类,注解,等等信息来做匹配和判断。
这里就不很详细的分析AbstractTypeHierarchyTraversingFilter和他的子类里面的逻辑了。只是大概的说说
首先在匹配的时候,会先调用matchSelf方法(这个逻辑也有点像快速失败的思想,如果这里直接匹配成功了,就不会有后续的操作了,后续的操作是为了会通过ASM来访问字节码)
之后在会matchClassName,匹配方法的名字。
之后会通过标志位控制(AbstractTypeHierarchyTraversingFilter#considerInherited),是否要判断父类,调用matchSuperClass,匹配父类的名字,如果这个方法返回了null,就会递归的在走一遍匹配的方法,这一次的匹配的主题就是父类了。
通过标志位置(considerInterfaces)来控制是否要匹配接口,调用matchInterface匹配接口的名字,如果这个方法返回了null,就会递归再走一遍。
AbstractTypeHierarchyTraversingFilter匹配的逻辑基本说清楚了,AssignableTypeFilter和AnnotationTypeFilter就是在上面说的几个步骤里面通过注解和类型来做匹配,在没有什么特殊的了。
到这里,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,反应在编码上如下所示
要注意,includeFilters是一个@Filter的数组。
此外,ComponentScanAnnotationParser还会注册一个excludeFilters来排除配置类标注了@ComponentScan的类,简单来说,就是排除了配置类,因为配置类在一开始的时候就注册进去了。
ComponentScan的basePackages和basePackageClasses
关于这个就直接看代码吧,可清楚了。
会先从basePackages获取值,如果没有会从basePackageClasses(他是一个class的数组)中获取,拿的是basePackageClasses所在的包,如果没有指定,就从当前声明的类所在的包中扫描。(就是标注@ComponentScan)注解所在包。这也就解释了,为啥Springboot中@SpringApplication标注的类要写在最外面了,因为写在最外面,扫描的路径就是主启动类所在的包已经下层包里面的class对象了,那么有个问题,能不能将Springboot的主启动类写在里面,是可以的,但是得手动指定@ComponentScan的扫描路径,否则在主启动类所在包外层的包里面的class就不会被扫描到
ComponentScan的basePackages和resourcePattern
他是扫描的后缀,默认是
完整的扫描路径如下所示
图中的resourcePattern就是@ComponentScan注解指定的后缀。
利用ClassPathBeanDefinitionScanner做加载和解析
上面配置了创建和配置了ClassPathBeanDefinitionScanner之后,这里做的就是扫描,将class文件加载到内存,通过MetadataReader来访问class文件, 通过filter来做过滤,之后封装为BeanDefintion注册到Spring中。
总体的思路就是上面说的。对应的代码在 ClassPathScanningCandidateComponentProvider#scanCandidateComponents,对了ClassPathScanningCandidateComponentProvider是ClassPathBeanDefinitionScanner的父类。
这里的代码分为四个部分:(相关的代码在ClassPathBeanDefinitionScanner#doScan)
-
扫描加载class文件到内存。
确定扫描路径,将class文件加载到内存中(对应的其实就是
Resource对象),这里的加载其实就是读取class文件,用Resource对象来包装一下 -
读取class文件信息,封装为MetaData。
因为要获取类的信息,比如类的名称,类的注解,类实现的接口,父类,等等这样的信息,所以,就得通过一个方式来获取,在Spring中,他的名字叫做
MetadataReader,这也是Spring中的老套路了,用Resource表示资源,通过MetadataReader的接口,可以得到
ClassMetadata和AnnotationMetadata和Resource,他们三分别对应的是类的元信息(接口,父类,是否是final,是否是abstract,是否是注解,是否是接口等等,这类的信息),类里面注解的信息,class文件。在
MetadataReader里面是通过创建SimpleAnnotationMetadataReadingVisitor(ASM)来直接访问字节码来获取类的信息的。我对字节码也是一知半解,就不再这里说了。对了,还得说一个点,创建MetadataReader是通过简单工厂设计模式来做的。 -
通过filter做过滤。
在获取到
MetadataReader后,后续的操作就可以通过MetadataReader来访问了,通过他就可以访问类的元信息,这个时候,将MetadataReader传递给TypeFilter来决定哪些class能被加载到Spring中,那些不行,对应的代码在ClassPathScanningCandidateComponentProvider#isCandidateComponent。其实就是调用上面说的excludeFilters和includeFilters。 -
封装为BeanDefinition,注册到Spring中。
到这一步,已经能确定是要注册到Spring中的Bean了。
创建
ScannedGenericBeanDefinition,将MetadataReader传递进去,这样就拿到BeanDefintion了,之后通过ScopeMetadataResolver来处理@Scope注解,生产Bean的名字,处理Bean标注的Spring的那些注解(@Lazy,@DependsOn等等,对应的代码在AnnotationConfigUtils#processCommonDefinitionAnnotations),通过BeanNameGenerator来生成Bean的名字,之后,将这一个完整的BeanDefintion注册到Spring中。这里还有一个点,扫描到的class如果是一个配置类的话,也就不能将他简单的注册进去,而是要解析配置类。
大体的思路就是上面说的这样,就不贴具体的代码了,下面围绕几个问题再来详细的说说。
问题
-
确定了扫描的路径,包括,前缀,后缀之后,他是怎么来扫描的?
-
Bean名称是怎么生成的,具体的逻辑是什么?其实就是BeanNameGenerator的功能。
-
MetaDataReader是怎么来读取Class文件的?
下面围绕这几个问题,展开说说
确定了扫描的路径,包括,前缀,后缀之后,他是怎么来扫描的?
具体的代码在PathMatchingResourcePatternResolver#getResources(String)里面。
通过上面的分析,指定了扫描的包的路径之后,后缀为采用默认的 **/*.class,完全的扫描的路径为( 扫描的包/**/*.class ),这种风格叫做Ant风格,在Spring中很常见,比如Spring xml启动的时候配置文件的位置,可以使用Ant风格来做的。
具体是下面的逻辑来做的。
- 确定根路径,其实就是上面说的指定的扫描的包。
- 确定匹配的全路径,用于之后做判断
- 递归拿到跟路径下面的所有的文件,在循环的时候将文件的路径传递给
PathMatcher来判断是否匹配。
PathMatcher使用来做匹配的,他重要的一个实现类是AntPathMatcher,
匹配了,就会将他添加到result里面,这样,result就是所有的结果了,就能拿到class文件了,还有,这里面觉不是这么简单,本质就是这,其中还有很多的判断,比如如果是文件系统怎么办,选择什么样的加载器,都是在这过程中需要用的。
Bean名称是怎么生成的,具体的逻辑是什么?其实就是BeanNameGenerator的功能。
这里说的主要就是BeanNameGenerator了,在ClassPathBeanDefinitionScanner里面默认的是AnnotationBeanNameGenerator。下面说的也是围绕着AnnotationBeanNameGenerator#generateBeanName来说的。
基本的思想如下:
- 既然是要从注解中获取bean的名字,所以,就得拿到注解,拿到属性。
- 但是这注解,不能乱写,属性也不能乱写,否则拿什么去检索,所以得增加一个判断,判断当前类上面的这些注解,有没有之前说好的那几种(@Component或者Component作为元注解的注解,@ManagedBean,@Named),并且规定的属性值value有没有。
- 如果没有这样的注解,或者注解中没有指定,就得给一个默认值。
通过这里,就可以干一个事情,自定义注解,实现@Component的功能,
自定义注解,实现@Component的功能。
这个例子是基于一开始的例子来改的
-
自定义注解
首先@Component得作为元注解,自定义的属性里面得有value,
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Component public @interface MyAn { @AliasFor(annotation = Component.class,value = "value") //通过aliasFor,来做属性值的共享 String value() default ""; } -
配置类上@ComponentScan怎么写?
需要指定TypeFIlter,类型为注解,value传递的是自定义的注解。
@Configuration(proxyBeanMethods = false) @ComponentScan(includeFilters = {@Filter(type = FilterType.ANNOTATION, value = {MyAn.class}) } ) public class ScanConfig { } -
测试
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的构造方法里面,重点看看SimpleAnnotationMetadataReadingVisitor和ClassReader吧。
到这里,@ComponentScan已经解析完了,下面结合上面的分析,自己随便搞搞。
举几个例子
-
匹配指定包下当前目录下面的class文件,别的不用匹配
-
@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 { } -
主启动类和组织结构图
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(); } } }
-
-
自定义bean的名字。
自定义注解,注解指定名字,但是不像上面说的那样,用@Component作为元注解,这里是没有的。自定义
NameGenerator,出现的结果就是,提供一个注解,可以指定bean的名字。下面的例子还是基于上面的例子来实现的。-
自定义注解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented public @interface MyAn { String name() default ""; } -
自定义
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; } } -
配置类添加@ComponentScan注解
通过 nameGenerator属性来指定。
@Configuration(proxyBeanMethods = false) @ComponentScan(includeFilters = {@Filter(type = FilterType.ANNOTATION, value = {MyAn.class}) }, resourcePattern = "*.class", nameGenerator = MyNameGenerator.class ) public class ScanConfig { } -
自定义Bean
@MyAn(name = "testc") public class TestBeanC { } -
主启动类
-
总结
@ComponentScan分析结束了,最直接,简单的思想是,加载指定路径下面的class文件,通过ASM来解析字节码,获取类的元信息,通过TypeFilter来判断那些class可以用,将符合的class文件解析为BeanDefinition(其中包括一些常用的注解),之后将他们注册到Spring中。
但是在指定扫描包的时候,支持ant风格的匹配,总体就是拿到指定扫描包下面的所有的文件,通过路径来判断是否匹配。
还有,在确定需要加载的class的时候,还得选择加载方式,比如,有的是文件系统的,有的是classpath下的,等等。
关于博客这件事,我是把它当做我的笔记,里面有很多的内容反映了我思考的过程,因为思维有限,不免有些内容有出入,如果有问题,欢迎指出。一同探讨。谢谢。