项目3:从0开始的RPC框架

一. 基本概念

区别于传统的增删改查型的业务项目,本项目侧重于开发框架,并且涉及架构方面的技术知识点。

1. 什么是RPC?

远程过程调用(Remote Procedure Call),是一种计算机通信协议,它允许程序在不同的计算机之间交互通信,以实现本地调用的效果。

类似于如今的外卖平台,外卖平台出现之前消费者需要到线下店铺购买,耗时耗力。现在通过外卖平台,只需要下单选择商品,无需关心数据在网络中如何传输的、外卖平台是怎么转发的、骑手是如何配送的等事情,等待商品即可。

进一步说明:可以让客户端在不清楚调用细节的情况下,实现对远程计算机上某个方法或服务的调用,就像调用本地方法一样。

RPC既然是一种计算机协议,那么就需要开发者去遵循这套规范进行实现,目前市面上常见的RPC框架有:Dubbo、GRPC等。

2. 为什么需要RPC?

在现实场景中,随着企业中业务量和应用功能模块的增多,单机部署运行已经无法满足需求,可能需要将业务或功能模块拆分到不同的机器和服务器上,以减轻压力,但有些功能是通用的,因此可以将这些通用的模块划分成公共模块,组成独立的一部分。

RPC就允许一个程序(服务消费者)像调用自己的本地方法一样去调用这个公共模块的接口(服务提供者),不需要了解数据的传输处理过程、底层网络通信的细节等。RPC已经帮助做完了这些事情。

举例:

项目A提供了点餐服务,项目B需要调用点餐服务完成下单。

//伪代码
interface OrderService{
     //点餐,返回orderId
     long order(参数1,参数2,参数3);
}

如果没有RPC,并且由于项目A和项目B都是独立的系统,所以不能像SDK一样作为依赖包直接引入。那么就需要项目A提供Web服务,同时编写一个点餐服务接口提供给外界,比如访问http://xxx.com 就能调用点餐服务。之后项目B作为服务消费者,需要自己构造请求,并通过HttpClient请求上述地址。理论上来说,如果B需要多个第三方服务,那么每个服务和方法的调用都需要编写一个HTTP请求,耗时耗力。

//伪代码
url = "http://xxx.com"
req = new Req(参数1,参数2,参数3)
res = httpClient.post(url).body(req).execute()
orderId = res.data.orderId

而通过RPC框架,项目B可以像调用本地方法一样完成调用,一行代码即可解决。

//伪代码
orderId = orderService.order(参数1,参数2,参数3)

 

二. 简易版RPC框架

1. 设计流程

(1)现在有一个消费者和一个服务提供者。

(2)消费者想要调用服务,需要提供者启动一个web服务,然后消费者通过请求客户端发送HTTP请求或其它协议的请求来调用。比如请求xxx.com/order地址后,就会调用提供者的order方法。

(3) 但如果提供者提供了多个服务和方法,每个服务和方法都要单独写一个接口,消费者想调用的话需要对每个接口都写一段HTTP调用逻辑。效率很低。

因此,可以提供一个统一的服务调用接口,通过请求处理器根据客户端的请求参数来调用不同的方法。同时在服务提供者程序中维护一个本地服务注册器,用于记录服务和对应实现类的映射。

此外,由于Java对象无法直接在网络中传输,因此需要对传递的参数进行序列化和反序列化。

例如:消费者想要调用orderService接口的order方法,发送请求,参数为service=orderService,method=order,然后请求处理器会根据serv从服务注册器中找到对应的服务实现类,并通过Java的反射机制调用method指定的方法。

(4)再进一步,为了简化消费者发送请求的代码,实现类似本地的一行调用。可以基于代理模式,为消费者要调用的接口生成一个代理对象,由代理对象完成请求和响应的过程。

至此,简易版RPC框架完成:

 网上的一个关于RPC响应流程的图(参考):

 2. 构造初始项目

(1)项目初始化

简易版RPC框架目录:

  • common模块包含示例代码的公共依赖,比如接口、Model等。
  • consumer模块包含示例消费者代码。
  • provider模块包含例服务提供者代码。
  • khr-rpc-easy模块是简易版RPC框架。

简易版RPC框架侧重于整个调用流程的实现。

(2)common模块

公共模块需要同时被消费者和服务提供者引入,主要包含和服务相关的接口和数据模型代码。

用户实体类User:

package com.khr.example.common.model;

import java.io.Serializable;

/**
 * 用户
 */

// 继承Serializable,用于指示该类实例可以被序列化和反序列化。
// 这意味着User对象可以在程序之间进行持久化存储或在网络通信中传输。
public class User implements Serializable{

    private String name;

    public  String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }
}

用户接口服务UserService,提供一个获取用户的方法:

package com.khr.example.common.service;

import com.khr.example.common.model.User;

/**
 * 用户服务
 */
public interface UserService {
    /**
     * 获取用户
     *
     * @param user
     * @return
     */
    User getUser(User user);

}

(3)provider模块

服务提供者是真正实现了接口的模块。

先引入hutool、lombok依赖。

服务实现类UserServiceImpl:

实现公共模块中定义的用户接口服务UserService,功能是打印用户名称并返回参数中的用户对象。

package com.khr.example.provider;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;


/**
 * 用户服务实现类
 */
public class UserServiceImpl implements UserService {

    public User getUser(User user){
        System.out.println("用户名:"+ user.getName());
        return user;
    }
}

服务提供者启动类EasyProviderExample:

提供服务的代码之后再补充。

package com.khr.example.provider;

import com.khr.example.common.service.UserService;
import com.khr.krpc.server.HttpServer;
import com.khr.krpc.server.VertxHttpServer;
import com.khr.krpc.registry.LocalRegistry;

/**
 * 简易服务提供者示例
 */
public class EasyProviderExample {

    public static void  main(String[] args){
        //提供服务
        
    }
}

(3)consumer模块

消费者模块是需要调用服务的模块。

同样先引入hutool、lombok等依赖。

创建消费者启动类EasyConsumerExample:

调用接口。目前无法获取到userService实例,先预留为null。之后的目标是能够通过RPC框架快速得到一个支持远程调用服务提供者的代理对象,像调用本地方法一样调用UserService的方法。

package com.khr.example.consumer;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.proxy.ServiceProxyFactory;


/**
 * 简易服务消费者示例
 */
public class EasyConsumerExample {

    public static void main(String[] args){
        //todo 需要获取UserService的实现类对象
        UserService userService = null;
        User user = new User();
        user.setName("KHR");
        //调用
        User newUser = userService.getUser(user);
        if (newUser != null){
            System.out.println(newUser.getName());
        } else {
            System.out.println("user == null");
        }
    }
}

3. Web服务器

消费者想要调用另一台机器上的服务提供者的方法,需要服务提供者开启可远程访问的服务。因此需要一个Web服务器,能够接受处理请求,并返回响应。

Spring Boot框架内置了Tomcat,还有NIO框架的Netty、Vert.x等都是Web服务器。本项目中使用高性能的NIO框架 Vert.x 作为RPC框架的Web服务器。关于Vert.x之后会另出文章总结。

先引入Vert.x的依赖。

创建Web服务器接口HttpServer:

定义统一的启动服务器方法,便于后续扩展,比如实现多种不同的Web服务器。

package com.khr.krpc.server;

/**
 * HTTP服务器接口
 */
public interface HttpServer {

    /**
     * 启动服务器
     *
     * @param port
     */
    void doStart(int port);
}

创建基于Vert.x实现的Web服务器VertxHttpServer:

监听指定端口并处理请求。

package com.khr.krpc.server;

import io.vertx.core.Vertx;

/**
 * Vertx HTTP 服务器
 */
public class VertxHttpServer implements HttpServer{

    /**
     * 启动服务器
     *
     * @param port
     */
    public void doStart(final int port) {
        //创建Vert.x实例
        Vertx vertx = Vertx.vertx();

        //创建HTTP服务器
        io.vertx.core.http.HttpServer server = vertx.createHttpServer();

        //监听端口并处理请求
        server.requestHandler(request ->{
           //处理HTTP请求
           System.out.println("Received request:"+ request.method()+" "+ request.uri());
           //发送HTTP响应
           request.response()
                    .putHeader("content-type","text/plain")
                    .end("Hello from Vert.x HTTP server!")
        });
           
        //启动HTTP服务器并监听指定端口
        server.listen(port, result -> {
            if (result.succeeded()) {
                System.out.println("Server is now listening on port" + port);
            } else {
                System.out.println("Failed to start server:" + result.cause());
            }
        });
    }
}

验证web服务器能否启动成功并接受请求:

修改provider模块的EasyProviderExample类,编写启动Web服务的代码。

通过浏览器访问localhost:8080,即可查看到" Hello from Vert.x HTTP server! "字样。

package com.khr.example.provider;

import com.khr.example.common.service.UserService;
import com.khr.krpc.server.HttpServer;
import com.khr.krpc.server.VertxHttpServer;
import com.khr.krpc.registry.LocalRegistry;

/**
 * 简易服务提供者示例
 */
public class EasyProviderExample {

    public static void  main(String[] args){

        //启动web服务
        HttpServer httpServer = new VertxHttpServer();
        httpServer.doStart(8080);
    }
}

4. 本地服务注册器

简易版RPC框架暂时不使用第三方注册中心(Nacos、Zookeeper等),先跑通整个流程。因此直接把服务注册到服务提供者本地。

在RPC模块中创建本地服务注册器LocalRegistry:

使用线程安全的ConcurrentHashMap存储服务注册信息,key为服务名称,value为服务的实现类。之后就可以根据要调用的服务名称获取到对应的实现类,然后通过反射进行方法调用了。

package com.khr.krpc.registry;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 本地注册中心
 */

public class LocalRegistry {

    /**
     * 注册信息存储
     */
    private static final Map<String,Class<?>> map = new ConcurrentHashMap<>();

    /**
     * 注册服务
     *
     * @param serviceName
     * @param implClass
     */
    public static void registry(String serviceName,Class<?> implClass){
        map.put(serviceName,implClass);
    }

    /**
     * 获取服务
     *
     * @param serviceName
     * @return
     */
    public static Class<?> get(String serviceName){
        return map.get(serviceName);
    }

    /**
     * 删除服务
     *
     * @param serviceName
     */
    public static void remove(String serviceName){
        map.remove(serviceName);
    }
}

修改EasyProviderExample:

给provider模块增加注册服务到注册器中的逻辑,也就是服务提供者启动时,会进行本地注册。

package com.khr.example.provider;

import com.khr.example.common.service.UserService;
import com.khr.krpc.server.HttpServer;
import com.khr.krpc.server.VertxHttpServer;
import com.khr.krpc.registry.LocalRegistry;

/**
 * 简易服务提供者示例
 */
public class EasyProviderExample {

    public static void  main(String[] args){
        //注册服务
        LocalRegistry.registry(UserService.class.getName(),UserServiceImpl.class);

        //启动web服务
        HttpServer httpServer = new VertxHttpServer();
        httpServer.doStart(8080);
    }
}

5. 序列化器

服务在本地注册后,就可以根据请求信息取出实现类并调用方法了。

但由于传递的参数是Java对象,仅运行在JVM中,如果想要在网络中传输,需要进行序列化与反序列化操作。序列化与反序列化不再做过多介绍,之前的Java八股中已经解释。

有多种不同的序列化方法,比如Java原生序列化、Json、Hessian等。为了方便实现,此处先选择Java原生序列化器。

在RPC模块中创建序列化接口Serializer:

提供序列化和反序列化两个方法,同时便于后续扩展。

package com.khr.krpc.serializer;

import java.io.IOException;

/**
 * 序列化器接口
 */
public interface Serializer {

    /**
     * 序列化
     *
     * @param object
     * @param <T>
     * @return
     * @throws IOException
     */
    <T> byte[] serialize(T object) throws IOException;

    /**
     * 反序列化
     *
     * @param bytes
     * @param type
     * @param <T>
     * @return
     * @throws IOException
     */

    <T> T deserialize(byte[] bytes,Class<T> type) throws IOException;
}

基于Java自带的序列化器实现JdkSerializer(现用现查):

package com.khr.krpc.serializer;

import java.io.*;

/**
 * JDK序列化器
 */

public class JdkSerializer implements Serializer{

    /**
     * 序列化
     *
     * @param object
     * @param <T>
     * @return
     * @throws IOException
     */
    @Override
    public <T> byte[] serialize(T object) throws IOException{
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
        objectOutputStream.writeObject(object);
        objectOutputStream.close();
        return outputStream.toByteArray();
    }

    /**
     * 反序列化
     *
     * @param bytes
     * @param type
     * @param <T>
     * @return
     * @throws IOException
     */
    @Override
    public <T> T deserialize(byte[] bytes,Class<T> type) throws IOException{
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        try{
            return (T) objectInputStream.readObject();
        } catch (ClassNotFoundException e){
            throw new RuntimeException(e);
        } finally {
            objectInputStream.close();
        }
    }
}

6. 提供者处理调用 — 请求处理器

请求处理器用于处理接收到的请求,并根据请求参数找到对应的服务和方法,通过反射实现调用,最后封装返回结果并响应请求。

在RPC模块中创建请求和响应封装类:

请求类RpcRequest的作用是封装调用所需的信息,比如服务名称、方法名称、调用参数的类型列表、参数列表等。这些都是Java反射机制所需的参数。

package com.khr.krpc.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * RPC请求
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RpcRequest implements Serializable{

    /**
     * 服务名称
     */
    private String serviceName;

    /**
     * 方法名称
     */
    private String methodName;

    /**
     * 参数类型列表
     */
    private Class<?>[] parameterTypes;

    /**
     * 参数列表
     */
    private Object[] args;
}

响应类RpcResponse的作用是封装调用方法后得到的返回值以及调用信息等。

package com.khr.krpc.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * RPC响应
 */
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RpcResponse implements Serializable{

    /**
     * 响应数据
     */
    private Object data;

    /**
     * 响应数据类型(预留)
     */
    private Class<?> dataType;

    /**
     * 响应信息
     */
    private String message;

    /**
     * 异常信息
     */
    private Exception exception;
}

创建请求处理器HttpServerHandler:

业务流程:

  • 反序列化请求为对象(因为在消费者发送请求给Web服务器时已经将各类参数序列化),并从请求对象中获取参数。
  • 根据服务名称从本地注册器中获取到对应的服务实现类。
  • 通过反射机制调用方法,得到返回结果。
  • 封装返回结果并序列化,然后写入到响应中。
package com.khr.krpc.server;

import com.khr.krpc.model.RpcRequest;
import com.khr.krpc.model.RpcResponse;
import com.khr.krpc.registry.LocalRegistry;
import com.khr.krpc.serializer.JdkSerializer;
import com.khr.krpc.serializer.Serializer;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.HttpServerResponse;

import java.io.IOException;
import java.lang.reflect.Method;


/**
 * HTTP请求处理
 */
public class HttpServerHandler implements Handler<HttpServerRequest> {

    @Override
    public void handle(HttpServerRequest request) {
        //指定序列化器
        final Serializer serializer = new JdkSerializer();

        //记录日志
        System.out.println("Received request:" + request.method() + " " + request.uri());

        //异步处理HTTP请求
        request.bodyHandler(body -> {
            byte[] bytes = body.getBytes();
            RpcRequest rpcRequest = null;
            try {
                rpcRequest = serializer.deserialize(bytes, RpcRequest.class);
            } catch (Exception e) {
                e.printStackTrace();
            }

            //构造响应结果对象
            RpcResponse rpcResponse = new RpcResponse();
            //如果请求为null,直接返回
            if (rpcRequest == null) {
                rpcResponse.setMessage("rpcRequest is null");
                doResponse(request, rpcResponse, serializer);
                return;
            }

            try {
                //获取要调用的服务实现类,通过反射调用
                Class<?> implClass = LocalRegistry.get(rpcRequest.getServiceName());
                Method method = implClass.getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());
                Object result = method.invoke(implClass.newInstance(), rpcRequest.getArgs());
                //封装返回结果
                rpcResponse.setData(result);
                rpcResponse.setDataType(method.getReturnType());
                rpcResponse.setMessage("ok");
            } catch (Exception e) {
                e.printStackTrace();
                rpcResponse.setMessage(e.getMessage());
                rpcResponse.setException(e);
            }
            //响应
            doResponse(request, rpcResponse, serializer);
        });
    }

    /**
     * 响应
     *
     * @param request
     * @param rpcResponse
     * @param serializer
     */
    void doResponse(HttpServerRequest request,RpcResponse rpcResponse,Serializer serializer){
        HttpServerResponse httpServerResponse = request.response()
                .putHeader("content-type","application/json");
        try{
            //序列化
            byte[] serialized = serializer.serialize(rpcResponse);
            httpServerResponse.end(Buffer.buffer(serialized));
        }catch (IOException e){
            e.printStackTrace();
            httpServerResponse.end(Buffer.buffer());
        }
}
}

不同的Web服务器对应的请求处理器实现方式也不同,Vert.x是通过实现Handler<HttpServerRequest>接口来自定义请求处理器,并且可以通过request.bodyHandler异步处理请求。

之后,再给HttpServer绑定请求处理器:

修改VertxHttpServer的代码,通过server.requestHandler绑定请求处理器。

package com.khr.krpc.server;

import io.vertx.core.Vertx;

/**
 * Vertx HTTP 服务器
 */
public class VertxHttpServer implements HttpServer{

    /**
     * 启动服务器
     *
     * @param port
     */
    public void doStart(final int port) {
        //创建Vert.x实例
        Vertx vertx = Vertx.vertx();

        //创建HTTP服务器
        io.vertx.core.http.HttpServer server = vertx.createHttpServer();

        //监听端口并处理请求
        server.requestHandler(new HttpServerHandler());

        //启动HTTP服务器并监听指定端口
        server.listen(port, result -> {
            if (result.succeeded()) {
                System.out.println("Server is now listening on port" + port);
            } else {
                System.out.println("Failed to start server:" + result.cause());
            }
        });
    }
}

至此,服务提供者已经能够接受请求并完成服务调用。

7. 消费者发起调用 — 基于代理模式

在之前完成的consumer模块中,消费者无法获取userService实例,我们是希望通过代理对象来发起调用,从而实现一行代码调用。

只要能获取到UserService对象(实现类),就能完成调用,那么问题是如何获取呢?如果把provider模块的UserServiceImpl的代码复制粘贴到consumer模块,那么RPC框架就失去了它存在的意义。在分布式系统中,当开发者调用其它项目或团队提供的接口时,一般只关注请求参数和响应结果(点外卖),而不关注具体实现(信息传输、配送等)。

因此可以通过生成代理对象的方式来简化消费者的调用过程,即把事情都交给中介去做。

代理的实现方式:静态代理和动态代理。

静态代理:

为每个特定类型的接口或对象编写一个代理类,即在consumer模块中创建静态代理类UserServiceProxy,实现UserService接口和getUser方法。

但getUser方法的实现不是复制粘贴provider模块中UserServiceImpl的代码,而是通过构造HTTP请求去调用服务提供者。代理对象在发送请求时要先将参数进行序列化。

package com.khr.example.consumer;

import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.model.RpcRequest;
import com.khr.krpc.model.RpcResponse;
import com.khr.krpc.serializer.JdkSerializer;
import com.khr.krpc.serializer.Serializer;

import java.io.IOException;

/**
 * 静态代理
 */

public class UserServiceProxy implements UserService {

    public User getUser(User user){
        //指定序列化器
        Serializer serializer = new JdkSerializer();

        //发请求
        RpcRequest rpcRequest = RpcRequest.builder()
                .serviceName(UserService.class.getName())
                .methodName("getUser")
                .parameterTypes(new Class[]{User.class})
                .args(new Object[]{user})
                .build();
        try{
            byte[] boduBytes = serializer.serialize(rpcRequest);
            byte[] result;
            try(HttpResponse httpResponse = HttpRequest.post("http://localhost:8080")
                    .body(boduBytes)
                    .execute()){
                result = httpResponse.bodyBytes();
            }
            RpcResponse rpcResponse = serializer.deserialize(result,RpcResponse.class);
            return(User)rpcResponse.getData();
        } catch (IOException e){
            e.printStackTrace();
        }

        return null;
    }
}

然后修改EasyConsumerExample:

添加一个代理对象并赋值给userService。

package com.khr.example.consumer;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.proxy.ServiceProxyFactory;


/**
 * 简易服务消费者示例
 */
public class EasyConsumerExample {

    public static void main(String[] args){
        //静态代理
        //UserService userService = new UserServiceProxy();

        User user = new User();
        user.setName("KHR");
        //调用
        User newUser = userService.getUser(user);
        if (newUser != null){
            System.out.println(newUser.getName());
        } else {
            System.out.println("user == null");
        }
    }
}

静态代理其实就是写一个实现类,但如果服务接口很多,需要为每个接口都写一个实现类,依然比较繁琐,灵活性较差。

因此RPC框架中一般采用动态代理。

动态代理的作用是根据要生成的对象类型,自动生成一个代理对象。常用的方式有JDK动态代理或CGLIB。此处使用JDK动态代理。

在RPC模块中创建动态代理类ServiceProxy:

需要实现InvocationHandler接口的invoke方法。

当用户调用某个接口的方法时,会改为调用invoke方法。在invoke方法中,可以获取到要调用的方法信息、传入的参数列表等,这些就是服务提供者需要的参数。用这些参数来构造请求对象即可。

package com.khr.krpc.proxy;


import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.khr.krpc.model.RpcRequest;
import com.khr.krpc.model.RpcResponse;
import com.khr.krpc.serializer.JdkSerializer;
import com.khr.krpc.serializer.Serializer;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 服务代理(JDK动态代理)
 */

public class ServiceProxy implements InvocationHandler {

    /**
     * 调用代理
     *
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy,Method method,Object[] args) throws Throwable{
        //指定序列化器
        Serializer serializer = new JdkSerializer();

        //构造请求
        RpcRequest rpcRequest = RpcRequest.builder()
                .serviceName(method.getDeclaringClass().getName())
                .methodName(method.getName())
                .parameterTypes(method.getParameterTypes())
                .args(args)
                .build();
        try{
            //序列化
            byte[] boduBytes = serializer.serialize(rpcRequest);
            //发送请求
            //todo 注意,这里地址被硬编码了,需要使用注册中心和服务发现机制解决
            try(HttpResponse httpResponse = HttpRequest.post("http://localhost:8080")
                    .body(boduBytes)
                    .execute()){
                byte[] result = httpResponse.bodyBytes();
                //反序列化
                RpcResponse rpcResponse = serializer.deserialize(result,RpcResponse.class);
                return rpcResponse.getData();
            }
        } catch (IOException e){
            e.printStackTrace();
        }

        return null;
    }
}

创建动态代理工厂ServiceProxyFactory:

根据指定类创建动态代理对象,使用了工厂设计模式,来简化对象的创建过程。

package com.khr.krpc.proxy;

import java.lang.reflect.Proxy;

/**
 * 服务代理工厂(用于创建代理对象)
 */

public class ServiceProxyFactory {

    /**
     * 根据服务类获取代理对象
     *
     * @param serviceClass
     * @param <T>
     * @return
     */
    public static <T> T getProxy(Class<T> serviceClass){
        return (T) Proxy.newProxyInstance( //为指定类型创建代理对象
                serviceClass.getClassLoader(),
                new Class[]{serviceClass},
                new ServiceProxy());
    }
}

修改EasyConsumerExample代码:

通过调用工厂为UserService获取动态代理对象。

package com.khr.example.consumer;

import com.khr.example.common.model.User;
import com.khr.example.common.service.UserService;
import com.khr.krpc.proxy.ServiceProxyFactory;


/**
 * 简易服务消费者示例
 */
public class EasyConsumerExample {

    public static void main(String[] args){
        //静态代理
        //UserService userService = new UserServiceProxy();

        //动态代理
        UserService userService = ServiceProxyFactory.getProxy(UserService.class);
        User user = new User();
        user.setName("KHR");
        //调用
        User newUser = userService.getUser(user);
        if (newUser != null){
            System.out.println(newUser.getName());
        } else {
            System.out.println("user == null");
        }
    }
}

至此,简易版RPC框架开发完成。

8. 测试调用

首先debug启动服务提供者。

之后debug启动消费者,在ServiceProxy代理类中加断点,可以发现调用userService的时候,实际上是去调用了代理对象的invoke方法,并获取到了serviceName、methodName、参数类型和列表等信息。

继续dubug,可以看到序列化后的请求对象,结构式字节数组:

在请求处理器中打断点,可以看到接受并反序列化后的请求与发送时的内容一致:

说明代理对象将请求参数序列化后发送到Web服务器中,被请求处理器接收后进行反序列化,这一流程实现了。

最后在provider与consumer模块中都输出了用户名称,说明整个流程跑通了。

后续将会持续扩展本项目,以实现更为复杂的RPC框架。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/687682.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

02眼电识别眼动--软件V1.0

对应视频链接点击直达 01项目点击下载&#xff0c;可直接运行&#xff08;含数据库&#xff09; 02眼电识别眼动--软件V1.0 对应视频链接点击直达构思结语其他以下是废话 构思 对于软件&#xff0c;主要就是接收数据、处理数据、储存和显示数据。 这是主要页面&#xff0c;…

爬取股票数据python

最近在搜集数据要做分析&#xff0c;一般的数据来源是一手数据&#xff08;生产的&#xff09;和二手数据&#xff08;来自其他地方的&#xff09;。 今天我们爬取同花顺这个网站的数据。url为&#xff1a;https://data.10jqka.com.cn/ipo/xgsgyzq/ 话不多说直接上代码。有帮…

QT项目实战: 五子棋小游戏

目录 内容介绍 一.添加头文件 二.画棋盘 1.宏定义 2.棋盘 三.画棋子 四.获取棋子摆放位置 五.判断棋子存在 六.判断胜利 1.变量定义和初始化 2.检查获胜条件 3.游戏结束处理 七.重绘 八.效果展示 九.代码 1.mainwindow.h 2.mainwindow.cpp 3.chessitem.h 4…

部署kubesphere报错

安装kubesphere报错命名空间terminted [rootk8smaster ~]# kubectl apply -f kubesphere-installer.yaml Warning: apiextensions.k8s.io/v1beta1 CustomResourceDefinition is deprecated in v1.16, unavailable in v1.22; use apiextensions.k8s.io/v1 CustomResourceDefini…

【Oracle篇】rman全库异机恢复:从单机环境到RAC测试环境的转移(第五篇,总共八篇)

&#x1f4ab;《博主介绍》&#xff1a;✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ &#x1f4ab;《擅长领域》&#xff1a;✌️擅长Oracle、MySQL、SQLserver、阿里云AnalyticDB for MySQL(分布式数据仓库)、Linux&#xff0c;也在扩展大数据方向的知识面✌️…

车载以太网测试要测些什么呢?

车载以太网测试大致可以分成两块&#xff1a;TC8测试和以太网通信测试。 TC8测试全称TC8一致性测试&#xff0c;其规范由OPEN联盟制定&#xff0c;包括车载以太网ECU从物理层到应用层的各层互操作性以及常规基础功能服务。目的在于提高不同ECU之间的兼容性。 TC8测试规范可以…

C++STL---stack queue知识汇总

前言 C将stack和queue划归到了Containers中&#xff0c;但严格的说这并不准确&#xff0c;stack和queue实际上已经不再是容器了&#xff0c;而是属于容器适配器&#xff0c;适配器做的功能是转换&#xff0c;即&#xff1a;它不是直接实现的&#xff0c;而是由其他容器封装转换…

ruoyi若依二次开发怎么添加扫描自己的controller和mapper,配置三个地方即可。

概要 首先&#xff0c;添加在com.ruoyi外的类&#xff0c;项目启动后&#xff0c;调用接口&#xff0c;是会返回404找不到的。 必须要对这以外的接口类进行配置。目录结构如下&#xff1a; 解决步骤 一、添加 com.ruoyi.framework.config 下&#xff1a; // 指定要扫描的M…

深度学习革命-AI发展详解

深度学习革命 《深度学习革命》是一部引人深思的作品&#xff0c;详细讲述了深度学习技术的发展历程及其对各个行业的深远影响。由杰出的计算机科学家、深度学习专家撰写&#xff0c;这本书不仅适合科技领域的专业人士阅读&#xff0c;也为普通读者提供了一个理解人工智能革命…

LeetCode322.零钱兑换

文章目录 题目描述解题思路递归记忆化搜索动态规划另一种实现 题目描述 https://leetcode.cn/problems/coin-change/description/?envTypestudy-plan-v2&envIdtop-interview-150 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount …

考研规划,这么学上岸率比985的学霸还高!

我觉得985考研上岸率低是可以理解的 因为大家本科能考上985&#xff0c;那研究生大概率会报考和本学校差不多或者更高水平的院校&#xff0c;甚至清华北大都有人敢报&#xff0c;去实现自己本科没有实现的梦想。 而且985其实保研的比例很高&#xff0c;一般有30%的保研比例了…

亚马逊测评自养号技术全攻略:一站式解决方案

在跨境电商这行&#xff0c;产品测评可是个大问题。如果你的商品销量少&#xff0c;评价也不多&#xff0c;那买家就很难注意到你的产品&#xff0c;更别提下单购买了。毕竟&#xff0c;大家都喜欢跟风买那些已经有很多人好评的产品&#xff0c;而不是冒险尝试一个全新的。 我们…

基于JSP技术的社区疫情防控管理信息系统

你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果有相关需求&#xff0c;文末可以找到我的联系方式。 开发语言&#xff1a;JSP 数据库&#xff1a;MySQL 技术&#xff1a;JSPJavaBeans 工具&#xff1a;MyEclipse、Tomcat、Navicat 系统展示 首页 用户注册与登录界…

《大宅门》特别活动走进李良济,开启探寻中医药文化之旅!

《大宅门》话剧将于6月14-16日在苏州湾大剧院上演&#xff0c;为了让大家了解到中医药知识&#xff0c;6月2日&#xff0c;李良济携手苏州湾大剧院举办《大宅门》特别活动“探寻中医药文化之旅”&#xff01; 6月2日下午&#xff0c;大家一起走进李良济&#xff0c;深度了解传统…

Django框架中级

Django框架中级 – 潘登同学的WEB框架 文章目录 Django框架中级 -- 潘登同学的WEB框架 中间件自定义中间件常用中间件process_view() 使用中间件进行URL过滤 Django生命周期生命周期分析 Django日志日志配置filter过滤器自定义filter 日志格式化formatter Django信号内置信号定…

51单片机STC89C52RC——1.1点亮一个LED

目录 STC单片机模块 LED模块 创建Keil项目 代码 效果 STC单片机模块 我们程序中要点亮的LED灯的位置如下图。 我们程序要用到的针脚如下图 LED模块 电路图如下 8个LED&#xff0c;全部点亮 每一bit位 都需要设置为0 二进制是0000 0000 。如果只点亮D1一个&#xff…

进口电动低温调节阀的作用-美国NOTON

进口电动低温调节阀在低温环境下的精确控制、广泛的工业应用、高效能设计、耐低温性和密封性、稳定性及可靠性以及安全性等方面发挥着重要作用。 进口电动低温调节阀的作用主要体现在以下几个方面&#xff1a; 低温环境下的精确控制&#xff1a; 进口电动低温调节阀是一种专为…

jenkins插件之Jdepend

JDepend插件是一个为构建生成JDepend报告的插件。 安装插件 JDepend Dashboard -->> 系统管理 -->> 插件管理 -->> Available plugins 搜索 Jdepend, 点击安装构建步骤新增执行shell #执行pdepend if docker exec phpfpm82 /tmp/composer/vendor/bin/pdepe…

怎么把wmv格式转换成mp4?四种将wmv转成MP4格式的方法

怎么把wmv格式转换成mp4&#xff1f;在微软操作系统中&#xff0c;我们经常会遇到wmv格式的视频文件。这种由微软开发的视频格式在微软平台上有很好的兼容性&#xff0c;但它也存在一些被忽视的缺点。首先&#xff0c;wmv格式支持数字版权管理&#xff0c;这意味着某些视频可能…

期权有用吗?期权都有哪些用途?

今天带你了解期权有用吗&#xff1f;期权都有哪些用途&#xff1f;期权和期货作为衍生工具,都有风险管理、资产配置和价格发现等功能。 期权有用吗&#xff1f; 期权作为一种衍生金融工具&#xff0c;具有多种用途和好处&#xff0c;对不同类型的投资者和市场参与者来说&#…