2023-07-29
原文作者:说好不能打脸 原文地址:https://yinwj.blog.csdn.net/article/details/49453303

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和被代理类之间的关系。

阅读全文