学习不用那么功利,二师兄带你从更高维度轻松阅读源码~

随着对Nacos源码的深入阅读,感觉越来越有意思了,大量的设计模式和基础知识点都在其中被运用。不论你是否阅读源码,都值得借鉴一下Nacos的运用案例。

今天这篇文章,给大家介绍一下Nacos Client中对代理模式的运用。阅读这篇文章,你可以不懂Nacos源码,但能够学到代理模式的运用;如果你准备阅读Nacos源码,不仅可以学到代理模式的案例,还可以更加深刻的感知到Nacos中的设计思想。

代理模式简介

通俗的来讲,代理模式就是让别人(代理)帮忙做你并不关心的事,作用就相当于日常生活中的中介。

比如,日常生活中,你想买辆车,你可以直接去自己挑选、质检等,但这个过程会耗费你大量的时间和精力。那么,此时你就可以找一个代理,来帮忙实现挑选、质检的事情。

对于软件设计来说,代理模式的定义为:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。

代理模式的结构

普通接口使用

在不使用代理模式时,我们大概是这样使用一个接口:

客户端在使用CarService接口时需要创建CarServiceImpl类的实例,然后进行业务逻辑处理。

但在某些场景下,一个客户类不想或者不能直接引用一个委托对象(CarServiceImpl),此时代理类对象可以在客户类和委托对象之间起到中介的作用,并提供相同的功能。

如果提供相同的功能,那么代理类和委托类就需要实现相同的接口。此时,上图就演变成了代理模式:

代理模式

在代理模式的图中,对比普通的直接使用,新增了代理类,并且代理类持有了委托类(真实对象)的引用。代理类本身并不真正实现服务,而是通过调用委托类的相关方法,来提供特定的服务,所以要持有真实类的引用。

代理类可以在业务功能执行的前后加入一些公共的服务,比如负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后对返回结果的处理等。

代理模式中的角色:

  • 抽象主题类(Subject):声明了目标对象和代理对象的共同接口,在任何可以使用目标对象的地方都可以使用代理对象。
  • 具体主题类(RealSubject):也称为委托角色或者被代理角色。定义了代理对象所代表的目标对象。
  • 代理类(Proxy):也叫委托类、代理类。代理对象内部含有目标对象的引用,从而可以在任何时候操作目标对象;代理对象提供一个与目标对象相同的接口,以便可以在任何时候替代目标对象。代理对象通常在客户端调用传递给目标对象之前或之后,执行某个操作,而不是单纯地将调用传递给目标对象。

代理模式实现

以上面的结构图为例,来看看代理模式的代码实现。

定义抽象主题类(CarService)、具体主题类(CarServiceImpl)、代理类(CarServiceProxy):

// 抽象主题类
public interface CarService {
    // 选车
    Car chooseCar();
    // 质量检查
    boolean qualityCheck();
}

// 具体主题类
public class CarServiceImpl implements CarService {
    @Override
    public Car chooseCar() {
        System.out.println("真实操作:选车");
        return new Car();
    }

    @Override
    public boolean qualityCheck() {
        System.out.println("真实操作:质量检测");
        return true;
    }
}

// 代理类
public class CarServiceProxy implements CarService {

    private CarServiceImpl real;

    public CarServiceProxy() {
        real = new CarServiceImpl();
    }

    @Override
    public Car chooseCar() {
        System.out.println("代理类CarServiceProxy选车:先添加一些日志");
        return real.chooseCar();
    }

    @Override
    public boolean qualityCheck() {
        System.out.println("代理类CarServiceProxy质量检测:先添加一些日志");
        return real.qualityCheck();
    }
}

对应的客户端测试类:

public class Client {

    public static void main(String[] args) {

        CarService carService = new CarServiceProxy();
        carService.chooseCar();
        carService.qualityCheck();
    }
}

直接使用代理类,就可以完成预期的工作。

执行程序,打印日志如下:

代理类CarServiceProxy选车:先添加一些日志
真实操作:选车
代理类CarServiceProxy质量检测:先添加一些日志
真实操作:质量检测

可以看出,在真实的操作之前,可以通过代理类添加一些其他的操作。

Nacos的代理模式实践

上面了解了代理模式的基本知识以及实例,下面就来看看Nacos中是如何实现代理模式的。

Nacos Client与注册中心进行通信采用了两种通信协议:HTTP协议和gRPC协议。这两个协议实现了共同的抽象主题类NamingClientProxy,具体主题类有NamingHttpClientProxy和NamingGrpcClientProxy,分别对应Http协议和gRPC协议实现。

此时,Nacos考虑到要支持通过配置来灵活选择具体的通信协议,而这个功能呢又没办法让这两个具体的主题类来实现,因此就产生了一个代理类NamingClientProxyDelegate来完成一些预先的处理和判断。

整个代理模式的使用类图如下:

代理模式

通过上图可以发现,Nacos的代理模式使用与标准的代理模式还有一些区别。

首先,NamingClientProxyDelegate同时代理了具体主题类,这可能考虑的是方便通信协议的配置切换。同时,在代理类中还处理了一些事件监听等额外功能。

其次,说话Nacos这块的命名并不友好,比如抽象主题直接以Proxy为后缀,容易让人混淆。这就导致与代理模式中的代理类命名冲突,于是将代理类的后缀替换为了Delegate。

上图中的客户类便是NacosNamingService,在其中实现了代理类的初始化操作,具体代码实现如下:

public class NacosNamingService implements NamingService {
    // ...
    private NamingClientProxy clientProxy;

    private void init(Properties properties) throws NacosException {
        // ...
        this.clientProxy = new NamingClientProxyDelegate(this.namespace, serviceInfoHolder, properties, changeNotifier);
    }

    @Override
    public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        NamingUtils.checkInstanceIsLegal(instance);
        clientProxy.registerService(serviceName, groupName, instance);
    }
    // ...
}  

抽象主题类NamingClientProxy为接口,部分代码如下:

public interface NamingClientProxy extends Closeable {

    void registerService(String serviceName, String groupName, Instance instance) throws NacosException;

    void deregisterService(String serviceName, String groupName, Instance instance) throws NacosException;

   // ...
}

代理类NamingClientProxyDelegate部分实现如下:

public class NamingClientProxyDelegate implements NamingClientProxy {
    // ...
    private final NamingHttpClientProxy httpClientProxy;

    private final NamingGrpcClientProxy grpcClientProxy;

    public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfoHolder, Properties properties,
            InstancesChangeNotifier changeNotifier) throws NacosException {
         // ...       
        this.httpClientProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, properties,
                serviceInfoHolder);
        this.grpcClientProxy = new NamingGrpcClientProxy(namespace, securityProxy, serverListManager, properties,
                serviceInfoHolder);
    }
  // ...
}

可以看出,代理类实现了NamingClientProxy接口,同时持有了NamingHttpClientProxy和NamingGrpcClientProxy的对象引用,并且对它们进行了初始化操作。

关于NamingHttpClientProxy和NamingGrpcClientProxy的代码我们就不再展示,它们首先继承了AbstractNamingClientProxy抽象类,该抽象类实现NamingClientProxy接口。

从整体上来说,Nacos中对代理模式的运用还是比较灵的,结合场景一个代理类代理了两个具体实现类,但同时在命名方面的问题,还有待商榷。

代理模式和装饰器模式的区别

在学习使用代理模式时,经常会有朋友与装饰器模式相混淆。这里就简单聊一下它们直接的区别。

装饰器模式中,装饰者(decorator)和被装饰者(decoratee)都实现同一个接口。代理模式中,代理类(proxy class)和真实处理的类(real class)都实现同一个接口。而且两者都对类的方法进行扩展,看起来边界的确比较模糊。

但还是有一些区别点的:

  • 装饰器模式强调的是增强自身,比如增加之后可提供更多的属性和方法;代理模式强调要让别人帮你去做一些本身与你业务没有太多关系的职责(记录日志、设置缓存)。代理模式是为了实现对象的控制,因为被代理的对象往往难以直接获得或者是其内部不想暴露出来。
  • 装饰模式是以对客户端透明的方式扩展对象的功能,是继承方案的一个替代方案;代理模式则是给一个对象提供一个代理对象,并由代理对象来控制对原有对象的引用;
  • 装饰模式是为装饰的对象增强功能;而代理模式对代理的对象施加控制,但不对对象本身的功能进行增强;

小结

代理模式在日常业务代码中还是比较少见的,本文我们重点介绍了静态代理模式及在Nacos中的运用。关于动态代理,在Spring的框架中可以看到很多实例,有机会我们再进行讲解。而Nacos中对代理模式的运用算是比较灵活,同时也并不是那么完美。这或许也提供了我们对代理模式认知的另外一个视角。

如果文章内容有问题或想技术讨论请联系我(微信:zhuan2quan,备注Nacos),如果觉得写的还不错,值得一起学习,那就关注一下吧。

Nacos系列文章

博主简介:《SpringBoot技术内幕》技术图书作者,酷爱钻研技术,写技术干货文章。

公众号:「程序新视界」,博主的公众号,欢迎关注~

技术交流:请联系博主微信号:zhuan2quan



《跟二师兄学Nacos吧》EXT-04篇 Nacos竟然是这样使用代理模式的?插图3

关注公众号:程序新视界,一个让你软实力、硬技术同步提升的平台

除非注明,否则均为程序新视界原创文章,转载必须以链接形式标明本文链接

本文链接:https://choupangxia.com/2021/07/28/nacosext-04/