设计模式综合实战二:基于Netty实现RPC框架(代理+责任链+命令+装饰器)
说实话,RPC框架这个东西,很多同学觉得遥不可及。其实说白了,它就是个远程调用工具。你调一个本地方法,它帮你把参数打包发到另一台机器上执行,再把结果传回来。就这么简单。
我当年刚接触Netty时,第一反应是:这玩意儿跟设计模式有啥关系?后来真正动手写了一个RPC框架,才发现——嗯,设计模式不是用来背的,是用来解决问题的。
一、整体架构:四个模式如何协作
先看一张图,把四个模式的关系理清楚:
这张图我画了好几次才满意。你看,客户端调用时先走代理模式,代理把调用封装成命令,命令通过责任链发送,中间用装饰器增强数据。服务端收到后反向走一遍。每个模式各司其职,互不干扰。
二、代理模式:让远程调用像本地一样
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调用流程是这样的:
- 客户端通过代理模式获得接口的代理对象
- 调用方法时,代理把调用信息封装成命令对象(RpcRequest)
- 命令对象经过责任链(编码器、压缩装饰器等)处理
- 通过网络发送到服务端
- 服务端责任链反向处理(解压、解码)
- 服务端反射执行真实方法,返回结果
- 结果再经过装饰器和责任链返回客户端
核心思想:每个模式解决一个特定问题。代理解决透明调用,命令解决请求封装,责任链解决处理流程,装饰器解决功能增强。四个模式各司其职,组合起来就是一个完整的RPC框架。
说实话,刚开始学设计模式时,我也觉得抽象。但真正动手写框架后才发现——这些模式不是凭空想出来的,是无数工程师踩坑踩出来的解决方案。你写代码时遇到类似问题,自然就会想到用对应的模式。
我建议你动手写一个mini版的RPC框架,不用太复杂,能调通一个接口就行。写完之后,你会发现设计模式不再是书本上的概念,而是你工具箱里的利器。
公众号:蓝海资料掘金营,微信deep3321