代理模式:RPC框架的前世今生

74 阅读8分钟

5.3 代理模式:RPC框架的前世今生

开场白:从“媒婆”到“跨服聊天”

话说,古代程序员小A想调用远在千里之外的小B写的函数,怎么办?总不能“飞鸽传书”吧?于是,一种神奇的“媒婆”模式诞生了——这就是代理模式的雏形。

在现代分布式系统中,RPC(Remote Procedure Call,远程过程调用)框架就像一位神通广大的“媒婆”,让我们可以像调用本地方法一样,调用远程服务。而代理模式,正是RPC框架实现“跨服聊天”的关键。

代理模式的核心思想:控制访问 + 功能增强

代理模式的核心思想很简单:为某个对象(被代理对象)提供一个代理,以控制对这个对象的访问。同时,代理对象可以在调用前后进行额外的处理,实现功能增强。

就好比明星都有经纪人(代理),粉丝(客户端)不能直接联系明星(被代理对象),必须通过经纪人。经纪人可以过滤掉不靠谱的合作(控制访问),也可以安排行程、宣传造势(功能增强)。

RPC框架中的代理:动态代理的舞台

在RPC框架中,我们通常使用动态代理来实现客户端对远程服务的透明调用。动态代理的妙处在于,它不需要为每个接口都手动创建一个代理类,而是在运行时动态生成代理对象。

Java提供了两种动态代理方式:

  1. JDK动态代理:基于接口实现,要求被代理对象必须实现至少一个接口。
  2. CGLIB动态代理:基于继承实现,通过生成被代理类的子类来创建代理对象。

动手实践:手写一个极简RPC框架(基于JDK动态代理)

为了让你更深入地理解代理模式在RPC框架中的应用,我们来手写一个极简的RPC框架。

1. 定义服务接口

// 服务接口
public interface HelloService {
    String sayHello(String name);
}

2. 服务端实现

// 服务端实现
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String name) {
        return "Hello, " + name + "!";
    }
}

3. 客户端代理生成器

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 客户端代理生成器
public class RpcClientProxy {

    public static <T> T getProxy(Class<T> interfaceClass, String host, int port) {
        return (T) Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 1. 封装请求参数
                        RpcRequest request = new RpcRequest();
                        request.setInterfaceName(method.getDeclaringClass().getName());
                        request.setMethodName(method.getName());
                        request.setParameterTypes(method.getParameterTypes());
                        request.setParameters(args);

                        // 2. 通过网络发送请求(这里简化为直接调用)
                        //    实际场景中,会使用Netty等框架进行网络通信
                        //    这里假设服务端已经启动,并监听指定端口
                        //    RpcResponse response = sendRequest(request, host, port);

                        // 3. 模拟网络调用,直接返回结果(简化处理)
                        if (method.getName().equals("sayHello")) {
                            return "Hello, " + args[0] + "!";
                        }
                        return null;
                    }
                }
        );
    }

    // 模拟网络请求(实际场景中会使用Netty等框架)
    private static RpcResponse sendRequest(RpcRequest request, String host, int port) {
        // ... 网络通信代码 ...
        return null; // 简化处理
    }
}

// 请求对象
class RpcRequest {
    private String interfaceName;
    private String methodName;
    private Class<?>[] parameterTypes;
    private Object[] parameters;

    // getter/setter省略
	public String getInterfaceName() {
		return interfaceName;
	}
	public void setInterfaceName(String interfaceName) {
		this.interfaceName = interfaceName;
	}
	public String getMethodName() {
		return methodName;
	}
	public void setMethodName(String methodName) {
		this.methodName = methodName;
	}
	public Class<?>[] getParameterTypes() {
		return parameterTypes;
	}
	public void setParameterTypes(Class<?>[] parameterTypes) {
		this.parameterTypes = parameterTypes;
	}
	public Object[] getParameters() {
		return parameters;
	}
	public void setParameters(Object[] parameters) {
		this.parameters = parameters;
	}
}

// 响应对象
class RpcResponse {
    private Object result;
    private Throwable exception;
	public Object getResult() {
		return result;
	}
	public void setResult(Object result) {
		this.result = result;
	}
	public Throwable getException() {
		return exception;
	}
	public void setException(Throwable exception) {
		this.exception = exception;
	}

    // getter/setter省略
}

4. 客户端调用

// 客户端调用
public class Client {
    public static void main(String[] args) {
        // 获取代理对象
        HelloService helloService = RpcClientProxy.getProxy(HelloService.class, "localhost", 8080);

        // 通过代理对象调用远程方法
        String result = helloService.sayHello("World");
        System.out.println(result); // 输出:Hello, World!
    }
}

代码解读:

  • RpcClientProxy.getProxy()方法是核心,它使用Proxy.newProxyInstance()动态创建代理对象。
  • InvocationHandlerinvoke()方法拦截了所有方法调用。
  • invoke()方法中,我们封装了请求参数(方法名、参数类型、参数值等),并通过网络发送给服务端(这里简化为直接返回结果)。
  • 客户端通过代理对象调用远程方法,就像调用本地方法一样,无需关心底层通信细节。

流程图:

sequenceDiagram
    participant Client
    participant Proxy
    participant Server

    Client->>Proxy: 调用sayHello("World")
    activate Proxy
    Proxy->>Proxy: 封装请求参数
    Proxy->>Server: 发送网络请求(简化为直接返回)
    activate Server
    Server-->>Proxy: 返回结果
    deactivate Server
    Proxy-->>Client: 返回结果
    deactivate Proxy

面试真题 & 答案

  1. 什么是代理模式?它有什么优点?

    • 答案: 代理模式为其他对象提供一种代理以控制对这个对象的访问。
    • 优点:
      • 职责清晰: 将客户端与目标对象解耦,降低系统耦合度。
      • 保护目标对象: 代理对象可以过滤请求,保护目标对象。
      • 增强功能: 代理对象可以在调用前后进行额外处理,实现功能增强。
  2. JDK动态代理和CGLIB动态代理有什么区别?

    • 答案:
      • JDK动态代理: 基于接口实现,要求被代理对象必须实现至少一个接口。
      • CGLIB动态代理: 基于继承实现,通过生成被代理类的子类来创建代理对象。如果目标对象没有实现接口,则可以使用CGLIB。
      • 性能: JDK动态代理在生成代理类时效率较高,但在调用方法时可能稍慢;CGLIB在生成代理类时稍慢,但在调用方法时可能更快。
  3. RPC框架中为什么要使用代理模式?

    • 答案:
      • 透明性: 让客户端像调用本地方法一样调用远程服务,屏蔽底层通信细节。
      • 解耦: 将客户端与服务端解耦,方便服务的升级和替换。
      • 功能增强: 可以在代理对象中实现负载均衡、服务发现、熔断降级等功能。
  4. 手写一个简单的RPC框架,你会考虑哪些方面?

    • 答案:
      • 服务注册与发现: 如何让客户端知道服务端的位置?
      • 序列化与反序列化: 如何将对象转换为字节流进行传输?
      • 网络通信: 如何高效地进行网络数据传输?(如使用Netty)
      • 协议设计: 如何定义请求和响应的格式?
      • 负载均衡: 如何将请求分发到多个服务端实例?
      • 容错处理: 如何处理网络异常、服务端宕机等情况?
  5. 除了RPC框架,代理模式还有哪些应用场景?

    • 答案:
      • Spring AOP: 基于动态代理实现面向切面编程。
      • MyBatis: 使用动态代理实现Mapper接口与SQL语句的映射。
      • 延迟加载: 如Hibernate中的延迟加载,先返回一个代理对象,在真正需要数据时才去数据库查询。
      • 安全代理: 控制对敏感对象的访问权限。
      • 缓存代理: 缓存方法的执行结果,避免重复计算。
  6. 动态代理的InvocationHandler接口中,invoke方法的三个参数分别是什么?

    • 答案:
      • proxy:代理对象本身。通常情况下,在invoke方法中不会使用该对象,主要是防止循环调用。
      • method:被调用的方法对象。通过该对象可以获取方法名、参数类型、注解等信息。
      • args:方法调用时传入的参数数组。
  7. 如果让你设计一个RPC框架的序列化机制,你会选择哪种序列化协议,为什么?

    • 答案:
      • 选项:
        • Java原生序列化: 优点是使用简单,缺点是性能较差,且不支持跨语言。
        • JSON: 优点是可读性好,跨语言支持好,缺点是序列化后的体积较大,性能一般。
        • Protobuf: 优点是性能高,序列化后的体积小,跨语言支持好,缺点是需要定义IDL文件。
        • Thrift: 类似于Protobuf,也是一种高性能、跨语言的序列化协议。
        • Avro: 优点是性能高,支持动态模式,适用于数据变化频繁的场景。
      • 选择:
        • 如果追求极致性能和跨语言支持,可以选择Protobuf或Thrift。
        • 如果对可读性有要求,或者数据结构变化频繁,可以选择JSON或Avro。
        • 如果是纯Java项目,且对性能要求不高,也可以选择Java原生序列化。
  8. 在RPC框架中,如果服务端处理请求超时了,客户端应该如何处理?

    • 答案:
      • 设置超时时间: 客户端在发起请求时,应该设置一个合理的超时时间。
      • 超时重试: 如果在超时时间内没有收到响应,可以进行有限次数的重试。
      • 熔断降级: 如果重试多次仍然失败,可以触发熔断机制,暂时停止对该服务的调用,并返回一个默认值或错误信息。
      • 异步调用: 可以使用异步调用的方式,避免阻塞当前线程。
      • 监控告警: 应该对超时事件进行监控和告警,及时发现和解决问题。

总结

代理模式是RPC框架的基石,它让远程调用像本地调用一样简单。通过动态代理,我们可以实现客户端的透明调用,并在代理对象中实现各种增强功能。掌握代理模式,是理解RPC框架原理的关键一步。