通过自定义注解和import动态加载bean

【前言】

相信大家平时都用过Mybatis和dubbo框架,
其中Mybatis有一个注解@MapperScan(“com.xxx.mapper”),用来加载指定路径下面的Mapper。
dubbo有个注解@DubboComponentScan(“com.xxx.service”),用来加载指定路径下面的服务。
但是大家有没有想过这两个注解是如何将指定路径下面的类动态加载进来的呢?
进一步点开这两个注解可以看到如下的类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(DubboComponentScanRegistrar.class)
public @interface DubboComponentScan {
...
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({MapperScannerRegistrar.class})
public @interface MapperScan {
...
}

可以发现这两个注解都使用了Import注解,并且DubboComponentScanRegistrar 和 MapperScannerRegistrar都实现了一个接口ImportBeanDefinitionRegistrar,在spring启动时候,正是通过这个接口动态加载指定路径,指定类型的bean

import注解作用

import可以直接将一个类导入到IOC容器中,也可以通过实现ImportSelector 和 ImportBeanDefinitionRegistrar的方式动态加载指定的多个类到IOC容器。
点开import直接源码可以看到,Target属性为ElementType.TYPE,说明只能作用在类上。

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

	Class<?>[] value();

}

下面分别介绍@Import使用的三种方式

@Import使用的三种方式

第一种直接导入类

这种就比较简单,直接启动类上或者Bean上使用注解@Import(DemoService3.class),就可以导入DemoService3这个bean.
下面主要说明ImportSelector 和 ImportBeanDefinitionRegistrar的方式。

第二种使用ImportBeanDefinitionRegistrar

简单用法

先介绍一种简单用法,直接实现ImportBeanDefinitionRegistrar,加载动态加载一个ServicePostProcessor

public class MyConfigrationRegistrar2 implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        // 获得builder
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(ServicePostProcessor.class);
        // 设置注册bean名称
        String generatedName = BeanDefinitionReaderUtils.generateBeanName(beanDefinitionBuilder.getBeanDefinition(), registry, false);
        // 注册bean
        registry.registerBeanDefinition(generatedName,beanDefinitionBuilder.getBeanDefinition());



    }
}


public class ServicePostProcessor  {


}

@Import(MyConfigrationRegistrar2.class)
public class MyConfig {
}

public class MainTest {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);
    }
}

打印结果
在这里插入图片描述
可以看到已经被加载,这是一种简单的用法。但是实际在,我们在实现一些底层框架时候,往往需要加载的类会很复杂,类型很多,路径也很多,那么就需要自定义路径、自定义注解
方便大家理解,模拟一下DubboComponentScan加载过程。

高阶用法(配合自定义注解)

1.首先自定义注解MyComponentScan

自定义一个注解MyComponentScan,用来设置加载的路径包路径

package com.test.registbean;

import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * 1.自定义注解
 *
 * @Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、
 * 类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
 * 在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
 * @Retention
 * @Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;
 * 而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,
 * 而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。
 * 使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyConfigrationRegistrar.class)// 导入加载类MyConfigrationRegistrar
public @interface MyComponentScan {
    String[] value();
}


2.定义ImportBeanDefinitionRegistrar实现类

ImportBeanDefinitionRegistrar 里获得要加载的packages,注册一个ServicePostProcessor,用来实现加载指定的类。

package com.test.registbean;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;

public class MyConfigrationRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

		// 获得注解中的value (获取到指定路径)
        AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(
        importingClassMetadata.getAnnotationAttributes(MyComponentScan.class.getName()));
        String[] value = annotationAttributes.getStringArray("value");
        Set<String> packages = new LinkedHashSet<String>(Arrays.asList(value));

        // 获得builder,设置要动态注册的Bean,这里注册了ServicePostProcessor,
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(ServicePostProcessor.class);
        beanDefinitionBuilder.addConstructorArgValue(packages);

        // 设置注册bean名称
        String generatedName = BeanDefinitionReaderUtils.generateBeanName(beanDefinitionBuilder.getBeanDefinition(), registry, false);
        // 注册bean
        registry.registerBeanDefinition(generatedName,beanDefinitionBuilder.getBeanDefinition());


    }
}

其实到这一步,就已经可以将ServicePostProcessor当做Bean注册到IOC容器中,只需要@Import(MyConfigrationRegistrar.class)即可,就能动态注册ServicePostProcessor这个Bean。

为了演示DubboComponentScan原理,方便大家理解,这里会对ServicePostProcessor进行一下其他增强处理,实现加载指定路径、指定类型的Bean.
实现这个功能,这里就需要2个要素。

  1. 被加载Bean的路径(上一步已经获得)
  2. 即使拿到了路径,也不是所有的类都要加载,这就需要定义一个标志,表明哪些类需要加载到IOC,这里就需要自定义一个注解MyService。
3.自定义注解MyService

自定义注解MyService,用来模拟Spring的注解@Service,后面会加载被MyService标记的类

package com.test.registbean;

import java.lang.annotation.*;

/**
 * 自定义service注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface MyService {
}

4.通过上面加载的ServicePostProcessor,实现加载指定路径下的Bean.

ServicePostProcessor实现了BeanDefinitionRegistryPostProcessor接口,那么在容器启动时候,就会调用实现方法postProcessBeanDefinitionRegistry

package com.test.registbean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.ResourceLoader;
import java.util.Set;

public class ServicePostProcessor implements BeanDefinitionRegistryPostProcessor, ResourceLoaderAware {

    private Set<String> packages;

    ResourceLoader resourceLoader;

    public ServicePostProcessor(Set<String> packages) {
        this.packages = packages;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    // MyBeanScanner ,用来加载被@MyService指定的Bean,其实现在下方
        MyBeanScanner myBeanScanner = new MyBeanScanner(registry);
        myBeanScanner.setResourceLoader(resourceLoader);
        myBeanScanner.registFilters();
        for (String packageValue : packages) {
            myBeanScanner.scan(packageValue);
        }
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
}

5.自定义MyBeanScanner

MyBeanScanner 用来加载指定路径下的Bean和过滤出被@Myservice标记的Bean

package com.test.registbean;

import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.core.type.filter.AnnotationTypeFilter;

import java.util.Set;

public class MyBeanScanner extends ClassPathBeanDefinitionScanner {

    public MyBeanScanner(BeanDefinitionRegistry registry) {
        super(registry);
    }
    public void registFilters() {
        // 只加载被MyService注解的bean
        addIncludeFilter(new AnnotationTypeFilter(MyService.class));

        // 同样的满足任意excludeFilters不会被加载
//         addExcludeFilter(new AnnotationTypeFilter(MyService.class));
    }

    // doScan方法会加载该路径下执行的类型bean
    @Override
    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        return super.doScan(basePackages);
    }
}

6.定义相关需要加载的Bean

一个有@Myservice注解,一个没有被注解。
DemoService

package com.test.registbean.service;

import com.test.registbean.MyService;

@MyService
public class DemoService {
}

DemoService2

package com.test.registbean.service;


public class DemoService2 {
    private String name;
}

7.测试

首先定义个MyConfig用来标记需要加载的路径,这里路径设置com.test.registbean.service,下面有2个类,一个DemoService(被@MyService标记,DemoService2未被注解标记)

package com.test.registbean;

import org.springframework.context.annotation.Import;

@MyComponentScan(value = "com.test.registbean.service")
public class MyConfig {
}
8.启动类MainTest
package com.test.registbean;

import com.test.registbean.service.DemoService;
import com.test.registbean.service.DemoService2;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class MainTest {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig.class);

        DemoService demoService = applicationContext.getBean(DemoService.class);
        System.out.println("demoservice====>" + demoService);


        DemoService2 demoService2 = applicationContext.getBean(DemoService2.class);
        System.out.println("demoservice2 ====>===> " + demoService2);

    }

}

9.打印结果

在这里插入图片描述
可以发现demoservice被成功注册,demoservice2未被注册。

ImportBeanDefinitionRegistrar 总结

上面几个过程基本是一个通用的动态加载Bean的模板,通过实现ImportBeanDefinitionRegistrar ,通过Import注解,在容器启动时,通过registerBeanDefinition就可以加载指定类,当加载类比较复杂时,我们可以配合自定义的注解加载。通过自定义的Scanner,使用addIncludeFilter过滤出需要加载的类。

第三种ImportSelector

ImportSelector

通过实现ImportSelector,返回类路径数组,直接可以加载指定的bean.

实现方法ImportSelectorTest

实现方法ImportSelectorTest,返回需要加载的Bean的全路径。

package com.test.registbean;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Set;

public class ImportSelectorTest implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        Set<String> set = importingClassMetadata.getAnnotationTypes();

		// 直接返回需要加载的类路径
        return new String[]{"com.test.registbean.service.DemoService2"};

    }
}

通过import将该实现类导入IOC即可。

Import导入ImportSelectorTest

比如将上面测试类中MyConfig,加上一行@Import(ImportSelectorTest.class)即可。

package com.test.registbean;

import org.springframework.context.annotation.Import;

@MyComponentScan(value = "com.test.registbean.service")
@Import(ImportSelectorTest.class)
public class MyConfig {
}

再次打印结果

在这里插入图片描述
可以发现demoservice 和demoservice2都被加载。

注意 ,ImportSelector也是SpringBoot自动装备的核心原理,springboot通过加载约定的配置文件META-INF/spring.factories,文件中有需要加载的类的全路径,通过ImportSelector实现自动加载。
springboot的自动装备原理,参考前一篇文章SpringBoot自动装配底层原理分析

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EmineWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值