动态注册Bean到Spring上下文中——基于FeignClient源码的阅读

在上一篇博文《基于Feign的局部请求拦截》的最后,我提出了如何实现系统启动将自定义注解的bean注入到Spring的ApplicationContext中,那么本博文我们就来探讨下具体的代码流程

基于Feign的局部请求拦截

小伙伴们在使用SpringCloud中集成的Feign功能时,只需要编写一个接口,然后再给接口上添加注解@FeignClient,然后配置上相关信息既可以调用其他系统的业务接口,非常的方便;

这里我们不讲解如果在SpringCloud中集成Feign功能,这个网上有大把的博文来讲解该如何使用,说的肯定比我的要详细,精彩;在这里我就基于上一篇博文中,如果将添加自定义注解的组件扫描注入Spring的上下文中;

说明:本博文主要实现的功能是将指定路径下的接口上添加指定注解的对象,通过代理工厂来生成对应的实例对象,然后将该对象注册到Spring的上下文中

具体的业务流程逻辑是:

  1. 在SpringBoot的启动类中添加自定义注解,在该注解中通过 @Import导入自定义注册器
  2. 自定义注册器主要实现如下接口:
    1. ImportBeanDefinitionRegistrar: 该类只能通过其他类@Import的方式来加载,通常是启动类或配置类,通过实现registerBeanDefinitions方法来向Spring上下文中注册自定义的bean组件
    2. ResourceLoaderAware: 获取资源加载器,可以获得外部资源文件
    3. BeanClassLoaderAware: 该接口有个setBeanClassLoader方法,与前两个接口类似,实现了该接口后,可以向bean中注入加载该bean的ClassLoader
    4. EnvironmentAware: 获取项目的环境信息
  3. ImportBeanDefinitionRegistrar中的registerBeanDefinitions来实现注册的功能
  4. 通过注解中指定的扫描路径,然后扫描添加指定注解的接口对象
  5. 然后通过代理工厂的方式来生成该接口的实例对象
  6. 将该实例对象注册到Spring的上下文中

代码

知道了大体的代码流程逻辑,我们就废话不多说了,直接上代码:

  1. 启动类上添加自定义注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = {"com.amos.baseframework.remote"})
@EnableRxFeignLocality(scanPackages = {"com.amos.baseframework.remote"})
public class BaseFrameworkApplication {

    public static void main(String[] args) {
        SpringApplication.run(BaseFrameworkApplication.class, args);
    }

EnableRxFeignLocality就是我们自定义的注解,具体的代码如下:

/**
 * Copyright © 2018 五月工作室. All rights reserved.
 *
 * @Project: springcloudfunctionsample
 * @ClassName: EnableRxFeignLocality
 * @Package: com.amos.baseframework.anno
 * @author: amos
 * @Description: Fegin局部拦截注册器
 * 

* 主要项目启动时扫描指定的包路径下面含有指定注解的组件, * 并且使用代理工厂生成对象,然后注册到Spring的ApplicationContext中 * @date: 2020/2/21 0021 下午 16:09 * @Version: V1.0 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(RxFeignLocalityRegister.class) public @interface EnableRxFeignLocality { /** * 扫描包路径 * * @return */ String[] scanPackages() default {}; }

这里我们就看到 @Import(RxFeignLocalityRegister.class) 这段代码,这里就是我们自定义的注册器

  1. 自定义注册器
/**
 * Copyright © 2018 五月工作室. All rights reserved.
 *
 * @Project: springcloudfunctionsample
 * @ClassName: RxFeignLocalityRegister
 * @Package: com.amos.baseframework.register
 * @author: amos
 * @Description:
 * @date: 2020/2/21 0021 下午 16:29
 * @Version: V1.0
 */
public class RxFeignLocalityRegister implements ImportBeanDefinitionRegistrar,
        ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {

    public static final Logger logger = LoggerFactory.getLogger(RxFeignLocalityRegister.class);

    private ClassLoader classLoader;

    private ResourceLoader resourceLoader;

    private Environment environment;

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

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

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    /**
     * 存放 @EnableRxFeignLocality 注解的所有属性
     */
    private Map enableRxFeignLocalityAttributes = null;

    /**
     * 实现该方法,向Spring上下文中注册指定路径下,指定注解的Bean对象
     *
     * @param metadata 注解的元信息
     * @param registry Spring内置的注册器
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        enableRxFeignLocalityAttributes = metadata.getAnnotationAttributes(EnableRxFeignLocality.class.getName(), Boolean.TRUE);
        logger.info("@EnableRxFeignLocality 注解中属性:{}", enableRxFeignLocalityAttributes);
        // 扫描自定义注解中指定路径下的Bean组件,并且将其注册到Spring的上下文中
        this.registerRxFeignClient(metadata, registry);
    }
    
    ......
}

这里我们主要来重写 registerBeanDefinitions方法来具体的功能

    /**
     * 扫描自定义注解中指定路径下的Bean组件,并且将其注册到Spring的上下文中
     *
     * @param metadata
     * @param registry
     */
    private void registerRxFeignClient(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 获取Provider 其实就是一个是扫描器,提供扫描的功能
        ClassPathScanningCandidateComponentProvider provider = this.getScanner();
        // 给扫描器设置资源加载器
        provider.setResourceLoader(resourceLoader);

        // 添加扫描组件需要过滤的类型,这里我们需要扫描所有添加注解 @RxFeignClient 的class
        AnnotationTypeFilter typeFilter = new AnnotationTypeFilter(RxFeignClient.class);
        // 扫描器
        provider.addIncludeFilter(typeFilter);

        String[] scanPackageArr = (String[]) enableRxFeignLocalityAttributes.get("scanPackages");
        // 如果没有配置则直接就不扫描了  方法直接返回即可
        if (null == scanPackageArr && scanPackageArr.length == 0) {
            logger.info("@RxFeignLocality 中的scanPackages值为空");
            return;
        }
        // 将需要扫描的路径数组 转化为 Set集合
        Set scanPackages = new HashSet<>(CollectionUtils.arrayToList(scanPackageArr));

        Iterator iterable = scanPackages.iterator();
        while (iterable.hasNext()) {
            String packages = iterable.next();
            // 获取指定包路径下面所有添加注解的bean
            Set beanDefinitions = provider.findCandidateComponents(packages);
            Iterator bi = beanDefinitions.iterator();
            while (bi.hasNext()) {
                BeanDefinition beanDefinition = bi.next();
                // 含有注解的bean
                if (beanDefinition instanceof AnnotatedBeanDefinition) {
                    AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
                    AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
                    // 该注解只能添加在接口上
                    Assert.isTrue(annotationMetadata.isInterface(), "@" + RxFeignClient.class.getName() + " 只能标记在接口上");

                    // 将扫描到的接口 根据代理工厂生成实例对象 并且将该实例对象注册到Spring的上下文中
                    this.registerRxFeignBean(registry, annotationMetadata, annotationMetadata.getAnnotationAttributes(RxFeignClient.class.getCanonicalName()));

                }
            }
        }
    }

    /**
     * 将接口根据代理工厂生成实例对象,并且将该实例对象注册到Spring的上下文中
     *
     * @param registry
     * @param annotationMetadata
     * @param annotationAttributes
     */
    private void registerRxFeignBean(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map annotationAttributes) {
        // 获取注解所在的类名
        String className = annotationMetadata.getClassName();
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(RxFeignClientFactoryBean.class);

        // 这里直接使用反射的方式 给通过代理工厂生成的实例对象进行赋值

        // 注意 这里annotationMetadata.getClassName() 是字符串类型的,而在代理工厂类中 resourceClass 是Class类型的
        // 按理说 赋值的话应该会报不合理参数的,但是这里运行没有问题,可能是Spring内部做了处理
        definition.addPropertyValue("resourceClass", annotationMetadata.getClassName());
        definition.addPropertyValue("instanceId", annotationAttributes.get("instanceId"));
        definition.addPropertyValue("url", annotationAttributes.get("directUrl"));
        definition.addPropertyValue("requestProtocolEnum", annotationAttributes.get("RequestProtocol"));
        definition.addPropertyValue("requestInterceptorClass", annotationAttributes.get("requestInterceptor"));
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        String clazzName = className.substring(className.lastIndexOf(".") + 1);
        String alias = this.lowerFirstCapse(clazzName);

        // 向Spring的上下文中注册bean组件
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

    }

    /**
     * 首字母变小写
     *
     * @param str
     * @return
     */
    public String lowerFirstCapse(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

    /**
     * 项目路径下的扫描器
     * 

* ClassPathScanningCandidateComponentProvider 是Spring提供的工具,可以按照自定义的类型,查找classpath下符合要求的class文件 * * @return */ protected ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false, this.environment) { @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { boolean isCandidate = false; // 过滤掉不是注解的 bean if (beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation()) { isCandidate = true; } return isCandidate; } }; }

在上面的代码中我看到了代理工厂RxFeignClientFactoryBean,通过这个代理工厂来生成接口对应的实例对象

  1. 代理工厂
/**
 * Copyright © 2018 五月工作室. All rights reserved.
 *
 * @Package com.amos.baseframework.beanfactory
 * @ClassName RxFeignClientFactoryBean
 * @Description TODO
 * @Author Amos
 * @Modifier
 * @Date 2020/2/23 21:36
 * @Version 1.0
 **/
public class RxFeignClientFactoryBean implements FactoryBean, InitializingBean, ApplicationContextAware {

    private ApplicationContext applicationContext;

    private Class resourceClass;

    private String instanceId;
    private String url;

    private RequestProtocolEnum requestProtocolEnum;

    private Class requestInterceptorClass;

    public static final String HTTPS = "https://";
    public static final String HTTP = "http://";

    @Override
    public Object getObject() throws Exception {
        return target();
    }

    /**
     * 获取目标对象
     *
     * @param 
     * @return
     */
    public  T target() {
        Client client = (Client) getFeignContext().getInstances(instanceId, Client.class);
        T t = (T) Feign.builder().decoder(new GsonDecoder())
                .encoder(new GsonEncoder())
                .client(client)
                .requestInterceptor(requestInterceptorNewInstance())
                .target(resourceClass, parseProtocol());
        return t;
    }

    public RequestInterceptor requestInterceptorNewInstance() {
        try {
            return requestInterceptorClass.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private String parseProtocol() {
        if (!StringUtils.isEmpty(url)) {
            return url;
        }
        switch (requestProtocolEnum) {
            case HTTP:
                return HTTP + instanceId;
            case HTTPS:
                return HTTPS + instanceId;
            default:
                return null;
        }

    }

    private FeignContext getFeignContext() {
        return this.applicationContext.getBean(FeignContext.class);
    }

    @Override
    public Class getObjectType() {
        return resourceClass;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(resourceClass, "标记类不能为空");
        if (StringUtils.isEmpty(instanceId) && StringUtils.isEmpty(url)) {
            throw new IllegalArgumentException("实例名和url不能同时为空");
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public Class getResourceClass() {
        return resourceClass;
    }

    public void setResourceClass(Class resourceClass) {
        this.resourceClass = resourceClass;
    }

    public String getInstanceId() {
        return instanceId;
    }

    public void setInstanceId(String instanceId) {
        this.instanceId = instanceId;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public RequestProtocolEnum getRequestProtocolEnum() {
        return requestProtocolEnum;
    }

    public void setRequestProtocolEnum(RequestProtocolEnum requestProtocolEnum) {
        this.requestProtocolEnum = requestProtocolEnum;
    }

    public Class getRequestInterceptorClass() {
        return requestInterceptorClass;
    }

    public void setRequestInterceptorClass(Class requestInterceptorClass) {
        this.requestInterceptorClass = requestInterceptorClass;
    }
}

 
 

至此,基本的功能就已经实现了,我们可以通过actuator来监控bean组件是否注册到Spring上下文中

  1. 引入 actuator
    pom文件中引入

    org.springframework.boot
    spring-boot-starter-actuator

然后 application.yml文件中添加

management:
  endpoint:
    web:
      base-path: /actuator
  endpoints:
    web:
      exposure:
        include: "*"

最后启动项目即可

完整的代码可以参考: spring-cloud-function-sample

你可能感兴趣的:(动态注册Bean到Spring上下文中——基于FeignClient源码的阅读)