七的博客

手写RPC框架系列(三) - 在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;
    }
}

在生产环境中偶发性的发现该方法执行过慢,我们希望打印该方法的执行时间,以提供依据来排查问题。常规的做法会有以下几种:

  1. 直接修改 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);
   
  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框架的开发和使用复杂度,同时提供灵活的可扩展性。