专栏首页微观技术框架扩展:注解 RPC Consumer属性动态注入

框架扩展:注解 RPC Consumer属性动态注入

分布式系统架构时代,RPC框架你一定不会陌生。目前主流的RPC框架有 dubbo、thrift、motan、grpc等。

消费端(RPC Consumer)通常只有服务接口定义,接口的业务逻辑实现部署在生产端(RPC Provider),服务调用一般是采用动态代理方式,通过Proxy创建一个代理类,借助增强方式完成网络的远程调用,获取执行结果。

这里有两个关键点:

1、如何实现一个通用的代理类?

2、如何在消费端动态注入接口的代理对象?

如何实现一个通用的代理类?

目前动态代理的实现方案有很多种,如JDK 动态代理、Cglib、Javassist、ASM、Byte Buddy等

JDK 动态代理的代理类是运行时通过字节码生成的,我们通过Proxy.newProxyInstance方法获取的接口实现类就是这个字节码生成的代理类

定义代理类RpcInvocationHandler,继承InvocationHandler接口,并重写invoke()方法。

public class RpcInvocationHandler implements InvocationHandler {

    private final String serviceVersion;
    private final long timeout;

    public RpcInvocationHandler(String serviceVersion, long timeout) {
        this.serviceVersion = serviceVersion;
        this.timeout = timeout;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        // todo
        // 1、封装RpcProtocol对象
        // 2、对象编码
        // 3、发送请求到服务端
        // 4、获取返回结果

        // 模拟生成一个订单号
        Long orderId = Long.valueOf(new Random().nextInt(100));

        String s = String.format("【RpcInvocationHandler】 调用方法:%s , 参数:%s ,订单id:%d", method.getName(), JSON.toJSONString(args), orderId);
        System.out.println(s);
        return orderId;
    }

如何在消费端动态注入接口的代理对象?

构造一个自定义Bean,并对该Bean下执行的所有方法拦截,增加额外处理逻辑。

OrderService是一个订单接口类,client端没有该接口的实现类。

定义注解@RpcReference,用于描述代理类的参数信息。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Autowired
public @interface RpcReference {
    String serviceVersion() default "1.0";
    long timeout() default 5000;
}

Spring 的 FactoryBean 接口可以帮助我们实现自定义的 Bean,FactoryBean 是一种特殊的工厂 Bean,通过 getObject() 方法返回对象,而并不是 FactoryBean 本身。

public class RpcReferenceBean implements FactoryBean<Object> {

    private Class<?> interfaceClass;

    private String serviceVersion;

    private long timeout;

    private Object object;

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

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

    public void init() throws Exception {
        object = Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass},
                new RpcInvocationHandler(serviceVersion, timeout));
    }
}

但是 RpcReferenceBean如何被spring容器识别并加载呢?需要借助Spring的其他扩展点:

1、BeanFactoryPostProcessor,在Spring 容器加载 Bean 的定义之后以及 Bean 实例化之前执行,方便用户对 Bean 的配置元数据进行二次修改。

2、ApplicationContextAware,通过它Spring容器会自动把上下文环境对象调用ApplicationContextAware接口中的setApplicationContext方法。通过ApplicationContext可以查找Spring容器中的Bean对象。

3、BeanClassLoaderAware, 获取 Bean 的类加载器

public class RpcConsumerPostProcessor implements ApplicationContextAware, BeanClassLoaderAware, BeanFactoryPostProcessor {

    private ApplicationContext applicationContext;
    private ClassLoader classLoader;
    private final Map<String, BeanDefinition> rpcBeanDefinitions = new LinkedHashMap<>();

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

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

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName);
            String beanClassName = beanDefinition.getBeanClassName();
            if (beanClassName != null) {
                Class<?> clazz = ClassUtils.resolveClassName(beanClassName, this.classLoader);
                ReflectionUtils.doWithFields(clazz, this::parseRpcReference);
            }
        }

        // 自定义bean注册到容器中
        BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
        this.rpcBeanDefinitions.forEach((beanName, beanDefinition) -> {
            if (applicationContext.containsBean(beanName)) {
                throw new IllegalArgumentException("spring context already has a bean named " + beanName);
            }
            registry.registerBeanDefinition(beanName, beanDefinition);
            System.out.println(String.format("registered RpcReferenceBean %s success!", beanName));
        });
    }

    private void parseRpcReference(Field field) {
        RpcReference annotation = AnnotationUtils.getAnnotation(field, RpcReference.class);
        if (annotation != null) {
            // 创建RpcReferenceBean类的Bean定义
            BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(RpcReferenceBean.class);
            beanDefinitionBuilder.setInitMethodName("init");
            beanDefinitionBuilder.addPropertyValue("interfaceClass", field.getType());
            beanDefinitionBuilder.addPropertyValue("serviceVersion", annotation.serviceVersion());
            beanDefinitionBuilder.addPropertyValue("timeout", annotation.timeout());

            BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition();
            rpcBeanDefinitions.put(field.getName(), beanDefinition);
        }
    }

}

RpcConsumerPostProcessor 从 beanFactory 中获取所有 Bean 的定义信息,然后对每个Bean下的field检测,如果field被声明了@RpcReference注解,通过BeanDefinitionBuilder重新构造RpcReferenceBean的定义,并为成员变量赋值。

最后借助BeanDefinitionRegistry将新定义的Bean重新注册到Spring容器中。由容器来实例化Bean对象,并完成IOC依赖注入

代码地址

https://github.com/aalansehaiyang/spring-boot-example/tree/master/spring-rpc-reference

本文分享自微信公众号 - 微观技术(weiguanjishu),作者:TomGE

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-01-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 函数式编程是如何提升代码的扩展性

    从工程学角度来讲,我们常说的软件工程一般采用面向对象编程,差别在与使用的编程语言不同,有人习惯用java,有人喜欢C#,各有各的特色,除了语法上略有差异,其本质...

    用户7676729
  • ​Proxy系统架构升级

    爬虫是个高风险行业,如果操作不当,很容易造成“攻击”假象,给数据源技术部门带来很多麻烦。另外随着大家的安全意识逐步提高,风控标准也越来越严。为了避免身份被识别,...

    用户7676729
  • Dubbo框架常见问题

    在分布式架构时代,dubbo 作为RPC框架,以其高性能、易扩展、配置简单、易上手被越来越多的公司所青睐,在国内互联网公司中口碑一直很好。因为其高频使用,很多面...

    用户7676729
  • 使用Redis实现延时任务(二)

    前一篇文章通过Redis的有序集合Sorted Set和调度框架Quartz实例一版简单的延时任务,但是有两个相对重要的问题没有解决:

    Throwable
  • Rust学习:如何解读函数签名?

    在Rust中,函数签名类似“讲故事”。经验丰富的Rust程序员,只需浏览一个函数的签名,就可以知道该函数大部分的行为。

    MikeLoveRust
  • 关于MCU产品开发参数存储的几种方案(开源项目持续收集整理中)

    在工作中,凡是涉及到产品开发几乎都会实现参数存储功能,一般参数存储会采用如下的存储介质进行,如:eeprom、spi flash、nand flash、SD卡等...

    morixinguan
  • ASP.NET MVC扩展库

    很多同学都读过这篇文章吧 ASP.NET MVC中你必须知道的13个扩展点,今天给大家介绍一个ASP.NET MVC的扩展库,主要就是针对这些扩展点进行。这个项...

    张善友
  • SaaS更要稳住

    ? 来源:ToBeSaaS  作者 :戴珂  ---- ? 国内SaaS的又一春,来了? 一天晚上,忽然收到好几条微信,内容是同一个:“知道吗?Sales...

    腾讯SaaS加速器
  • 清华大学研究团队提出基于忆阻器阵列的新型脑机接口

    脑机接口技术作为信息科学与神经科学等多学科交叉融合的前沿领域,在康复医学、医疗电子等领域得到了广泛关注与应用。来自清华大学的科研团队提出基于忆阻器阵列的新型脑机...

    脑机接口社区
  • Visual Studio 2008 每日提示(十三)

    #121、如何设置vs启动时(工作区)加载内容 原文链接:How to customize what Visual Studio opens to 操作步骤:...

    Jianbo

扫码关注云+社区

领取腾讯云代金券