课程总结与项目实战:综合运用三种模式设计一个简易的RPC框架核心模块
好,终于到了最后一章。说实话,每次讲到这里,我都有点感慨。前面我们花了大量篇幅,把单例、工厂、观察者这三种模式掰开揉碎了讲。但模式这东西,光背定义没用,你得真刀真枪地用一回,才能体会到它的妙处。
这一章,我们就来干一件实在的事——用这三种模式,搭一个简易RPC框架的核心模块。别被“RPC”三个字吓到,说白了就是让一个程序能像调用本地方法一样,调用另一台机器上的方法。我们只做最核心的部分:服务注册、服务发现、客户端调用。
整体架构:三种模式各司其职
先看看整体设计。我画了一张图,帮你理清思路。
从图上你能看到,三种模式分别负责不同的事情:
- 单例模式:保证注册中心全局只有一个实例,避免数据混乱
- 工厂模式:统一创建服务实例和客户端代理对象,隐藏创建细节
- 观察者模式:服务上下线时通知客户端,实现动态感知
第一步:用单例模式实现注册中心
注册中心是整个RPC框架的大脑。所有服务启动后,都要来这里报到。如果注册中心有多个实例,那客户端就不知道该信谁了。
我个人习惯用枚举方式实现单例,简单又安全。来看看代码:
public enum RegistryCenter {
INSTANCE;
// 服务名 -> 服务地址列表
private final ConcurrentHashMap<String, List<String>> services = new ConcurrentHashMap<>();
public void register(String serviceName, String address) {
services.computeIfAbsent(serviceName, k -> new CopyOnWriteArrayList<>())
.add(address);
System.out.println("服务注册成功: " + serviceName + " @ " + address);
}
public List<String> discover(String serviceName) {
return services.getOrDefault(serviceName, Collections.emptyList());
}
public void unregister(String serviceName, String address) {
List<String> list = services.get(serviceName);
if (list != null) {
list.remove(address);
}
}
}
这里有个细节要注意。我用的是 CopyOnWriteArrayList,而不是普通的 ArrayList。为什么?因为注册中心可能被多个线程同时访问,写操作虽然少,但读操作极其频繁。CopyOnWriteArrayList 在遍历时不需要加锁,性能更好。
第二步:用工厂模式创建服务和代理
服务端要暴露服务,客户端要调用服务。这两件事都涉及对象的创建。如果到处 new,后期维护起来会非常痛苦。工厂模式就是用来解决这个问题的。
我设计了一个抽象工厂,既能创建服务端实例,也能创建客户端代理:
public interface RpcFactory {
<T> T createService(Class<T> serviceInterface, T serviceImpl);
<T> T createProxy(Class<T> serviceInterface, String remoteAddress);
}
public class DefaultRpcFactory implements RpcFactory {
@Override
public <T> T createService(Class<T> serviceInterface, T serviceImpl) {
// 将服务实现注册到注册中心
String serviceName = serviceInterface.getName();
RegistryCenter.INSTANCE.register(serviceName, "localhost:8080");
return serviceImpl;
}
@Override
@SuppressWarnings("unchecked")
public <T> T createProxy(Class<T> serviceInterface, String remoteAddress) {
// 使用JDK动态代理创建远程调用代理
return (T) Proxy.newProxyInstance(
serviceInterface.getClassLoader(),
new Class<?>[]{serviceInterface},
new RpcInvocationHandler(remoteAddress)
);
}
}
你看,客户端拿到的其实是一个代理对象。调用方法时,代理会帮你把参数序列化、发网络请求、等结果返回。这些脏活累活,工厂模式帮你封装得干干净净。
第三步:用观察者模式实现动态感知
服务不可能永远在线。某个服务挂了,或者新加了一个节点,客户端怎么知道?轮询当然可以,但效率太低。观察者模式就是干这个的。
我设计了一个简单的事件总线:
public class ServiceEventBus {
private final List<ServiceChangeListener> listeners = new CopyOnWriteArrayList<>();
public void subscribe(ServiceChangeListener listener) {
listeners.add(listener);
}
public void unsubscribe(ServiceChangeListener listener) {
listeners.remove(listener);
}
public void publish(ServiceEvent event) {
for (ServiceChangeListener listener : listeners) {
listener.onServiceChanged(event);
}
}
}
public class ServiceEvent {
private String serviceName;
private String address;
private EventType type; // ADD, REMOVE
// getter / setter 略
}
public interface ServiceChangeListener {
void onServiceChanged(ServiceEvent event);
}
客户端只需要实现 ServiceChangeListener 接口,然后订阅事件总线。当有服务上线或下线时,注册中心会调用 publish 方法,所有订阅者都会收到通知。
我曾经在一个微服务项目里用过类似的机制。当时有个服务节点频繁抖动,每次上下线都要手动改配置。后来用观察者模式做了自动感知,运维同学终于不用半夜爬起来改配置文件了。
unsubscribe 方法。
整合:让三种模式协同工作
好了,三个核心组件都搭好了。现在把它们串起来,看看完整的调用流程:
- 服务端启动:通过工厂模式创建服务实例,同时注册到单例的注册中心
- 注册中心通知:通过观察者模式,广播服务上线事件
- 客户端订阅:收到通知后,通过工厂模式创建远程代理对象
- 客户端调用:调用代理方法,代理内部完成网络传输和结果返回
- 服务端响应:处理请求,返回结果
你看,三种模式各司其职,又互相配合。单例保证了数据的全局一致性,工厂统一了对象的创建逻辑,观察者实现了动态的变更通知。缺一个,整个框架就会变得别扭。
避坑指南:我踩过的几个坑
最后,分享几个我在实际项目中踩过的坑,希望能帮你少走弯路:
- 单例的序列化问题:如果注册中心需要持久化或网络传输,记得实现
readResolve方法,否则反序列化时会破坏单例 - 工厂的扩展性:不要把工厂做成万能类。我见过有人把数据库连接、日志、缓存全塞到一个工厂里,结果工厂代码上千行。建议按职责拆分
- 观察者的异常处理:一个监听器抛异常,会不会影响其他监听器?我建议在
publish方法里加 try-catch,保证每个监听器独立运行
好了,课程到这里就结束了。三种模式,一个框架,希望能让你对设计模式的理解从“知道”变成“会用”。代码这东西,光看没用,动手写一遍才是自己的。找个周末,照着这个思路自己搭一遍,相信我,收获会很大。