5.3 代理模式:RPC框架的前世今生
开场白:从“媒婆”到“跨服聊天”
话说,古代程序员小A想调用远在千里之外的小B写的函数,怎么办?总不能“飞鸽传书”吧?于是,一种神奇的“媒婆”模式诞生了——这就是代理模式的雏形。
在现代分布式系统中,RPC(Remote Procedure Call,远程过程调用)框架就像一位神通广大的“媒婆”,让我们可以像调用本地方法一样,调用远程服务。而代理模式,正是RPC框架实现“跨服聊天”的关键。
代理模式的核心思想:控制访问 + 功能增强
代理模式的核心思想很简单:为某个对象(被代理对象)提供一个代理,以控制对这个对象的访问。同时,代理对象可以在调用前后进行额外的处理,实现功能增强。
就好比明星都有经纪人(代理),粉丝(客户端)不能直接联系明星(被代理对象),必须通过经纪人。经纪人可以过滤掉不靠谱的合作(控制访问),也可以安排行程、宣传造势(功能增强)。
RPC框架中的代理:动态代理的舞台
在RPC框架中,我们通常使用动态代理来实现客户端对远程服务的透明调用。动态代理的妙处在于,它不需要为每个接口都手动创建一个代理类,而是在运行时动态生成代理对象。
Java提供了两种动态代理方式:
- JDK动态代理:基于接口实现,要求被代理对象必须实现至少一个接口。
- 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()动态创建代理对象。InvocationHandler的invoke()方法拦截了所有方法调用。- 在
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
面试真题 & 答案
-
什么是代理模式?它有什么优点?
- 答案: 代理模式为其他对象提供一种代理以控制对这个对象的访问。
- 优点:
- 职责清晰: 将客户端与目标对象解耦,降低系统耦合度。
- 保护目标对象: 代理对象可以过滤请求,保护目标对象。
- 增强功能: 代理对象可以在调用前后进行额外处理,实现功能增强。
-
JDK动态代理和CGLIB动态代理有什么区别?
- 答案:
- JDK动态代理: 基于接口实现,要求被代理对象必须实现至少一个接口。
- CGLIB动态代理: 基于继承实现,通过生成被代理类的子类来创建代理对象。如果目标对象没有实现接口,则可以使用CGLIB。
- 性能: JDK动态代理在生成代理类时效率较高,但在调用方法时可能稍慢;CGLIB在生成代理类时稍慢,但在调用方法时可能更快。
- 答案:
-
RPC框架中为什么要使用代理模式?
- 答案:
- 透明性: 让客户端像调用本地方法一样调用远程服务,屏蔽底层通信细节。
- 解耦: 将客户端与服务端解耦,方便服务的升级和替换。
- 功能增强: 可以在代理对象中实现负载均衡、服务发现、熔断降级等功能。
- 答案:
-
手写一个简单的RPC框架,你会考虑哪些方面?
- 答案:
- 服务注册与发现: 如何让客户端知道服务端的位置?
- 序列化与反序列化: 如何将对象转换为字节流进行传输?
- 网络通信: 如何高效地进行网络数据传输?(如使用Netty)
- 协议设计: 如何定义请求和响应的格式?
- 负载均衡: 如何将请求分发到多个服务端实例?
- 容错处理: 如何处理网络异常、服务端宕机等情况?
- 答案:
-
除了RPC框架,代理模式还有哪些应用场景?
- 答案:
- Spring AOP: 基于动态代理实现面向切面编程。
- MyBatis: 使用动态代理实现Mapper接口与SQL语句的映射。
- 延迟加载: 如Hibernate中的延迟加载,先返回一个代理对象,在真正需要数据时才去数据库查询。
- 安全代理: 控制对敏感对象的访问权限。
- 缓存代理: 缓存方法的执行结果,避免重复计算。
- 答案:
-
动态代理的
InvocationHandler接口中,invoke方法的三个参数分别是什么?- 答案:
proxy:代理对象本身。通常情况下,在invoke方法中不会使用该对象,主要是防止循环调用。method:被调用的方法对象。通过该对象可以获取方法名、参数类型、注解等信息。args:方法调用时传入的参数数组。
- 答案:
-
如果让你设计一个RPC框架的序列化机制,你会选择哪种序列化协议,为什么?
- 答案:
- 选项:
- Java原生序列化: 优点是使用简单,缺点是性能较差,且不支持跨语言。
- JSON: 优点是可读性好,跨语言支持好,缺点是序列化后的体积较大,性能一般。
- Protobuf: 优点是性能高,序列化后的体积小,跨语言支持好,缺点是需要定义IDL文件。
- Thrift: 类似于Protobuf,也是一种高性能、跨语言的序列化协议。
- Avro: 优点是性能高,支持动态模式,适用于数据变化频繁的场景。
- 选择:
- 如果追求极致性能和跨语言支持,可以选择Protobuf或Thrift。
- 如果对可读性有要求,或者数据结构变化频繁,可以选择JSON或Avro。
- 如果是纯Java项目,且对性能要求不高,也可以选择Java原生序列化。
- 选项:
- 答案:
-
在RPC框架中,如果服务端处理请求超时了,客户端应该如何处理?
- 答案:
- 设置超时时间: 客户端在发起请求时,应该设置一个合理的超时时间。
- 超时重试: 如果在超时时间内没有收到响应,可以进行有限次数的重试。
- 熔断降级: 如果重试多次仍然失败,可以触发熔断机制,暂时停止对该服务的调用,并返回一个默认值或错误信息。
- 异步调用: 可以使用异步调用的方式,避免阻塞当前线程。
- 监控告警: 应该对超时事件进行监控和告警,及时发现和解决问题。
- 答案:
总结
代理模式是RPC框架的基石,它让远程调用像本地调用一样简单。通过动态代理,我们可以实现客户端的透明调用,并在代理对象中实现各种增强功能。掌握代理模式,是理解RPC框架原理的关键一步。