首页 > 其他分享 >架构设计:系统间通信(17)——服务治理与Dubbo 中篇(分析)

架构设计:系统间通信(17)——服务治理与Dubbo 中篇(分析)

时间:2024-09-24 09:20:40浏览次数:11  
标签:架构设计 17 DUBBO 代理 接口 间通信 proxy BusinessInterface public

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析


阶段4、深入jdk其余源码解析


阶段5、深入jvm源码解析

码哥源码部分

码哥讲源码-原理源码篇【2024年最新大厂关于线程池使用的场景题】

码哥讲源码【炸雷啦!炸雷啦!黄光头他终于跑路啦!】

码哥讲源码-【jvm课程前置知识及c/c++调试环境搭建】

​​​​​​码哥讲源码-原理源码篇【揭秘join方法的唤醒本质上决定于jvm的底层析构函数】

码哥源码-原理源码篇【Doug Lea为什么要将成员变量赋值给局部变量后再操作?】

码哥讲源码【你水不是你的错,但是你胡说八道就是你不对了!】

码哥讲源码【谁再说Spring不支持多线程事务,你给我抽他!】

终结B站没人能讲清楚红黑树的历史,不服等你来踢馆!

打脸系列【020-3小时讲解MESI协议和volatile之间的关系,那些将x86下的验证结果当作最终结果的水货们请闭嘴】

2-5、设计模式:代理模式和JAVA对代理模式的支持

2-5-1、典型的代理模式

下面这个类图说明了“代理模式”的典型设计设计结构:

202307292151469581.png

典型的代理模式可用一句话进行概括:外部系统/外部模块要调用某个具体业务的实现A,不能直接进行实调用,而要通过一个代理对象进行间接的调用。典型的dialing模式中有四个角色:

  • Subject:业务接口定义。这个业务接口定义相关实现类的行为、事件等特性。
  • RealSubject:您可以看业务定义的真实实现。设计的原则是:无论何种情况下它并不知道自己被“代理”了。
  • Proxy:代理身份,帮助外部系统/外部模块完成具体业务实现A的调用。
  • Client:外部系统/外部模块。

接下来我们使用JAVA语言实现这个设计:

  • 业务接口定义(BusinessInterface):
    /**
     * 这是一个业务接口:给第三方模块调用的处理过程。
     * @author yinwenjie
     */
    public interface BusinessInterface {
        /**
         * @param username
         */
        public void dosomething(String username);
    }
  • 业务接口的真实实现类(RealBusinessImpl):
    /**
     * 这个类就是这个业务接口的真实实现。
     * @author yinwenjie
     */
    public class RealBusinessImpl implements BusinessInterface {
    
        /* (non-Javadoc)
         * @see testDesignPattern.proxy.BusinessInterface#dosomething(java.lang.String)
         */
        @Override
        public void dosomething(String username) { 
            // 这里偷懒了一下,没有在工程中导入log4j的依赖。用System.out进行显示
            System.out.println("正在为用户:" + username + ",进行真实的业务处理。。。");
        }
    }
  • 业务接口的代理实现类(ProxyBusinessImpl)
    package testDesignPattern.proxy.java;
    
    import testDesignPattern.proxy.BusinessInterface;
    import testDesignPattern.proxy.RealBusinessImpl;
    
    /**
     * 用java实现的传统的“代理模式”,有很多弊端。最大的弊端就是:<br>
     * 调用者必须清楚,自己将调用的某个对象需要被代理。。。。
     * @author yinwenjie
     */
    public class ProxyBusinessImpl implements BusinessInterface {
    
        /**
         * 真实的调用对象
         */
        private RealBusinessImpl realBusiness;
    
        public ProxyBusinessImpl(RealBusinessImpl realBusiness) {
            this.realBusiness = realBusiness;
        }
    
        /* (non-Javadoc)
         * @see testDesignPattern.proxy.BusinessInterface#dosomething(java.lang.String)
         */
        @Override
        public void dosomething(String username) {
            System.out.println("---------正式业务执行前;");
            this.realBusiness.dosomething(username);
            System.out.println("---------正式业务执行后;");
        }
    }
  • 运行起来(Main):
    package testDesignPattern.proxy.java;
    
    import testDesignPattern.proxy.BusinessInterface;
    import testDesignPattern.proxy.RealBusinessImpl;
    
    public class Main {
        public static void main(String[] args) throws RuntimeException {
            /*
             * 调用者必须知道,我要使用RealBusinessImpl具体的实现;
             * 必须使用ProxyBusinessImpl进行代理。
             * 
             * 这个做法写设计模式的实现实例倒还可以,没有什么实际意义
             * */
            RealBusinessImpl realBusiness = new RealBusinessImpl();
            BusinessInterface proxyBusinessInterface = new ProxyBusinessImpl(realBusiness);
    
            proxyBusinessInterface.dosomething("yinwenjie");
        }
    }

从以上代码的注解中,我们就可以发现典型代理模式的问题:调用者必须知道,我要使用RealBusinessImpl具体的实现是不被代理的,并且代理者还需要知道具体的代理者是谁。

2-5-2、JAVA支持的动态代理

为了解决这个明显的问题,聪明的程序员们发明出代理模式的变形设计——动态代理模式:在继承了代理模式优点的同时,通过动态代理模式第三方模块/系统并不需要知道“代理者”的实现细节了,并且代理者内部可以通过配置文件(或者其他导向性文件),对任何实现类进行代理(实际上这也是Spring框架的核心设计模式,Spring框架并不是这个系列博文的讲解返回,感兴趣的读者可以自行参考其源码)。下面我们来看看JAVA中对动态代理的支持:

  • 调用处理器:
    package testDesignPattern.proxy.dynamicjava;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    import testDesignPattern.proxy.BusinessInterface;
    
    /**
     * (代理)调用处理器。<br>
     * 什么意思呢:当“代理者”被调用时,这个实现类中的invoke方法将被触发。<br>
     * “代理者”对象,外部模块/外部系统所调用的方法名、方法中的传参信息都将以invoke方法实参的形式传递到方法中。
     * 
     * @author yinwenjie
     */
    public class BusinessInvocationHandler implements InvocationHandler {
    
        /**
         * 真实的业务处理对象
         */
        private BusinessInterface realBusiness;
    
        public BusinessInvocationHandler(BusinessInterface realBusiness) {
            this.realBusiness = realBusiness;
        }
    
        /* (non-Javadoc)
         * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("(代理)调用处理器被激活=====");
            System.out.println("“代理者对象”:" + proxy.getClass().getName());
            System.out.println("“外部模块/外部系统”调用的方法名:" + method.getName());
    
            System.out.println("---------正式业务执行前;");
            Object resultObject = method.invoke(this.realBusiness, args);
            System.out.println("---------正式业务执行后;");
    
            return resultObject;
        }
    
    }
  • 下面的代码说明了外部模块/外部系统如何进行调用:
    package testDesignPattern.proxy.dynamicjava;
    
    import java.lang.reflect.Proxy;
    
    import testDesignPattern.proxy.BusinessInterface;
    import testDesignPattern.proxy.RealBusinessImpl;
    
    public class Main {
        public static void main(String[] args) throws Exception {
            BusinessInterface realBusiness = new RealBusinessImpl();
            BusinessInvocationHandler invocationHandler = new BusinessInvocationHandler(realBusiness);
    
            /*
             * 生成一个动态代理实例。里面的三个参数需要讲解一下:
             * 1-loader:这个newProxyInstance会有一个返回值,即代理对象。
             * 那么问题就是类实例的创建必须要有classloader的支持,第一个参数就是指等“代理对象”的创建所依据的classloader
             * 
             * 2-interfaces:第二个参数是一个数组。在设计原理中,有一个重要的原则是“依赖倒置”,它的实践经验是:“依赖接口,而不是以来实现”。
             * 所以,JAVA中动态代理的支持假定程序员是遵循这一原则的:所有业务都定义的接口。这个参数就是为动态代理指定“代理对象所实现的接口”,
             * 由于JAVA中一个类可以实现多个接口,所以这个参数是一个数组(我的实例代码中,只为真实的业务实现定义了一个接口BusinessInterface,
             * 所以参数中指定的也就只有这个接口).另外,这个参数的类型是Class,所以如果您不定义接口,而是指定某个具体类,也是可行的。但是这不符合设计原则。
             * 
             * 3-InvocationHandler:这个就是我们的“调用处理器”,这个参数没有太多解释的
             * */
            BusinessInterface proxyBusiness = (BusinessInterface)Proxy.newProxyInstance(
                    Thread.currentThread().getContextClassLoader(), 
                    new Class[]{BusinessInterface.class}, 
                    invocationHandler);
    
            // 正式调用
            proxyBusiness.dosomething("yinwenjie");
        }
    }

正如代码中注释的说明Proxy.newProxyInstance方法有三个参数:

  • loader:这个newProxyInstance会有一个返回值,即代理对象。那么问题就是类实例的创建必须要有classloader的支持,第一个参数就是指等“代理对象”的创建所依据的classloader
  • interfaces:第二个参数是一个数组。在设计原理中,有一个重要的原则是“依赖倒置”,它的实践经验是:“依赖接口,而不是以来实现”。所以,JAVA中动态代理的支持假定程序员是遵循这一原则的:所有业务都定义的接口。这个参数就是为动态代理指定“代理对象所实现的接口”,由于JAVA中一个类可以实现多个接口,所以这个参数是一个数组(我的实例代码中,只为真实的业务实现定义了一个接口BusinessInterface。所以参数中指定的也就只有这个接口).另外,这个参数的类型是Class,所以如果您不定义接口,而是指定某个具体类,也是可行的。但是这不符合设计原则。
  • invocationHandler:这个就是我们的“调用处理器”,这个参数没有太多解释的

好的设计,遵从的原则之一:一种类型的问题,一定使用一种特定的设计来解决;绝对不会出现两种(或者是多种)解决方式。
——《架构之美》

3、DUBBO框架深入设计分析

202307292151487162.png

上图摘自DUBBO官网——技术手册(http://dubbo.io/Developer+Guide-zh.htm),可以肯定的是DUBBO官方技术手册上的技术细节介绍要比我本人文章中的技术细节的介绍详实得多。但是就像我前文说过的那样: 之所以介绍DUBBO框架不只是为读者介绍DUBBO服务治理框架本身,更重要的是通过这个系列文章的讲解向读者介绍整个系统间通信技术的知识层次。 而作为搭建在RPC要件之上的服务治理框架是又是这个知识体系中重要的一环,所以必须进行讲解。

在这个文章中,我还会有多出引用DUBBO官网的用户手册和技术手册。DUBBO团队对文档的维护是做得比较到位,一点是我非常钦佩的。我们先来看看DUBBO官方文档中,对于上图中各层的功能描述:

  • config:配置层,对外配置接口,以ServiceConfig,ReferenceConfig为中心,可以直接new配置类,也可以通过spring解析配置生成配置类。
  • proxy:服务代理层,服务接口透明代理,生成服务的客户端Stub和服务器端Skeleton,以ServiceProxy为中心,扩展接口为ProxyFactory。
  • registry:注册中心层,封装服务地址的注册与发现,以服务URL为中心,扩展接口为RegistryFactory,Registry,RegistryService。
  • cluster:路由层,封装多个提供者的路由及负载均衡,并桥接注册中心,以Invoker为中心,扩展接口为Cluster,Directory,Router,LoadBalance。
  • monitor:监控层,RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory,Monitor,MonitorService 。
  • protocol:远程调用层,封将RPC调用,以Invocation,Result为中心,扩展接口为Protocol, Invoker, Exporter。
  • exchange:信息交换层,封装请求响应模式,同步转异步,以Request, Response为中心,扩展接口为Exchanger,ExchangeChannel,ExchangeClient,ExchangeServer。
  • transport:网络传输层,抽象mina和netty为统一接口,以Message为中心,扩展接口为Channel,Transporter,Client,Server,Codec。
  • serialize:数据序列化层,可复用的一些工具,扩展接口为Serialization,ObjectInput,ObjectOutput,ThreadPool。

4、SPI和扩展点:

SPI:Service Provider Interface。在前文我们已经提到过,一个接口如果存在多个实现,那么我们必须依靠new关键字来告诉调用者这个接口的具体实现;用new关键的位置和时机都是非常重要的,因为这代表者调用者需要了解‘具体实现’;

前文还提到了Spring框架使用‘bean’配置关键字的形式帮我们解决了new关键字的问题,让调用者本身不需要关注所调用接口的具体实现。但是在和Spring框架相对独立的DUBBO框架中,如何达到这样的效果呢?

这里要进行一下说明:网上很多帖子提到DUBBO和Spring是可以无缝结合的,但是又没有分析DUBBO框架为什么可以和Spring框架无缝结合;这让很多读者认为BUDDO框架是基于Spring开发的。

但如果您研究过DUBBO的源代码(或者读过DUBBO相关技术文档),您就会发现。DUBBO和Spring完全是两个不同的技术组件,所谓无缝结合只是指DUBBO的service层、包括config层可以被Spring托管而已(实际上这和DUBBO框架的核心实现没有半毛钱关系);

但是这两个美丽的软件,采用的设计思路却是非常的一致 :教科书似的设计模式应用。

DUBBO框架扩展了(或者说另外实现了)基于标准JAVA的“服务自动发现”机制;为了说清楚DUBBO是如何找到某个内部接口的实现类的,我们首先就要讲清楚JAVA的SPI机制,并且再对DUBBO进行了哪些扩展进行一些必要的说明。

4-1、JAVA自带的SPI

对于JAVA中的接口和实现,我们一般情况下(或者说您在学习JAVA的时候),会采用如下的方式进行定义和使用(上文已经做了详细注释,这里的代码把注释精简了):

  • 业务接口定义(BusinessInterface):
    public interface BusinessInterface {
        public void dosomething(String username);
    }
  • 业务接口的真实实现类(RealBusinessImpl):
    public class RealBusinessImpl implements BusinessInterface {
        public void dosomething(String username) { 
            System.out.println("正在为用户:" + username + ",进行真实的业务处理。。。");
        }
    
        public static void main(String[] args) throws RuntimeException {
            BusinessInterface realBusiness = new RealBusinessImpl();
            realBusiness.dosomething("yinwenjie");
        }
    }

实际上,从JDK1.5版本开始,您无需使用new关键字指定具体的实现类。您可以在META-INF/searvices文件夹下建立一个名叫xxxx.BusinessInterface的文件(注意xxxx代表您的包名,整个文件名与BusinessInterface接口的完整类名相同),然后在文件内容中书写“xxxxx.RealBusinessImpl”(注意是完整BusinessInterface接口实现类的名字)。保存这个文件后,您就可以通过JDK提供的java.util.ServiceLoader工具类实例化这个接口了。代码片段如下:

    .....
    ServiceLoader<BusinessInterface> interface = ServiceLoader.load(BusinessInterface.class);  
    // 这样写的原因是,您可以一次指定这个接口的多个具体实现
    Iterator<BusinessInterface> iinterface= interface.iterator();  
    if (iinterface.hasNext()) {  
        BusinessInterface interfaceItem = iinterface.next();
        interfaceItem.dosomething("yinwenjie");
    }
    ...

4-2、DUBBO框架做的修改

在DUBBO框架中,主要作者william.liangf和ding.lid对JDK提供的SPI机制进行了修改(更准确的说法是“新建”):META-INF/dubbo文件夹下,使用K-V的方式描述要创建的具体类,这种方式在DUBBO框架中被称为“扩展点”。

DUBBO框架中对“扩展点”功能支持在com.alibaba.dubbo.common.extension包中,主要的类为ExtensionLoader。在这个包中,作者也对为什么要扩展JDK的SPI功能进行了说明:

202307292151589393.png

5、RPC模块

5-1、proxy:代理层

proxy代理层按照DUBBO官方文档的解释,是用来生成RPC调用的Stub和Skeleton,这样做的目的是让您在DUBBO服务端定义的具体业务实现不需要关心“它将被怎样调用”,也是您定义的服务接口“与RPC框架脱耦”。下图是DUBBO框架proxy层的主要类图结构:

202307292152000574.png

如上图所示(图有点小,请右键放大后查看),proxy层的AbstractProxyFactory类及其两个子类采用了我们上文讲到的典型抽象工厂模式进行设计;

那么AbstractProxyFactory下的工厂实现是如何工作的呢?这里我们以JavassistProxyFactory工厂进行讲解。因为在DUBBO接口ProxyFactory设置的默认extension(扩展点),就是对JavassistProxyFactory工厂进行实例化。

extension扩展点:
DUBBO框架内部的配置扩展信息,主要作用是通过JAVA注解形式告诉抽象类,应该实例化其下的哪一个实现。DUBBO扩展点配置文件的存储位置在jar包中的MATE-INF/dubbo/internal文件夹下。针对ProxyFactory接口来说,其扩展点配置文件为这个目录下的com.alibaba.dubbo.rpc.ProxyFactory文件;

上文中,我们提到过javassist这个组件的功能:在运行时动态加载class,并进行实例化。那么基于javassist的JavassistProxyFactory所提供的Invoker就是要完成这个工作,下面是JavassistProxyFactory中提供Invoker的代码片段:

    public <T> Invoker<T> getInvoker(T proxy, Class<T> type, URL url) {
        // TODO Wrapper类不能正确处理带$的类名
        final Wrapper wrapper = Wrapper.getWrapper(proxy.getClass().getName().indexOf('$') < 0 ? proxy.getClass() : type);
        return new AbstractProxyInvoker<T>(proxy, type, url) {
            @Override
            protected Object doInvoke(T proxy, String methodName, 
                                      Class<?>[] parameterTypes, 
                                      Object[] arguments) throws Throwable {
                return wrapper.invokeMethod(proxy, methodName, parameterTypes, arguments);
            }
        };
    }

(上面源码片段中的‘TODO’不是我加的,而是DUBBO主要作者之一的william.liangf加的)这里要说明一下DUBBO框架中里面common模块的com.alibaba.dubbo.common.bytecode.Wrapper类,这个类作为生成“运行时class代码”的工具类存在。主要的逻辑过程在:

    private static Wrapper makeWrapper(Class<?> c) {
        .......
    }

另外com.alibaba.dubbo.common.bytecode.Wrapper中常量:

    private static final Map<Class<?>, Wrapper> WRAPPER_MAP = new ConcurrentHashMap<Class<?>, Wrapper>();

很清楚的说明了Wrapper和被代理类之间的关系。

标签:架构设计,17,DUBBO,代理,接口,间通信,proxy,BusinessInterface,public
From: https://blog.csdn.net/smart_an/article/details/142473307

相关文章

  • 架构设计:系统间通信(19)——MQ:消息协议(上)
    作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬学习必须往深处挖,挖的越深,基础越扎实!阶段1、深入多线程阶段2、深入多线程设计模式阶段3、深入juc源码解析阶段4、深入jdk其余源码解析......
  • 架构设计:系统间通信(18)——服务治理与Dubbo 下篇(继续分析)
    作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬学习必须往深处挖,挖的越深,基础越扎实!阶段1、深入多线程阶段2、深入多线程设计模式阶段3、深入juc源码解析阶段4、深入jdk其余源码解析......
  • 20240910_021725 c语言 强制转换
    关于强转大转小就需要强转演练......
  • 20240910_031725 c语言 字符做加法
    ......
  • ADAU1701的Dynamics Processors算法补充例程合集(10个例程)
    作者的话做ADAU1701,心血来潮,再过了一遍SigmaDSP的算法合辑,发现有不少遗留的,比较有特点的算法,就在这个系列文章里一一呈现吧。ADAU1701我写了超过100个例程,但是都很早期,2018年开始弄的,我感觉并不是很全,那这一次就彻底把他补全一下,这个系列文章,将把我能够找到的,ADI原厂提供......
  • 17xx 物流查询平台 last-event-id 参数逆向分析
    声明本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作......
  • CF 1762 F
    考虑怎么不重不漏的计算每一个区间。可以发现,每一个可行的区间一定是可以找到\(i_1\simi_k\)使\(a_{i_1}\sima_{i_k}\)是单调不增或者不降的。这是因为,考虑有一个地方比两边都要小,那么我们可以直接忽略它,两边的差一定在\(k\)以内。比两边都大同理。因此我们现在就要算单......
  • `std::string_view`(c++17) 和 `std::stringstream` 使用区别·
    std::string_view和std::stringstream都是C++中处理字符串的工具,但它们的设计目标和使用场景非常不同。我们可以通过几方面进行对比。1.设计目的和核心功能std::string_view:设计用于只读访问字符串或字符序列。是一个轻量级的字符串视图,不会持有字符串的数据,仅仅是对......
  • 框架漏洞(5-rce s2-057 CVE-2017-8046 CVE-2018-1273 Shiro-550)
    5-rce步骤一:环境部署cdvulhub/thinkphp/5-rcedocker-composeup-d步骤二:输入系统命令: whoami/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami步骤三:写入webshell到1.php/index.php?s=index/think\ap......
  • <<编码>> 第 17 章 自动操作(1)--计算设备 示例电路
    info::操作说明首先闭合清零(clear)开关,清除8位触发器(锁存器)中的内容,并把16位计数器的输出置为0000H然后闭合RAM控制面板的控制端开关(接管信号开关)手动输入一组要加的数注:RAM地址0,1,2,3处已预设了1,2,3,4四个值断开接管控制开......