手写RPC框架系列(三) - 在RPC框架中实现代理模式
手写RPC框架系列(三) - 在RPC框架中实现代理模式
本章节将主要探讨以下几个内容:
代理模式简介
为什么要使用代理模式
理解静态代理和动态代理(JDK动态代理和Cglib动态代理)
通过例子感受代理模式的作用
在RPC框架中实现代理模式
1. 代理模式简介
代理模式 (英语: Proxy Pattern) 是一种常见的设计模式,是前人总结出来的软件开发中的问题解决方案。在日常开发中比较常见的应用场景就是在不修改目标对象的情况下,对目标对象做功能拓展,增强其功能。
我们常用的 Spring Aop 中就是大量使用了动态代理,可以说代理模式在日常开发过程中是到处都存在的,它并不是一个比较实用的设计模式。在我过往的项目经验中,代理模式主要是用来对原来的功能扩展额外的功能,同时又不用去修改原来的功能代码。可以通过代理类将请求转发给委托类的前后,添加一些额外的操作,比如请求过滤、记录日志、缓存等。
2. 为什么需要使用代理模式
在 RPC 框架中使用代理模式,主要有以下原因:
- RPC 调用实际上是通过网络进行的,需要处理网络连接、数据序列化、异常处理等细节。
- 客户端只需要跟代理对象交互,无需关注底层网络操作细节,降低开发跟使用的复杂性。
- 可以通过代理模式添加负载均衡、失败重试、超时处理等功能。
- 可以定义各种钩子函数在发送请求前后。
- 客户端只需要关注业务逻辑,其他的脏活累活由代理类去实现。
3. 理解代理模式
Java 中的代理模式分为静态代理以及动态代理两种。
静态代理需要为每个原始类手动编写代理类,缺乏通用性和灵活性。
JDK动态代理基于接口实现,要求原始类必须实现接口。利用反射在运行时动态创建代理类。
Cglib动态代理基于继承实现,不要求原始类实现接口。通过操作字节码在运行时动态创建代理类,性能优于JDK动态代理,但调试更困难。
3.1 静态代理
就跟它名字一样,它运用的时候是静态的,不能通用处理同一场景的问题。本质上,就是对原方法的一个包裹,在代理方法中会去调用原始对象的相应方法。
public interface OrderService{
public List<OrderData> selectOrdersByUid(String userId);
}
public class OrderServiceImpl implement OrderService {
public List<OrderData> selectOrdersByUid(String userId){
// 查询数据库,再返回结果
final List<OrderData> result = queryDbUserOrders(userId);
return result;
}
}
public class OrderServiceProxy implement OrderService {
private OrderService originService;
pulic void OrderServiceProxy(OrderService originService){
this.originService = originService;
}
public List<OrderData> selectOrdersByUid(String userId){
long startTime = currentTime();
// 调用原对象中的函数
final List<OrderData> result = this.originService.selectOrdersByUid(userId);
long endTime = currentTime();
long consumeTime = endTime - startTime;
System.out.println("总耗时为" + consumeTime);
return res;
}
}
//调用
final OrderService proxy = new OrderServiceProxy(new OrderServiceImpl());
final List<OrderData> orderList = proxy.selectOrdersByUid(1);
3.2 动态代理
3.2.1 JDK 动态代理
JDK 动态代理是利用了 JDK 中的 API,可以在运行时动态创建代理类。 JDK 动态代理接口是基于接口的,所以原始类必须实现一个接口( 这也算是一个缺点 )。
// ..... OrderService 以及 OrderService 的代码同上不变
public class ProxyHelper {
private Object originObject;
public ProxyFactory(Object originObject) {
this.originObject = originObject;
}
// 为目标对象生成代理对象
public Object getProxyInstance() {
return Proxy.newProxyInstance(originObject.getClass().getClassLoader(), originObject.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = currentTime();
Object returnValue = method.invoke(target, args);
long endTime = currentTime();
long consumeTime = endTime - startTime;
System.out.println("总耗时为" + consumeTime);
return returnValue;
}
});
}
}
// 调用代理后的方法
final OrderService target = new OrderServiceImpl();
final OrderService proxy = (OrderService) new ProxyFactory(target).getProxyInstance();
final List<OrderData> orderList = proxy.selectOrdersByUid(1);
使用动态代理后具备通用性,假如 OrderService
又加了一个新函数 selectOrdersByName()
,那么还是可以复用该代理类进行耗时统计。
final OrderService target = new OrderServiceImpl();
final OrderService proxy = (OrderService) new ProxyFactory(target).getProxyInstance();
final List<OrderData> orderList = proxy.selectOrdersByName("admin");
3.2.2 Cglib 动态代理
上面的代理方式都是 JDK 提供的代理模式,可以看出都是需要通过接口的方式,代理去生成一个子类进行功能拓展。而 cglib 则没有这种限制,它是直接通过操作字节码来达到增强功能的。使用 Cglib 去实现的最大优点是没有实现接口的类也可以进行增强。
// ..... OrderService 以及 OrderService 的代码同上不变
public class CglibProxyHelper implements MethodInterceptor{
private Object originObject;
public CglibProxyHelper(Object originObject) {
this.originObject = originObject;
}
//生成代理对象
public Object getProxyInstance() {
Enhancer en = new Enhancer();
en.setSuperclass(originObject.getClass());
en.setCallback(this);
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
long startTime = currentTime();
Object returnValue = method.invoke(originObject, args);
long consumeTime = endTime - startTime;
System.out.println("总耗时为" + consumeTime);
return returnValue;
}
}
// 调用
DaoService target = new DaoServiceImpl();
DaoService proxy = (DaoService) new CglibProxyHelper(target).getProxyInstance();
Map<String,String> res = proxy.selectById(1);
两种动态代理的优缺点对比如下:
- Cglib 代理不需要接口,JDK 动态代理需要原始类实现接口。
- Cglib 代理性能更高,因为 JDK 动态代理是通过反射去调用原始类的方法。
- Cglib 提供更多的钩子函数,可以运行在更多的场景中,实现更复杂的功能。
- Cglib 调试更加困难,不如 JDK 动态代理简洁明了。
4. 通过例子感受代理模式的作用
通过一个简单的例子来感受代理模式在实际项目中的运用,假设我们有个订单相关的服务接口,接口中提供了一个通过用户 ID 查询该用户所有订单信息的方法。
public interface OrderService{
public List<OrderData> selectOrdersByUid(String userId);
}
public class OrderServiceImpl implement OrderService {
public List<OrderData> selectOrdersByUid(String userId){
// 查询数据库,再返回结果
final List<OrderData> result = queryDbUserOrders(userId);
return result;
}
}
在生产环境中偶发性的发现该方法执行过慢,我们希望打印该方法的执行时间,以提供依据来排查问题。常规的做法会有以下几种:
- 直接修改
OrderServiceImpl
中的代码,加入耗时统计部分逻辑。
public class OrderServiceImpl implement OrderService {
public List<OrderData> selectOrdersByUid(String userId){
long startTime = currentTime();
// 查询数据库,再返回结果
final List<OrderData> result = queryDbUserOrders(userId);
long endTime = currentTime();
long consumeTime = endTime - startTime;
System.out.println("总耗时为" + consumeTime);
return result;
}
}
// 调用
OrderService orderService = new OrderServiceImpl();
List<OrderData> orderList = orderService.selectOrdersByUid(1);
- 使用代理模式去增强,不修改原代码,下面为伪代码:
public class ProxyHelper {
// 生成代理对象
public Object getProxyInstance(Object target) {
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = currentTime();
Object returnValue = method.invoke(target, args);
long endTime = currentTime();
long consumeTime = endTime - startTime;
System.out.println("总耗时为" + consumeTime);
return returnValue;
}
});
}
}
// 调用代理后的方法
OrderService target = new OrderServiceImpl();
OrderService proxy = (OrderService) new ProxyFactory(target).getProxyInstance();
List<OrderData> orderList = proxy.selectOrdersByUid(1);
对比两种方式:
- 方法一比较简单,实现起来也快,但是需要动原先的代码。 这种情况下如果是自己写的代码那么改起来比较方便,如果是开源框架中的代码,则无法用方法一去统计耗时。
- 方法二比较复杂一点,运用了 Java 中的动态代理机制,可以使用在不同的场景中。基于这种方式去扩展,可以抽取为公共的组件提供给所有的方法去使用。
上面的例子是项目实际开发过程中会碰到的真实问题,只是场景会比这个更加复杂,但是基本的套路跟原理是类似的。
5. 实现代理模式
在 RPC 框架中,我们使用动态代理技术,在客户端未远程服务接口生成代理对象,从而实现透明的远程方法调用。
我们在 rpc-core 模块下,新建包 com.suny.rpc.nettyrpc.core.client
, 在包中新建 Java 类 RpcClientProxy
,然后实现 InvocationHandler
接口,实现目前先置空,后续我们将补充方法中的逻辑。
package com.suny.rpc.nettyrpc.core.client;
import com.suny.rpc.nettyrpc.core.model.RpcRequest;
import com.suny.rpc.nettyrpc.core.model.RpcResponse;
import com.suny.rpc.nettyrpc.core.network.RpcRequestSender;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.UUID;
/**
* 客户端代理
*/
@Slf4j
public class RpcClientProxy implements InvocationHandler {
public <T> T getProxy(Class<T> clazz) {
return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class<?>[]{clazz}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 发送请求给远程服务接口,然后获取应答结果
}
}
6. 总结
代理模式在RPC框架中扮演重要角色,利用代理模式可以大大降低RPC框架的开发和使用复杂度,同时提供灵活的可扩展性。