【RPC】手写简易 RPC 框架 --重构,实现与 Spring 整合

上一篇我们通过 BIO 实现了一个简易的 RPC 框架,但是,它还有很大的优化空间,本篇我们就对它进行改造 --把所有 bean 都交给 Spring 去管理。

我们先来看一下 version2 的框架结构:

在这里插入图片描述

注:如果相对于 version1 没有改变的类,我会在标题后标注上“未变”,看过 version1 的同学可以直接看变化的地方。

1.RpcRequest(未变)

RpcRequest 封装了消费者要调用方法的具体信息,是我们的自定义协议,或者说是消息格式。

涉及两个过程:

  • Consumer 编码:RpcRequest -> 数据流(二进制) => 告诉 Provider 要执行哪个方法
  • Provider 解码:数据流(二进制)-> RpcRequest => 获取 Consumer 要调用哪个方法
// 注:只有实现了序列化接口,才能实现远程传输
public class RpcRequest implements Serializable {

    private String className;  // 类(接口/服务)
    private String methodName;  // 方法
    private Object[] parameters;  // 参数

    public String getClassName() {
        return className;
    }

    public void setClassName(String className) {
        this.className = className;
    }

    public String getMethodName() {
        return methodName;
    }

    public void setMethodName(String methodName) {
        this.methodName = methodName;
    }

    public Object[] getParameters() {
        return parameters;
    }

    public void setParameters(Object[] parameters) {
        this.parameters = parameters;
    }
}

2.RpcService(新增)*

对于服务 Bean,我们不能像普通 bean 一样直接 @Compent,因为它的注解里面还应该包括更多信息,比如负载均衡策略,版本信息等,所以这里为所有的服务 bean 新定义一个注解。

@Target(ElementType.TYPE) // 用在类/接口
@Retention(RetentionPolicy.RUNTIME) // 运行时
@Component // 有该注解的类,会被Spring实例化,然后放入IOC容器
public @interface RpcService {

    // 记录要发布服务的接口
    Class<?> value();
}

注:接口信息其实可以在类上直接获取,这里只是为了模拟通过注解传递信息

通过注解标识要发布的服务,相比上一个版本,有了可扩展性,后期还可以增加更多属性。

3.RpcServer(修改)*

负责将请求派发给不同线程。

PS:引入 Spring 的意义就在于对 Bean 的管理(生老病死)更灵活,这里就是对服务 Bean 的管理更灵活

在 IOC 容器初始化时

  • 通过实现 ApplicationContextAware 扩展点,在对象初始后拿到注解的配置服务接口信息
  • 通过实现 InitializingBean 扩展点,在对象初始化后阻塞在这里接收请求
public class RpcServer implements ApplicationContextAware, InitializingBean {

    ExecutorService executorServic = Executors.newCachedThreadPool();

    // 存放接口名(服务名)与服务Bean 的对应关系
    private Map<String, Object> handlerMap = new HashMap<>();

    private int port;

    public RpcServer(int port) {
        this.port = port;
    }

    @Override
    /**
     * ApplicationContextAware 接口是 Spring 的一个扩展点
     * setApplicationContext() 在 Bean 初始化之后执行,可以获取到所有 Bean
     */
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 通过注解拿到服务Bean
        Map<String, Object> serviceBeanMap = applicationContext.getBeansWithAnnotation(RpcService.class);
        if (!serviceBeanMap.isEmpty()) {
            for (Object serviceBean : serviceBeanMap.values()) {
                RpcService rpcService = serviceBean.getClass().getAnnotation(RpcService.class);
                // 拿到服务名(接口名)
                String serviceName = rpcService.value().getName();
                // 放入容器
                handlerMap.put(serviceName, serviceBean);
            }
        }
    }

    @Override
    /**
     * InitializingBean 接口也是 Spring 的一个扩展点
     * afterPropertiesSet() 在 Bean 初始化后执行,在 setApplicationContext() 之后
     */
    public void afterPropertiesSet() throws Exception {

        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(port);
   
            // 注:因为当 bean 走到 afterPropertiesSet() 已经完成了初始化,
            //    所以,可以 while(true) 让 Provider 一直停在这个阶段
            while (true) {
                Socket socket = serverSocket.accept();
                // 这里是将handlerMap传入给具体的处理器,让处理器在其中拿到具体的服务Bean
                // 注:这里就不能像v1直接传入一个固定的service,而是要动态获取
                executorServic.execute(new ProcessorHandler(socket, handlerMap));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null){
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

注:这里并没有处理一个服务有多个实现的情况,对于一个服务多个实现可以参考 dubbo 的 SPI 自适应扩展点和激活扩展点的处理逻辑。

4.ProcessorHandler(修改)

Provider 线程的具体调用逻辑:

  1. 接收(解码):接收 Client 请求,将二进制流转换成 RpcRequest
  2. 执行:获取要执行的方法和参数,调用服务实现对象去执行方法。注意,与 version1 的区别就在这里,执行方法时多了一步在 handlerMap 中取出服务 bean。
  3. 发送(编码):将方法执行结果返回,将基本类型/Java对象转换成 二进制流

PS:由于这里采用的是BIO,并且传输时涉及到对象的编解码,所以

  • 可以统一调用 ObjectOutputStream#writeObject() 编码
  • 可以统一调用 ObjectInputStream#readObject() 解码,然后再将 Object 转换成包装类/普通Java对象
public class ProcessorHandler implements Runnable {

    private Socket socket;
    private Map<String, Object> handlerMap;
	
	// 相较于v1这里传入的不是一个具体的实例,而是所有的服务bean
    public ProcessorHandler(Socket socket,Map<String, Object> handlerMap) {
        this.socket = socket;
        this.handlerMap = handlerMap;
    }

    @Override
    public void run() {
        try {
            // Object~Stream也是包装类,其作用在于将字节流解析成java对象
            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            // 解析出具体的请求信息(RPCRequest)
            RpcRequest rpcRequest = (RpcRequest)objectInputStream.readObject();

            // 反射调用本地服务
            Object res = invoke(rpcRequest);

            // 将执行结果返回
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());
            objectOutputStream.writeObject(res);
            objectOutputStream.flush();  // 切记要flush手动刷新

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    // 通过反射具体执行provider提供的方法(即消费者要调用的方法)
    public Object invoke(RpcRequest request) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, ClassNotFoundException {
        // 首先通过handlerMap拿到具体的服务Bean
        Object service = handlerMap.get(request.getClassName());
        // 若无相应服务则报错
        if (service == null) {
            throw new RuntimeException("server not found:" + service);
        }

        // 根据实参获取形参列表
        // 注:获取形参列表后才能确定一个方法
        Object[] args = request.getParameters();
        Class<?>[] types = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            types[i] = args[i].getClass();
        }

        // 通过全类名拿到具体具体Class对象
        Class<?> clazz = Class.forName(request.getClassName());

        // 获取 Method
        Method method = clazz.getMethod(request.getMethodName(), types);
        
        // 执行方法
        // 注:service 这里是单例模式,但是也可以new一个对象后再执行具体方法
        Object res = method.invoke(service, args);

        return res;
    }
}

5.RpcProxyClient(未变)

代理对象,通过 JDK 动态代理生成一个代理对象

PS:这里创建一个代理对象是因为,服务的实现实例在 Provider,但 Consumer 调用服务的具体方法时也需要一个实例,而 Consumer 并没有这个实例。

public class RpcProxyClient {

    // 创建代理对象,代理的就是指定服务
    // 注:因为 Provider 发布时就是一个端口一个服务,所以这里代理的唯一标识就是 host:port
    public <T>T clientProxy(final Class<T> interfaceCls, final String host, final int port) {
        return (T)Proxy.newProxyInstance(interfaceCls.getClassLoader(), new Class[]{interfaceCls},
                new RemoteInvocationHandler(host, port));
    }

}

6.RemoteInvocationHandler(未变)

代理对象的具体逻辑,核心是 invoke 方法,当 Consumer 调用了服务的方法时,就会走到 invoke():

  1. 构建请求信息 RpcRequest
  2. 发送(编码):将 RpcRequest 转换成二进制流,发送
  3. 线程阻塞等待 Provider 处理结果
  4. 接收(解码):接收 Provider 的处理结果,将二进制流转换成基本类型/Java对象,并返回给上层函数

注:234步的逻辑都是网络 IO 相关,所以后面单独封装了一个 RpcNetTransport 类去实现

public class RemoteInvocationHandler implements InvocationHandler {

    private String host;
    private int port;

    public RemoteInvocationHandler(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Override
    // 所有请求都会进入这里
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 构建调用Provider的请求参数
        RpcRequest rpcRequest = new RpcRequest();
        rpcRequest.setClassName(method.getDeclaringClass().getName());
        rpcRequest.setMethodName(method.getName());
        rpcRequest.setParameters(args);

        // 进行远程调用,并返回执行结果
        RpcNetTransport netTransport = new RpcNetTransport(host, port);
        Object res = netTransport.send(rpcRequest);

        return res;
    }
}

7.RpcNetTransport(未变)

/**
 * 实现网络调用
 */
public class RpcNetTransport {

    private String host;
    private int port;

    public RpcNetTransport(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public Object send(RpcRequest request) {
        Socket socket = null;
        Object result = null;
        ObjectInputStream inputStream = null;
        ObjectOutputStream outputStream = null;

        try {
            // 将调用的服务的具体信息通过网络写出
            socket = new Socket(host, port);

            outputStream = new ObjectOutputStream(socket.getOutputStream());
            // writeObject实际上是一种序列化
            outputStream.writeObject(request);
            outputStream.flush();

            // 阻塞等待Provider的返回结果
            inputStream = new ObjectInputStream(socket.getInputStream());
            result = inputStream.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        return result;
    }

}

8.ProviderConfig(新增)*

JavaConfig

  1. 负责注册 RpcServer到 IOC 容器
  2. 负责扫描指定包下的所有 @Compent 注解(@RpcService)
@Configuration
@ComponentScan(basePackages = "com.xupt.yzh")
public class ProviderConfig {

    @Bean(name = "myRpcServer")
    public RpcServer rpcServer() {
        return new RpcServer(8080);
    }
}

注:这里是直接写死了要扫描的包,也可以再优化为通过配置。

9.ConsumerConfig(新增)

JavaConfig,负责注册 RpcProxyClient 到 IOC 容器。那么用户在获取代理时就直接从 IOC 容器获取了,不用再 new 了。

@Configuration
public class ConsumerConfig {

    @Bean(name = "rpcProxyClient")
    public RpcProxyClient proxyClient() {
        return new RpcProxyClient();
    }
}

结果测试

在这里插入图片描述

api

public interface TestService {

    String test(String name);
}

Provider

服务实现类(发布服务)

@RpcService(TestService.class)
public class TestServiceImpl implements TestService {

    @Override
    public String test(String name) {
        System.out.println("new requst coming..." + name);

        Random random = new Random();
        String json = "{\"name\":" + "\"" + name + "\"" + ", \"age\":" + random.nextInt(40) + "}";
        return json;
    }
}

启动 Provider:

public class Provider {

    public static void main(String[] args) {
		// 启动 IOC 容器
        new AnnotationConfigApplicationContext(ProviderConfig.class).start();
    }
}

Consumer

public class Consumer {

    public static void main(String[] args) {

        // 与v1相比,区别是通过IOC容器获取代理Bean
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConsumerConfig.class);
        RpcProxyClient rpcProxyClient = context.getBean(RpcProxyClient.class);

        TestService service = rpcProxyClient.clientProxy(TestService.class, "localhost", 8080);
        String json = service.test("张三");
        System.out.println(json);
    }
}

结果如下:

在这里插入图片描述

到此,将 RPC 框架中所有的 bean 交给 Spring 管理的升级优化就完成了!

完整代码我放到 GitHub 上了,有需要参考的同学点击这里跳转…

既然这么重要个改造都能完成,咱么再趁着首热再扩展一个功能 – 添加版本控制。

扩展:如何添加版本控制?

具体步骤我就不一一列代码出来了,同样是修改代码的几个地方,我都通过 todo 按顺序标示出来了:

在这里插入图片描述
在这里插入图片描述
这部分代码我也放到 GitHub 上,感性趣或者看不太明白的同学可以再看看代码,调试调试,点击这里跳转…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

A minor

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值