设计模式综合实战二:基于Netty实现RPC框架(代理+责任链+命令+装饰器)

说实话,RPC框架这个东西,很多同学觉得遥不可及。其实说白了,它就是个远程调用工具。你调一个本地方法,它帮你把参数打包发到另一台机器上执行,再把结果传回来。就这么简单。

我当年刚接触Netty时,第一反应是:这玩意儿跟设计模式有啥关系?后来真正动手写了一个RPC框架,才发现——嗯,设计模式不是用来背的,是用来解决问题的。

一、整体架构:四个模式如何协作

先看一张图,把四个模式的关系理清楚:

基于Netty的RPC框架 — 四种设计模式协作图 客户端 调用方 代理模式 JDK动态代理 命令模式 RpcRequest封装 责任链模式 ChannelPipeline 装饰器模式 ByteBuf包装 服务端 提供方 返回结果 图例: 模块 设计模式 调用流程 返回流程

这张图我画了好几次才满意。你看,客户端调用时先走代理模式,代理把调用封装成命令,命令通过责任链发送,中间用装饰器增强数据。服务端收到后反向走一遍。每个模式各司其职,互不干扰。

二、代理模式:让远程调用像本地一样

RPC最核心的体验是什么?就是让开发者感觉不到远程的存在。代理模式正好干这个活。

我习惯用JDK动态代理,因为接口是RPC的天然契约。来看代码:

public class RpcProxyFactory {
    
    @SuppressWarnings("unchecked")
    public static <T> T create(Class<T> interfaceClass, String host, int port) {
        return (T) Proxy.newProxyInstance(
            interfaceClass.getClassLoader(),
            new Class[]{interfaceClass},
            new RpcInvocationHandler(host, port)
        );
    }
    
    static class RpcInvocationHandler implements InvocationHandler {
        private final String host;
        private final int port;
        
        RpcInvocationHandler(String host, int port) {
            this.host = host;
            this.port = port;
        }
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // 1. 构建命令对象
            RpcRequest request = new RpcRequest();
            request.setClassName(method.getDeclaringClass().getName());
            request.setMethodName(method.getName());
            request.setParameterTypes(method.getParameterTypes());
            request.setParameters(args);
            
            // 2. 通过Netty发送
            RpcClient client = new RpcClient(host, port);
            RpcResponse response = client.send(request);
            
            // 3. 返回结果
            return response.getResult();
        }
    }
}

关键点:代理对象拦截所有方法调用,统一转成RpcRequest。调用方完全不知道底层走了网络。

我曾经遇到一个坑:代理类里忘了处理toString()hashCode()这些Object方法。结果调试时一打印代理对象,直接死循环了。嗯,后来加了个判断:如果是Object方法,直接调用目标对象。

三、命令模式:把调用封装成对象

RPC的本质是什么?就是把「方法调用」这个动作,变成一个可以在网络上传输的对象。这不就是命令模式吗?

命令模式把请求封装成对象,让请求可以参数化、排队、记录日志。在RPC里,这个对象就是RpcRequest

public class RpcRequest implements Serializable {
    private String requestId;      // 请求ID,用于异步回调
    private String className;      // 接口全限定名
    private String methodName;     // 方法名
    private Class<?>[] parameterTypes;  // 参数类型
    private Object[] parameters;   // 参数值
    
    // getter/setter 省略
}

public class RpcResponse implements Serializable {
    private String requestId;
    private Object result;
    private Throwable error;
    
    // getter/setter 省略
}

你想想看,如果没有命令模式,你怎么把方法调用序列化?难道把Method对象直接扔到网络上?那肯定不行。命令模式给了我们一个标准化的封装方式。

个人经验:requestId一定要加。我早期版本没加,结果并发请求一多,响应回来不知道对应哪个请求。后来用UUID生成requestId,配合ConcurrentHashMap做回调映射,问题就解决了。

四、责任链模式:Netty的ChannelPipeline

Netty最精彩的设计之一就是ChannelPipeline。它本质上就是个责任链模式。每个ChannelHandler处理自己的那部分逻辑,处理完传给下一个。

我一般会在Pipeline里加这么几个Handler:

public class RpcServerInitializer extends ChannelInitializer<SocketChannel> {
    
    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();
        
        // 1. 解码器:字节 -> RpcRequest
        pipeline.addLast(new RpcDecoder(RpcRequest.class));
        
        // 2. 编码器:RpcResponse -> 字节
        pipeline.addLast(new RpcEncoder(RpcResponse.class));
        
        // 3. 业务处理器:执行远程调用
        pipeline.addLast(new RpcServerHandler());
    }
}

每个Handler只关心一件事。解码器只管拆包解包,业务处理器只管反射调用。这就是责任链的魅力——解耦。

注意:Handler的顺序很重要。解码器必须在业务处理器之前,否则你拿到的是一堆乱码。我曾经调了半天发现顺序写反了,数据全乱了。

五、装饰器模式:增强ByteBuf

装饰器模式在Netty里无处不在。最典型的就是ByteBuf的包装。Netty提供了很多装饰器类,比如:

装饰器类 作用 使用场景
UnpooledHeapByteBuf 堆内存缓冲区 小数据量,GC友好
UnpooledDirectByteBuf 直接内存缓冲区 大数据量,避免拷贝
CompositeByteBuf 组合多个缓冲区 零拷贝合并数据
WrappedByteBuf 包装已有缓冲区 添加额外功能

我写RPC框架时,自定义了一个RpcByteBuf,在写入数据前自动压缩:

public class CompressByteBuf extends WrappedByteBuf {
    
    public CompressByteBuf(ByteBuf wrapped) {
        super(wrapped);
    }
    
    @Override
    public ByteBuf writeBytes(byte[] src) {
        try {
            byte[] compressed = compress(src);
            return super.writeInt(compressed.length)
                       .writeBytes(compressed);
        } catch (IOException e) {
            throw new RuntimeException("压缩失败", e);
        }
    }
    
    private byte[] compress(byte[] data) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (GZIPOutputStream gzip = new GZIPOutputStream(baos)) {
            gzip.write(data);
        }
        return baos.toByteArray();
    }
}

你看,装饰器模式的好处就是:不修改原有类,就能动态添加功能。想加压缩就包一层,想加加密再包一层。完全符合开闭原则。

六、完整调用流程

把四个模式串起来,整个RPC调用流程是这样的:

  1. 客户端通过代理模式获得接口的代理对象
  2. 调用方法时,代理把调用信息封装成命令对象(RpcRequest)
  3. 命令对象经过责任链(编码器、压缩装饰器等)处理
  4. 通过网络发送到服务端
  5. 服务端责任链反向处理(解压、解码)
  6. 服务端反射执行真实方法,返回结果
  7. 结果再经过装饰器责任链返回客户端

核心思想:每个模式解决一个特定问题。代理解决透明调用,命令解决请求封装,责任链解决处理流程,装饰器解决功能增强。四个模式各司其职,组合起来就是一个完整的RPC框架。

说实话,刚开始学设计模式时,我也觉得抽象。但真正动手写框架后才发现——这些模式不是凭空想出来的,是无数工程师踩坑踩出来的解决方案。你写代码时遇到类似问题,自然就会想到用对应的模式。

我建议你动手写一个mini版的RPC框架,不用太复杂,能调通一个接口就行。写完之后,你会发现设计模式不再是书本上的概念,而是你工具箱里的利器。


公众号:蓝海资料掘金营,微信deep3321