神奇的BeanDefinitionRegistrar

使用场景介绍

由于工作上是负责一个数智中台,一个配置智能客服与机器人的中台,比如一个我们在打开一个app后,点反馈功能的一个im对话功能。在该对话中,有一个很重要的接口,是query接口,当用户发送消息时,其实就是调用这个query,从而去判断回复什么,是否需要转人工等数据。
而在query接口中,需要去判断用户发送的文本会命中后台配置的哪一个标准问,这个判断是别的小组底层unit做的,我们需要做的是调用对方的接口。而且这种调用底层unit的场景还蛮多的

项目一开始是对HttpClient进行了再封装成一个HttpUnit的,调用方法时是调用这样的doPost方法

public static <T> T doPost(String target,
    String uri,
    QueryString qs,
    Headers headers,
    Object body,
    boolean isJsonBody,
    Map<String, MultiContent> multis,
    ConfigOption configOption,
    List<CustomizePostProcessor> customizePostProcessors,
    Type typeOfT) {
    return doPost(target, uri, qs, headers, body, isJsonBody, multis,
    configOption, customizePostProcessors, typeOfT, null);
}

但随着调用的底层unit接口越来越多,调用底层接口本身需要携带某些特定的参数,又与中台本身逻辑耦合在一起,可维护性变得很差,于是后面就有了一种基于注解的方案,有点类似于OpenFeign,接口声明像下面这样,但同时可以添加一些自己的逻辑进去,包括可以修改重传次数什么的。

@HttpUtil.AHttpClient(target = "https://aip.baidubce.com",
    connectTimeout = "3500",
    readTimeout = "3500",
    retryTimes = "3",
    requestInterceptors = "unitAccessTokenInterceptor") // 在request前的一个拦截器
public interface UnitModelApi {
    
    @PostMapping("/rpc/2.0/unit/v3/faqskill/model/train")
    UnitResult<Model> train(@RequestBody TrainModelDTO trainModelDTO);
}

然后再调用时我们只需要这样调用

UnitAccessTokenInterceptor.setRobotId(robotId);  // 添加调用所需的AccessToken
result = unitQueryOfflineApi.query(queryDTO);

这样就变得简单很多了。

使用介绍

我们点进去看拦截器,很简单的就把token注入进去

@Slf4j
@Component
public class UnitAccessTokenInterceptor implements RequestInterceptor {

    private static final ThreadLocal<Long> ROBOT_IDS = new ThreadLocal<>();

    public static void setRobotId(long robotId) {
        ROBOT_IDS.set(robotId);
    }

    @Override
    public void intercept(String uri, QueryString qs, Headers headers, JsonElement body) {
        //省略若干代码...
        Long theRobotId = ROBOT_IDS.get();
        UnitAccountService unitAccountService = ApplicationContextUtil.getBean("unitAccountService");
        String accessToken = unitAccountService.getAccessTokenToRedis(theRobotId);
        qs.addQsKv("access_token", accessToken);   // qs是url请求参数
        ROBOT_IDS.remove();
    }
}

可以看到,是把线程需要的token放进qs中,qs其实是HttpUtil中的k/v参数,简化了调用远程接口所需参数的注入。那为什么写上@AHttpClient注解,就可以办到呢?在HttpUtil中有两个注解,

public class HttpUtil {
    
    // 省略若干行代码
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface AHttpClient {
        String name() default "";
        String value() default "";
        String target() default "";
        String connectTimeout() default "";
        String readTimeout() default "";
        String retryTimes() default "";
        String retryInterval() default "";
        String[] requestInterceptors() default {};
    }

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Import(AHttpClientScannerRegistrar.class)   // 注意这里,引入我们自己的类注册器
    @Documented
    public @interface EnableAHttpClients {
        /*
         * 需要扫描的包路径, value与basePackages同意
         */
        String[] value() default {};

        String[] basePackages() default {};
    }
}

我们在启动类上注上@EnableAHttpClients,即表示开启我们自己的AHttpClient注解功能,在这个
@EnableAHttpClients中,可以看到,它上面又@Import(AHttpClientScannerRegistrar.class),表示引入了我们自己的BeanDefinitionRegistar注册类,这个类有什么用呢?
ImportBeanDefinitionRegistrar接口其实是Spring的扩展点之一,它可以支持我们自己的代码封装成BeanDefinition对象;实现此接口的类会回调postProcessBeanDefinitionRegistry方法,注册到Spring容器中。

AHttpClientScannerRegistrar注册类

private static class AHttpClientScannerRegistrar implements ImportBeanDefinitionRegistrar,
                                                            ResourceLoaderAware, EnvironmentAware {
    private ResourceLoader resourceLoader;

    private Environment environment;

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

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

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
                                        BeanDefinitionRegistry registry) {
        // 创建扫描带有@AHttpClient注解类的scanner
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(
                false, this.environment) {
            @Override
            protected boolean isCandidateComponent(
                    AnnotatedBeanDefinition beanDefinition) {
                return true;
            }
        };
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(AHttpClient.class));

        // 获取带有@EnableClients注解的实际属性值: value/basePackages, 取到需要扫描的包路径
        Set<String> basePackages = new HashSet<>();
        Map<String, Object> enableAHttpClientsAttributes = metadata
                .getAnnotationAttributes(EnableAHttpClients.class.getName());
        if (enableAHttpClientsAttributes != null) {
            for (String pkg : (String[]) enableAHttpClientsAttributes.get("value")) {
                if (StringUtils.isNotBlank(pkg)) {
                    basePackages.add(pkg);
                }
            }
            for (String pkg : (String[]) enableAHttpClientsAttributes.get("basePackages")) {
                if (StringUtils.isNotBlank(pkg)) {
                    basePackages.add(pkg);
                }
            }
        }
        if (basePackages.isEmpty()) {
            basePackages.add(ClassUtils.getPackageName(metadata.getClassName()));
        }

        log.info("[AHttpClientScannerRegistrar] basePackages = {}", JsonUtil.toJson(basePackages));

        // 逐个扫描出带有@AHttpClient注解的类
        for (String basePackage : basePackages) {
            for (BeanDefinition candidateComponent : scanner.findCandidateComponents(basePackage)) {
                // 如果容器类不是注解,跳过
                if (!(candidateComponent instanceof AnnotatedBeanDefinition)) {
                    continue;
                }

                // 获取metaData
                AnnotationMetadata aHttpClientMetadata =
                                        ((AnnotatedBeanDefinition) candidateComponent).getMetadata();
                String className = aHttpClientMetadata.getClassName();
                log.info("[AHttpClientScannerRegistrar] |-- [{}] candidate class found", className);

                Class<?> clazz = null;
                try {
                    clazz = Class.forName(className);
                } catch (ClassNotFoundException e) {
                    // throw new RuntimeException("[AHttpClientScannerRegistrar] invalid class " + className, e);
                    log.warn("[AHttpClientScannerRegistrar] |--- [{}] invalid class: ", className, e);
                    continue;
                }

                // 并且需要确保,该注解是声明在接口上的,而不是类上的
                if (!aHttpClientMetadata.isInterface()) {
                    log.warn("[AHttpClientScannerRegistrar] |--- [{}] @AHttpClient can only be used on interface! "
                            + "this one will be ignored", className);
                    continue;
                }

                // 获取到带有@AHttpClient注解的实际属性值
                Map<String, Object> aHttpClientAttributes = aHttpClientMetadata
                                        .getAnnotationAttributes(AHttpClient.class.getName());

                // beanName 默认为clazz simpleName 首字母小写
                String beanName = StringUtils.uncapitalize(clazz.getSimpleName());
                // 从attributes中获取指定的name, 替换默认beanName, 如果有的话
                if (aHttpClientAttributes != null) {
                    String specifiedBeanName = (String) aHttpClientAttributes.get("name");
                    if (StringUtils.isNotBlank(specifiedBeanName)) {
                        beanName = specifiedBeanName;
                    }
                }

                /**
            	* 下面这句话是最重要的
            	* 使用AHttpClientFactoryBean来创建BeanDefinitionBuilder
                * BeanDefination不是真正的bean, 真正的bean实例化由AHttpClientFactoryBean来完成
            	* 用户逻辑上是感知不到所用类是一个普通类还是一个工厂类的,但工厂类可以返回这个工厂的getObjet()返回的类
				*/
                BeanDefinitionBuilder aHttpClientBeanDefinitionBuilder = BeanDefinitionBuilder
                                                        .genericBeanDefinition(AHttpClientFactoryBean.class);
                aHttpClientBeanDefinitionBuilder.addPropertyValue("type", clazz);
                aHttpClientBeanDefinitionBuilder.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

                // 将@AHttpClient中的各个属性, 添加到beanDefinination中
                // 注意: 这里需要确保属性在@AHttpClient, AHttpClientFactoryBean中保持名称一致
                if (aHttpClientAttributes != null) {
                    // 属性: value & target (优先value)
                    for (String attrName : new String[] {"value", "target"}) {
                        String value = (String) aHttpClientAttributes.get(attrName);
                        if (StringUtils.isNotBlank(value)) {
                            value = environment.resolveRequiredPlaceholders(value);
                        }
                        if (StringUtils.isNotBlank(value)) {
                            log.info("[AHttpClientScannerRegistrar] |--- [{}] addProperty: target = {}",
                                    className, value);
                            // aHttpClientBeanDefinition.getPropertyValues().add("target", value);
                            aHttpClientBeanDefinitionBuilder.addPropertyValue("target", value);
                            // 取到即退出循环
                            break;
                        }
                    }

                    // 属性: connectTimeout, readTimeout, retryTimes, retryInterval
                    for (String attrName : new String[] {
                            "connectTimeout", "readTimeout", "retryTimes", "retryInterval"}) {
                        String value = (String) aHttpClientAttributes.get(attrName);
                        if (StringUtils.isNotBlank(value)) {
                            value = environment.resolveRequiredPlaceholders(value);
                        }
                        // 这组属性, 本质上是int, 这里开始转换
                        if (StringUtils.isNotBlank(value)) {
                            int valueInt = Integer.parseInt(value);
                            log.info("[AHttpClientScannerRegistrar] |--- [{}] addProperty: {} = {}",
                                    className, attrName, valueInt);
                            // aHttpClientBeanDefinition.getPropertyValues().add(attrName, valueInt);
                            aHttpClientBeanDefinitionBuilder.addPropertyValue(attrName, valueInt);
                        }
                    }

                    // requestInterceptors
                    String[] requestInterceptors = (String[]) aHttpClientAttributes.get("requestInterceptors");
                    log.info("[AHttpClientScannerRegistrar] |--- [{}] addProperty: requestInterceptors = {}",
                                className, JsonUtil.toJson(requestInterceptors));
                    // aHttpClientBeanDefinition.getPropertyValues().add("requestInterceptors", requestInterceptors);
                    aHttpClientBeanDefinitionBuilder.addPropertyValue("requestInterceptors", requestInterceptors);
                }

                // 注册
                BeanDefinitionHolder holder = new BeanDefinitionHolder(
                        aHttpClientBeanDefinitionBuilder.getBeanDefinition(), beanName);
                BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

                log.info("[AHttpClientScannerRegistrar] |-- [{}] beanDefination created", className);
            }
        }
    }
}

AHttpClientFactoryBean工厂类

大概意思就是说,把@AHttpClient类加入到容器中,并且会把使用AHttpClientFactoryBean来构造一个BeanDefinitionBuilder,把属性注入进去,之后用户在Autowired引入接口类时,实际上就是返回AHttpClientFactoryBean的getObject()所返回的类,那我们来看看这个方法内有什么

public static class AHttpClientFactoryBean
            implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {

        // 一系列属性注入
        private boolean propsAllSet = false;

        private ApplicationContext applicationContext;

        private Class<?> type;

        private String target;

        private int connectTimeout = CONFIG_NOT_SET;

        private int readTimeout = CONFIG_NOT_SET;

        private int retryTimes = CONFIG_NOT_SET;

        private int retryInterval = CONFIG_NOT_SET;

        private String[] requestInterceptors;

        public Class<?> getType() {
            return type;
        }

        public void setType(Class<?> type) {
            log.info("[AHttpClientFactoryBean.setType] type = {}", type);
            this.type = type;
        }

        public void setTarget(String target) {
            log.info("[AHttpClientFactoryBean.setTarget] target = {}", target);
            this.target = target;
        }

        public void setConnectTimeout(int connectTimeout) {
            log.info("[AHttpClientFactoryBean.setConnectTimeout] connectTimeout = {}", connectTimeout);
            this.connectTimeout = connectTimeout;
        }

        public void setReadTimeout(int readTimeout) {
            log.info("[AHttpClientFactoryBean.setReadTimeout] readTimeout = {}", readTimeout);
            this.readTimeout = readTimeout;
        }

        public void setRetryTimes(int retryTimes) {
            log.info("[AHttpClientFactoryBean.setRetryTimes] retryTimes = {}", retryTimes);
            this.retryTimes = retryTimes;
        }

        public void setRetryInterval(int retryInterval) {
            log.info("[AHttpClientFactoryBean.setRetryInterval] retryInterval = {}", retryInterval);
            this.retryInterval = retryInterval;
        }

        public void setRequestInterceptors(String[] requestInterceptors) {
            log.info("[AHttpClientFactoryBean.setRequestInterceptors] requestInterceptors = {}",
                        JsonUtil.toJson(requestInterceptors));
            this.requestInterceptors = requestInterceptors;
        }

        @Override
        public Object getObject() throws Exception {
            // 通过HttpCallerBuilder创建接口代理, 并注册成bean放入spring容器中
            if (!propsAllSet) {
                throw new RuntimeException("[AHttpClientFactoryBean.getObject] props not set yet, can't build");
            }

            HttpCallerBuilder builder = HttpCallerBuilder.build();
            builder.setConnectTimeout(connectTimeout);
            builder.setReadTimeout(readTimeout);
            builder.setRetryTimes(retryTimes);
            builder.setRetryInterval(retryInterval);
            if (requestInterceptors != null) {
                for (String requestInterceptorBeanName : requestInterceptors) {
                    if (StringUtils.isNotBlank(requestInterceptorBeanName)) {
                        builder.addRequestInterceptor(applicationContext.getBean(
                                requestInterceptorBeanName, RequestInterceptor.class));
                    }
                }
            }

            return builder.target(type, target);   // 返回代理对象
        }

        @Override
        public Class<?> getObjectType() {
            return type;
        }

        @Override
        public void afterPropertiesSet() throws Exception {
            log.info("[AHttpClientFactoryBean.afterPropertiesSet] all set");
            propsAllSet = true;
        }

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

HTTP代理
可以看到最后会去执行builder.target(type, target);返回代理对象,执行动态代理的invoke方法,我们知道这个方法的参数是有Method的,我们可以获取这个上的@RequestMapping等的注解进行获取封装,最后会去调用到doGet或doPost方法进行方法的真正执行,这部分的代码比较长就不贴了。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 省略
}

原文内容来自

https://www.yuque.com/yinhe-dglmu/zye9qy/aofso8
有很多项目干货

总结

  1. 通过@EnableAHttpClient导入了AHttpClientScannerRegistrar,触发Spring应用程序对classpath中@AHttpClient修饰类的扫描。
  2. 解析到@AHttpClient修饰类后,通过扩展Spring Bean Definition的注册逻辑,最终注册一个AHttpClientFeactoryBean进入Spring容器。
  3. Spring容器在初始化其他用到@AHttpClient接口的类时,其实获得的是AHttpClientFactoryBean产生的一个代理对象Proxy。
  4. 基于Java原生的动态代理机制,针对Proxy的调用,都会统一转发给一个Invocationhandler,又该Handler完成后续的HTTP转换,发送,接收,HTTP响应的工作。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值