课程总结与项目实战:综合运用三种模式设计一个简易的RPC框架核心模块

好,终于到了最后一章。说实话,每次讲到这里,我都有点感慨。前面我们花了大量篇幅,把单例、工厂、观察者这三种模式掰开揉碎了讲。但模式这东西,光背定义没用,你得真刀真枪地用一回,才能体会到它的妙处。

这一章,我们就来干一件实在的事——用这三种模式,搭一个简易RPC框架的核心模块。别被“RPC”三个字吓到,说白了就是让一个程序能像调用本地方法一样,调用另一台机器上的方法。我们只做最核心的部分:服务注册、服务发现、客户端调用。

整体架构:三种模式各司其职

先看看整体设计。我画了一张图,帮你理清思路。

简易RPC框架核心模块架构图 服务端 服务实例 UserService / OrderService 服务注册中心 单例模式(Registry) 网络监听器 观察者模式(事件通知) 注册 注册中心 服务名 → 地址映射 单例模式保证全局唯一 工厂模式创建服务 观察者模式通知变更 发现 客户端 服务代理 工厂模式创建代理对象 调用处理器 序列化 / 网络传输 结果回调 观察者模式处理异步响应 三种设计模式在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 在遍历时不需要加锁,性能更好。

我的经验: 曾经在一个高并发项目里,我用了同步的 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)
        );
    }
}

你看,客户端拿到的其实是一个代理对象。调用方法时,代理会帮你把参数序列化、发网络请求、等结果返回。这些脏活累活,工厂模式帮你封装得干干净净。

核心思想: 工厂模式在这里扮演了“统一入口”的角色。无论是服务端注册,还是客户端代理创建,都走同一个工厂。以后想换实现(比如从JDK代理换成CGLIB),只需要改工厂内部代码,调用方完全无感知。

第三步:用观察者模式实现动态感知

服务不可能永远在线。某个服务挂了,或者新加了一个节点,客户端怎么知道?轮询当然可以,但效率太低。观察者模式就是干这个的。

我设计了一个简单的事件总线:

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 方法,所有订阅者都会收到通知。

我曾经在一个微服务项目里用过类似的机制。当时有个服务节点频繁抖动,每次上下线都要手动改配置。后来用观察者模式做了自动感知,运维同学终于不用半夜爬起来改配置文件了。

注意: 观察者模式虽然好用,但要注意内存泄漏问题。如果客户端订阅了事件总线,但忘记取消订阅,服务端会一直持有客户端的引用,导致客户端无法被GC回收。我建议在客户端关闭时,显式调用 unsubscribe 方法。

整合:让三种模式协同工作

好了,三个核心组件都搭好了。现在把它们串起来,看看完整的调用流程:

  1. 服务端启动:通过工厂模式创建服务实例,同时注册到单例的注册中心
  2. 注册中心通知:通过观察者模式,广播服务上线事件
  3. 客户端订阅:收到通知后,通过工厂模式创建远程代理对象
  4. 客户端调用:调用代理方法,代理内部完成网络传输和结果返回
  5. 服务端响应:处理请求,返回结果

你看,三种模式各司其职,又互相配合。单例保证了数据的全局一致性,工厂统一了对象的创建逻辑,观察者实现了动态的变更通知。缺一个,整个框架就会变得别扭。

避坑指南:我踩过的几个坑

最后,分享几个我在实际项目中踩过的坑,希望能帮你少走弯路:

  • 单例的序列化问题:如果注册中心需要持久化或网络传输,记得实现 readResolve 方法,否则反序列化时会破坏单例
  • 工厂的扩展性:不要把工厂做成万能类。我见过有人把数据库连接、日志、缓存全塞到一个工厂里,结果工厂代码上千行。建议按职责拆分
  • 观察者的异常处理:一个监听器抛异常,会不会影响其他监听器?我建议在 publish 方法里加 try-catch,保证每个监听器独立运行

好了,课程到这里就结束了。三种模式,一个框架,希望能让你对设计模式的理解从“知道”变成“会用”。代码这东西,光看没用,动手写一遍才是自己的。找个周末,照着这个思路自己搭一遍,相信我,收获会很大。