spring如何扫描解析bean(注册bean的多种方式)

注解bean

我们常用的向spring容器中添加bean的方式主要有三种

  • @Component注解
  • @Configuration 加 @bean
  • @Import

那么spring是如何解析这些注解的,本文具体研究这个问题

parse

spring解析注解bean的代码写在ConfigurationClassParser类的parse方法,参数就是SpringApplication.run时传入的启动主类

spring启动时会解析我们的主配置类(就是带@SpringBootApplication的启动类),解析的任务交给解析器ConfigurationClassParser,对应的方法就是parse

ConfigurationClassParser中一个属性configurationClasses,用来存放解析出来的结果

private final Map<ConfigurationClass, ConfigurationClass> configurationClasses = new LinkedHashMap<>();

对应的get方法

public Set<ConfigurationClass> getConfigurationClasses() {
    return this.configurationClasses.keySet();
}

processConfigurationClass

parse方法最终会走向processConfigurationClass方法

ConfigurationClassParser.parse

这个方法贴主要代码

do {
    sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);

调用doProcessConfigurationClass方法,如果有返回值,递归调用doProcessConfigurationClass,看spring的注释说// Recursively process the configuration class and its superclass hierarchy.,也就是递归解析配置类和他的父类,所以这代码的意思就是解析配置类,如果有父类再解析父类,如果父类有父类再一直解析下去。
所以重点就来到了doProcessConfigurationClass方法

doProcessConfigurationClass

doProcessConfigurationClass(spring源码中的doXXX一般都很重要),接下来就分析这个代码,贴完整代码

protected final SourceClass doProcessConfigurationClass(
        ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
        throws IOException {
    if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
        // 1.内部类  这一步看看有没有内部类,一般不咋用
        processMemberClasses(configClass, sourceClass, filter);
    }

    // 2.@PropertySource 这一步解析@PropertySource注解,更改配置文件位置时会使用,一般使用默认位置,不咋更改
    for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), PropertySources.class,
            org.springframework.context.annotation.PropertySource.class)) {
        if (this.environment instanceof ConfigurableEnvironment) {
            processPropertySource(propertySource);
        }
        else {
            logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                    "]. Reason: Environment must implement ConfigurableEnvironment");
        }
    }

    // 3.@ComponentScan 这一步就很重要了,解析@ComponentScan注解
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    if (!componentScans.isEmpty() &&
            !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
        for (AnnotationAttributes componentScan : componentScans) {
            // 开始扫描,把@ComponentScan指定包下的@Component类全部扫描出来
            Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
            // 把所有扫描到的beanClass递归解析,所以我们也可以加多个@Configuration类
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
                if (bdCand == null) {
                    bdCand = holder.getBeanDefinition();
                }
                //判断是不是ConfigurationClass 带@Configuration注解和@Component注解都算
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                    // 解析
                    parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        }
    }

    // 4.@Import 解析@Import注解
    processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

    // 5.@ImportResource解析@ImportResource解析
    AnnotationAttributes importResource =
            AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
    if (importResource != null) {
        String[] resources = importResource.getStringArray("locations");
        Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
        for (String resource : resources) {
            String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
            configClass.addImportedResource(resolvedResource, readerClass);
        }
    }

    // 6.@Bean 解析带有@Bean注解的方法,加入到configClass的beanMethods属性中
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
    for (MethodMetadata methodMetadata : beanMethods) {
        configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }

    // 6.接口@Bean 解析实现的接口中带有@Bean注解的默认方法,加入到configClass的beanMethods属性中
    processInterfaces(configClass, sourceClass);

    // 7.父类 如果有父类返回父类,以继续解析
    if (sourceClass.getMetadata().hasSuperClass()) {
        String superclass = sourceClass.getMetadata().getSuperClassName();
        if (superclass != null && !superclass.startsWith("java") &&
                !this.knownSuperclasses.containsKey(superclass)) {
            this.knownSuperclasses.put(superclass, configClass);
            // Superclass found, return its annotation metadata and recurse
            return sourceClass.getSuperClass();
        }
    }

    // 没有父类,解析结束
    return null;
}

整个流程总结如下

  • 解析内部类
  • 解析@PropertySource注解
  • 解析@ComponentScan注解,扫描指定包下的所有@Component类,并递归解析
  • 解析@Import注解
  • 解析@ImportResource注解
  • 解析@Bean
  • 解析实现接口中的@Bean
  • 返回父类

接下来一个个看

解析内部类

if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
    // 1.内部类  这一步看看有没有内部类,一般不咋用
    processMemberClasses(configClass, sourceClass, filter);
}

也就是说如果一个类有@Component注解,会解析他的内部类如果也有@Component会注册成bean,写个代码测试一下

@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        Nested nested = context.getBean(Nested.class);
        System.out.println(nested); // com.pqsir.parser.ClassParserApplication$Nested@4e7912d8
    }

    @Component
    class Nested {

    }
}

所以内部类也可以注册到bean容器

解析@PropertySource注解

这个真没用过,我觉得配置文件放在规定的地就不错,以后也好找,这个就不研究了

解析@ComponentScan

这个都懂,就是扫描的包路径,值的注意的是扫描到的class都会递归调用parser
这一步的代码细分析下

  • 扫描包下的类
Set<BeanDefinitionHolder> scannedBeanDefinitions =
                    this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());

这个componentScanParser内部有个scaner(扫描器),扫描@Component注解的类(包括子注解@Configuration@Service等)

  • 循环判断扫描到的类是否是ConfigurationClass,如果是则递归解析
// 如果是ConfigurationClass
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
    // 解析
    parse(bdCand.getBeanClassName(), holder.getBeanName());
}

这一步ConfigurationClass不单单是指带@Configuration注解的类,带@Component注解的也算ConfigurationClass,spring内部有个集合,只要是这个集合里的注解,都算ConfigurationClass

// ConfigurationClassUtils
private static final Set<String> candidateIndicators = new HashSet<>(8);
static {
    candidateIndicators.add(Component.class.getName());
    candidateIndicators.add(ComponentScan.class.getName());
    candidateIndicators.add(Import.class.getName());
    candidateIndicators.add(ImportResource.class.getName());
}
public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
    // 省略
    // 是否有候选注解
    for (String indicator : candidateIndicators) {
        if (metadata.isAnnotated(indicator)) {
            return true;
        }
    }
    // 省略
}

总结【1】带@Configuration或@Component类都是ConfigurationClass

最后做个小测试--主类制定了@ComponentScan包下又一个带@ComponentScan的类指向另一个包,那么这两个包下的bean都会被注入,测试一下,我们建两个包parserparser2
parser2下一个普通bean:BeanOut

package com.pqsir.parser2;
@Component
public class BeanOut {
    @Override
    public String toString() {
        return "BeanOut";
    }
}

parser下ClassParserApplication主类扫描parser和 OtherConfiguration:在parser包下定义扫描parser2

package com.pqsir.parser
@Configuration
@ComponentScan("com.pqsir.parser") //扫描parser
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanOut bean = context.getBean(BeanOut.class);
        System.out.println(bean); // BeanOut
    }
}
@Configuration
@ComponentScan("com.pqsir.parser2")
public class OtherConfiguration {
}

最终正常输出"BeanOut"

解析@Import注解

这个也比较好理解,就是如果某个bean带@Import注解,就把@Import注解指定的类也注册成bean
测试一下,我们把上一步BeanOut的@Component注解去掉,删除OtherConfiguration

@Configuration
@ComponentScan("com.pqsir.parser")
@Import({BeanOut.class})
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanOut bean = context.getBean(BeanOut.class);
        System.out.println(bean); // BeanOut
    }
}

结果也可以正常输出
这个@Import的最大好处可以把一些第三方的类给注入到bean容器,因为第三方的类一般也改不了总不能去加@Component注解吧,而且如果你是一个第三方开发者,肯定不希望一直的工具依赖spring(万一哪天没人用了你的工具也废了),所以通过@Import把你的工具引入spring是一个完美的解决方案。
还有个比较大的好处是可以做封装,@Import注解可以被继承,比如我们写个自定义注解继承了@Import,并指定import的类,就可以把这个类注册到bean容器中,甚至可以让这个类继承一些后置处理器来给bean容器做调整,比如AOP的@EnableAspectJAutoProxy就是用到这一点,还有mybaits的也是用@MapperScan也是使用Import的方式完成一些mapper bean的生成工作
上例是@Configuration+@Import,用@Component+@Import也ok,因为【1】

解析@ImportResource注解

主要为了兼容之前的xml写法

解析@Bean

@Bean注解一般经常使用,使用工厂方法创建一个bean,一般就是@Configuration+@Bean,由于上述原因【1】,所以@Component+@Bean也可以。

解析实现接口中的@Bean

这是对@Bean注解的一个扩展,解析实现的接口中带有@Bean注解的默认方法,写个例子测试一下

  • 接口(包含默认方法带@Bean注解,本身不带任何注解)
public interface IBeanA {
    @Bean
    default BeanOut beanOut() {
        return new BeanOut();
    }
}
  • 实现(带@Component注解)
@Component
//@Import({BeanOut.class})
public class BeanA implements IBeanA {

}
  • 测试类
@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanOut bean = context.getBean(BeanOut.class);
        System.out.println(bean); // BeanOut
    }
}

也会正常输出,这个真没想到什么使用场景,遇到再说吧

返回父类

最后一步如果返回父类继续递归解析,测试一下

  • 父类(没有@Component注解)
public class BeanFather {
    @Override
    public String toString() {
        return "BeanFather";
    }
}
  • 子类(有@Component注解)
@Component
public class BeanSon extends BeanFather{
}
  • 测试类(尝试获取父类bean)
@Configuration
@ComponentScan("com.pqsir.parser")
public class ClassParserApplication {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
        BeanFather bean = context.getBean(BeanFather.class);
        System.out.println(bean); // BeanFather 
    }
}

正常可获取

最后

完成这一系列的解析扫描再解析过程,就可以通过getConfigurationClasses拿到所有扫描并解析到的类。
spring拿到这些类之后再通过一个readerConfigurationClasse转换为bean定义,注册到beanFactory,所以parser+reader,就完成了这些bean的扫描&解析&注册工作,代码在ConfigurationClassPostProcessor中。

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    // 创建一个解析器
    ConfigurationClassParser parser = new ConfigurationClassParser(
                this.metadataReaderFactory, this.problemReporter, this.environment,
                this.resourceLoader, this.componentScanBeanNameGenerator, registry);
    // 开始解析,这个candidates就是我们传入的MainApplication
    parser.parse(candidates);
    // 获取解析到的类
    Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
    // 初始化一个reader
    this.reader = new ConfigurationClassBeanDefinitionReader(
                        registry, this.sourceExtractor, this.resourceLoader, this.environment,
                        this.importBeanNameGenerator, parser.getImportRegistry());
    // 把上面解析到的类转化为bean定义并注册到bean定义注册器(registry)
    this.reader.loadBeanDefinitions(configClasses);
}

扩展

@Configuration和@Component的区别

上文很多@Configuration的注解都可以用@Component代替,甚至主类使用@Component来替换@Configuration也能正常跑,那问题来了,他俩就没有区别吗,那要@Configuration有啥用。
却别主要两方面
一.首先,@Configuration和@Service,@Controller注解很像,都继承@Component注解,没有实际的什么作用只是告诉别人这个类是个配置类型的bean
二.其次,也是实际功能上的区别,使用@Configuration类+@Bean,sping会生成一个cglib动态代理,这个代理的工能就是第一次调用@Bean的方法会直接执行并返回结果,同时存储结果,下一次调用同样方法直接返回结果,这样可以保证单例,不管调用多少次@Bean的方法最终得到的结果是同一个对象,而使用@Component则不会

做个测试

通过@Bean注册三个bean A B C,其中BC依赖注入A,先使用@Configuration

@Configuration
public class BeanConfiguration {
    @Bean
    public A a() {
        return new A();
    }
    @Bean
    public B b() {
        B b = new B();
        b.a = a();
        return b;
    }
    @Bean
    public C c() {
        C c = new C();
        c.a = a();
        return c;
    }
    static class A {
    }
    static class B {
        public A a;
    }
    static class C {
        public A a;
    }
}

试一下

ApplicationContext context = new AnnotationConfigApplicationContext(ClassParserApplication.class);
BeanConfiguration.B b = context.getBean(BeanConfiguration.B.class);
BeanConfiguration.C c = context.getBean(BeanConfiguration.C.class);
System.out.println(b.a.equals(c.a));

输出结果是true
如果把@Configuration改成@Component,输出结果就变成false了,这个代理的代码ConfigurationClassPostProcessor.enhanceConfigurationClasses中,有兴趣的自己研究吧
其实这种写法本来就不太好,还是觉得用依赖注入更好,如下

@Configuration
public class BeanConfiguration {
    @Bean
    public A a() {
        return new A();
    }
    @Bean
    public B b(A a) {
        B b = new B();
        b.a = a;
        return b;
    }
    @Bean
    public C c(A a) {
        C c = new C();
        c.a = a;
        return c;
    }
    static class A {
    }
    static class B {
        public A a;
    }
    static class C {
        public A a;
    }
}

这种写法就算改成@Component也没问题

@Import+ImportBeanDefinitionRegistrar

上面说了@Import可以导入bean,一个或多个bean,但如果比如把一个包下的所有类都注入到bean,它就不能实现了,除非你一个一个写,但是这样新增一个就得写一个。
这种需求其实很常见,比如mybaits的@MapperScan,他需要你指定一个mapper的位置,然后把mapper全部注入到bean容器,不管你加多少mapper都会注入进去。
spring批量注册bean是有个后置处理器支持的,就是BeanDefinitionRegistryPostProcessor,如果你某个bean实现了BeanDefinitionRegistryPostProcessor,就会拿到bean定义注册器BeanDefinitionRegistry,然后爱怎么注册bean定义、注册多少随你。
所以我最开始遇到这种需求解决思路是@Import+BeanDefinitionRegistryPostProcessor,虽然可行,但获取不到注解的属性,比如@MapperScan的value

所以spring要引入ImportBeanDefinitionRegistrar这个接口,其中重要方法

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

通过实现这个方法也能拿到BeanDefinitionRegistry(ConfigurationClassPostProcessor本身继承BeanDefinitionRegistryPostProcessor,所以可以拿到),然后可以按照自己的意思注册bean定义,更重要的:通过第一个参数importingClassMetadata可以获取使用@Import注解的类的元数据,就可以获取到外层注解的属性值
那么问题来了,什么时候执行呐。
刚才"最后"章节的代码有一句this.reader.loadBeanDefinitions(configClasses);,就是在这个时候继承这个接口的类(被@Import引入)执行registerBeanDefinitions方法

registerBeanDefinitions

可以自己去找代码,整个过程再ConfigurationClassPostProcessor执行的生命周期执行完毕

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容