【noob-rpc】②简易版RPC框架
【noob-rpc】②简易版RPC框架
核心概念
1.基础概念
什么是 RPC?
专业定义:RPC(Remote Procedure Cal)即远程过程调用,是一种计算机通信协议,它允许程序在不同的计算机之间进行通信和交互,就像本地调用一样。
PRC框架的引入:基于多个子系统/服务之间的调用,让开发者可以向调用本地方法一样调用其他项目的代码来进一步实现功能。
(可结合原生cv方式、http远程调用等方面扩展思考)
为什么需要 RPC?
回到 RPC 的概念,RPC 允许一个程序(称为服务消费者)像调用自己程序的方法一样,调用另一个程序(称为服务提供者)的接口,而不需要了解数据的传输处理过程、底层网络通信的细节等。这些都会由 RPC 框架来完成,使得开发者可以轻松调用远程服务,快速开发分布式系统。
举个例子,现在有个项目 A提供了点餐服务,项目 B需要调用点餐服务完成下单。
点餐服务和接口的示例伪代码如下:
public interface OrderService{
// 模拟点餐方法
long order(参数n);
}
如果没有 RPC 框架,项目 B 怎么调用项目 A的服务呢?
首先,由于项目A和项目 B都是独立的系统,不能像SDK一样作为依赖包引入。那么就需要项目A提供 web 服务并且编写一个点餐接口暴露服务,比如访问一个接口地址就能调用点餐服务;然后项目 B作为服务消费者,需要自行构造请求,并通过 HttpClient请求上述地址。如果项目B需要调用更多第三方服务,每个服务和方法的调用都编写一个 HTTP 请求,则后续代码维护迭代会非常麻烦
url = "http://api/order"
req = new Req(参数n);
res = httpClient.post(url).body(req).execute();
orderId = res.data.orderId;
通过引入RPC框架,则可一行代码实现调用
orderId = orderService.order(参数n);
2.构建思路
简易版RPC框架构建思路
原生调用=》RPC框架引入
【1】原生调用
【2】通过http调用访问
【3】提供统一的服务调用接口,根据不同的处理器调用服务
【4】基于代理模式,引入代理对象完成接口请求和响应的过程,进而简化消费者的请求操作
基于上述构建思路可以构建一个简化版的RPC框架,完成一个消费者请求调用的的过程,但实际应用中还需考虑到一些扩展的情况
扩展版PRC框架构建思路
服务注册和发现
消费者如何知道自己要调用的服务接口和参数请求呢?
可以通过提供一个服务注册中心存储服务提供者相关的一些信息(例如服务地址、服务方法信息等),一般现成的服务注册中心有redis、zookeeper等
负载均衡
如果存在多个服务提供者,消费者应该调用哪个服务提供者呢?
可以给服务调用方增加负载均衡能力,通过指定不同的算法来决定调用哪一个服务提供者,比如轮询、随机、根据性能动态调用等。
容错机制
如果服务调用失败该如何处理?
其他扩展
除了上面几个经典设计外,如果想要做一个相对完整的 RPC 框架,还要考虑很多问题。
【1】服务提供者下线了怎么办?需要一个失效节点剔除机制
【2】服务消费者每次都从注册中心拉取信息,性能会不会很差?可以使用缓存来优化性能
【3】如何优化 RPC 框架的传输通讯性能?比如选择合适的网络框架、自定义协议头、节约传输体积等
【4】如何让整个框架更利于扩展?比如使用 Java 的 SPI机制、配置化等等
在了解PRC框架搭建的过程中,可以一步步由浅入深,通过做一个 RPC 项目学习到网络、序列化、代理、服务注册发现、负载均衡、容错、可扩展设计等知识
项目构建
1.模块构建
# 项目结构说明
noob-rpc(根目录)
sample-common(公共模块:公共依赖,包括接口、Model等内容)
sample-consumer(子模块:消费者)
sample-provider(子模块:提供者)
noob-rpc-easy(简易版RPC框架)
noob-rpc-core:rpc框架核心代码
sample-springboot-consumer(子模块:消费者,基于springboot框架)
sample-springboot-provider(子模块:提供者,基于springboot框架)
noob-rpc-springboot-starter(注解驱动的RRC框架,可在sringboot框架中快速使用)
maven构建模板说明
Archetype ID | 说明 |
---|---|
maven-archetype-archetype | 一个样例原型 |
maven-archetype-j2ee-simple | 简单的J2EE应用程序样例 |
maven-archetype-mojo | Maven插件样本的示例 |
maven-archetype-plugin | Maven插件样本 |
maven-archetype-plugin-site | Mave插件网站的样例 |
maven-archetype-portlet | JSR-268门户样例 |
maven-archetype-quickstart | Maven工程样例 |
maven-archetype-simple | 一个简单的Maven工程 |
maven-archetype-site | Maven网站的样例,它演示了对诸如APT、XDoc和FML等文档类型的支持,并演示了如果把网站国际化(i18n) |
maven-archetype-site-simple | Maven网站样例 |
maven-archetype-webapp | Maven的Webapp工程样例 |
一般常用的maven模板是maven-archetype-quickstart、maven-archetype-webapp
【1】maven-archetype-quickstart默认的Archetype,基本内容包括:
一个包含junit依赖声明的pom.xml
src/main/java主代码目录及一个名为App的类
src/test/java测试代码目录及一个名为AppTest的测试用例
【2】maven-archetype-webapp
一个最简单的Maven war项目模板,当需要快速创建一个Web应用的时候可以使用它。生成的项目内容包括:
一个packaging为war且带有junit依赖声明的pom.xml
src/main/webapp/日录
src/main/webapp/index.jsp文件件
src/main/webapp/WEB-INF/web.xml文件
项目创建:noob-rpc
sample-common(公共模块:公共依赖,包括接口、Model等内容)
sample-consumer(子模块:消费者)
sample-provider(子模块:提供者)
noob-rpc-easy(简易版RPC框架)
【1】创建一个根目录noob-rpc,随后通过idea打开并创建maven模块
下载maven模板需要注意网络连通问题,以及关注maven关联的中央仓库是否可以提供指定的依赖版本信息(参考maven仓库配置),配置修改完成重启idea清理maven缓存即可
<mirrors>
<!-- 阿里云仓库 -->
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<!-- 中央仓库1 -->
<mirror>
<id>repo1</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo1.maven.org/maven2/</url>
</mirror>
<!-- 中央仓库2 -->
<mirror>
<id>repo2</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo2.maven.org/maven2/</url>
</mirror>
<!-- 中央仓库在中国的镜像 -->
<mirror>
<id>maven.net.cn</id>
<name>oneof the central mirrors in china</name>
<url>http://maven.net.cn/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
如果maven模板构建太慢,也可自己创建普通java(基于maven构建)项目,随后配置相关内容完成模块构建
如果模板构建失败需要移除,则可选择指定的模块进行移除(此处注意不要直接进行删除操作,而是借助工程中的操作进行移除,否则就算删除了内容关联的配置还是残存在其中),移除模板有两步:【1】选择对应子模块,右键选择remove module;【2】在对应目录删除子模块工程
如果是手动创建模块,则在pom.xml中引入maven编译组件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
2.代码构建(公共模块、消费者、提供者)
先理解每个模块之间的调用关系,然后再进一步进行构建,按需引入依赖关系
sample-common
引入公共相关的内容:用户实体、用户服务接口定义
/**
* 用户信息
*/
public class User implements Serializable {
private String name;
public User(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/**
* 用户服务接口
*/
public interface UserService {
/**
* 获取用户信息
* @param user
* @return
*/
User getUser(User user);
}
sample-provider
构建步骤说明:
【1】pom.xml引入相关依赖
【2】定义UserServiceImpl实现Uservice接口方法
【3】定义EasyProviderSample接口服务启动类(对外提供接口方法)(todo:此处先构建框架,具体实现需依托于后面的web服务器、服务注册器、序列化器等进一步完善)
pom.xml
<dependencies>
<!-- 引入公共模块 -->
<dependency>
<groupId>com.noob.rpc</groupId>
<artifactId>sample-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://doc.hutool.cn/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- https://projectlombok.org/ -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
UserServiceImpl实现UserService接口(此处的UserService为sample-common模块定义的)
/**
* 用户接口实现类
*/
public class UserServiceImpl implements UserService {
@Override
public User getUser(User user) {
System.out.println("用户名:"+user.getName());
return user;
}
}
sample-consumer
构建步骤说明:
【1】pom.xml中引入相关依赖(可参考提供者模块)
【2】创建消费者启动类EasyConsumerSample,模拟调用服务方提供的接口(todo:此处UserService的引入需要依托于后续的构建步骤,此处先构建基础框架思路)
pom.xml
<dependencies>
<!-- 引入公共模块 -->
<dependency>
<groupId>com.noob.rpc</groupId>
<artifactId>sample-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://doc.hutool.cn/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- https://projectlombok.org/ -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
创建消费者启动类EasyConsumerSample,模拟调用服务方提供的接口
public class EasyConsumerSample {
public static void main(String[] args) {
// todo 获取UserService的实现对象
UserService userService = null;
User user = new User("noob");
// 调用
User newUser = userService.getUser(user);
if(newUser != null) {
System.out.println(newUser.getName());
}else {
System.out.println("user == null");
}
}
}
3.web服务器构建(noob-rpc-easy)
要让服务提供者提供可远程访问的服务,则需要一个web服务器,可以处理请求并响应
web服务器的选择可以是springboo内嵌tomcat、NIO框架中的Netty、Vert.x等,此处选择高性能框架Vert.x作为RPC框架的web服务器
构建步骤说明:
【1】pom.xml引入相关依赖
【2】自定义HttpServer接口定义服务处理相关方法(例如监听端口请求响应等),便于后续扩展
【3】自定义VertxServer实现HttpServer接口,完成接口请求响应
【4】Main类中验证测试服务器构建是否成功
pom.xml依赖引入
<dependencies>
<!-- https://mvnrepository.com/artifact/io.vertx/vertx-core -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.1</version>
</dependency>
<!-- https://doc.hutool.cn/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- https://projectlombok.org/ -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
</dependencies>
自定义编写服务器接口HttpServer(定义统一的启动服务器方法,用作后续扩展,可实现多种不同的web服务器)
/**
* Http 服务器接口
*/
public interface HttpServer {
// 启动服务器
void doStart(int port);
}
编写基于Vert.x实现web服务器(VertxHttpServer)
/**
* 基于Vert.x实现web服务器(监听指定端口并处理请求)
*/
public class VertxHttpServer implements HttpServer {
@Override
public void doStart(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 Vert.x HTTP Server");
});
// 启动 HTTP 服务器并监听指定端口
server.listen(port, result -> {
if (result.succeeded()) {
System.out.println("Server is now listening on port " + port);
} else {
System.err.println("Failed to start server: " + result.cause());
}
});
}
}
测试验证
public class Main {
public static void main(String[] args) {
// 启动web服务测试
HttpServer httpServer = new VertxHttpServer();
httpServer.doStart(8080);
}
}
启动Main进行验证,访问localhsot:8080,可看到对应端口监听生效,并相应返回信息
4.本地服务注册器(noob-rpc-easy)
目前简易版RPC着重测试跑通流程,暂时先不引入第三方注册中心,将服务注册到服务提供者本地,在noob-rpc-easy模块中创建本地服务注册器LocalRegistry,借助线程安全的ConcurrentHashMap存储服务注册信息(key:服务名称、value服务实现类),随后则可根据调用的服务名称获取到对应的实现类,并通过反射进行方法调用
本地服务注册器和注册中心的作用是有区别的。注册中心的作用侧重于管理注册的服务、提供服务信息给消费者;而本地服务注册器的作用是根据服务名获取到对应的实现类,完成调用必不可少的模块。
构建步骤说明:
【1】创建本地服务注册器:LocalRegistry(存储接口服务信息)
【2】服务提供者需要将对外提供的服务接口注册到本地服务注册器中
创建本地服务注册器:LocalRegistry
/**
* 本地服务注册器
*/
public class LocalRegistry {
/**
* 注册信息存储
*/
private static final Map<String, Class<?>> map = new ConcurrentHashMap<>();
/**
* 注册服务
* @param serviceName
* @param implClass
*/
public static void register(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);
}
}
服务提供者需要将对外提供的服务接口注册到本地服务注册器中
如何理解注册这个概念?:从语法上来说就是将自己的服务接口信息放到本地服务注册器中定义的Map中,从实现上来看就是调用LocalRegistry的register方法
那么如果调用这个注册方法呢?回归到最原始的方法就是模块间的maven依赖引入,即在sample-provider中引入noob-rpc-easy模块作为依赖(前面之所以没有直接引入是怕概念混淆,主要是先一步步按部就班理解每个模块之间的调用关系再按需引入,不然一步切入就很容易混淆各个模块的作用)
# 1.引入noob-rpc-easy依赖
<!-- 引入noob-rpc-easy模块 -->
<dependency>
<groupId>com.noob.rpc</groupId>
<artifactId>noob-rpc-easy</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
# 2.EasyProviderSample:调用注册方法实现注册
/**
* 服务提供者启动类,通过main方法编写提供服务的代码
*/
public class EasyProviderSample {
public static void main(String[] args) {
// 提供服务
LocalRegistry.register(UserService.class.getName(), UserServiceImpl.class);
// 启动web服务
HttpServer httpServer = new VertxHttpServer();
httpServer.doStart(8080);
}
}
5.序列化器(noob-rpc-easy)
服务在本地注册后,可以根据请求信息取出实现类并调用方法。但是在编写处理请求的逻辑前,要先实现序列化器模块。因为无论是请求或响应,都会涉及参数的传输。而 Java对象是存活在JVM虚拟机中的,如果想在其他位置存储并访问、或者在网络中进行传输,就需要进行序列化和反序列化。
什么是序列化和反序列化呢?(序列化:将Java对象转为可传输的字节数组;反序列化:将字节数组转换为Java对象)
有很多种不同的序列化方式,比如Java原生序列化、JSON、 Hessian、 Kryo、 protobuf 等。为了实现方便,此处选择Java原生的序列化器
构建步骤说明:
【1】自定义序列化接口Serializer,提供序列化和反序列化两个方法定义,便于后续扩展更多的序列化器
【2】基于JDK自带的序列化器实现
【3】
自定义序列化接口Serializer,提供序列化和反序列化两个方法定义
/**
* 自定义序列化器:提供序列化和反序列化两个接口
*/
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;
}
JdkSerializer:基于JDK自带的序列化器实现
/**
* 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.请求处理器(noob-rpc-easy):提供者处理调用
请求处理器是RPC框架实现的关键,它的作用是:处理接收到的请求,并且根据请求参数找到对应的服务和方法,通过反射实现调用,最终封装返回结果并响应请求
构建步骤说明:
【1】在RPC模块中定义请求类、响应封装类
【2】编写请求处理器:HttpServerHandler
【3】给web服务器绑定请求处理器(即VertxServer中绑定自定义的请求处理器HttpServerHandler)
在RPC模块中定义请求类、响应封装类
RpcRequest:请求参数定义,包括服务名称、方法名称、调用参数类型列表、调用参数列表(Java反射机制所需参数)
RpcResponse:响应参数定义,包括封装调用方法获取的返回值、调用信息(例如异常情况)等
/**
* RPC 请求参数定义
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RpcRequest implements Serializable {
/**
* 服务名称
*/
private String serviceName;
/**
* 方法名称
*/
private String methodName;
/**
* 参数类型列表
*/
private Class<?>[] parameterTypes;
/**
* 参数列表
*/
private Object[] args;
}
/**
* RPC 响应参数定义
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RpcResponse implements Serializable {
/**
* 响应数据
*/
private Object data;
/**
* 响应数据类型(预留)
*/
private Class<?> dataType;
/**
* 响应信息
*/
private String message;
/**
* 异常信息
*/
private Exception exception;
}
编写请求处理器:HttpServerHandler
请求处理的的业务流程:
【1】反序列化请求为对象,并从请求对象中获取参数
【2】根据服务名称从本地注册器中获取到对应的服务实现类
【3】通过反射机制调用方法,得到返回结果
【4】对返回结果进行封装和序列化,并写入到响应中
不同web服务器对应请求处理器的实现方式不同,此处web服务器是基于Vert.x实现,则其对应的请求处理器是通过实现Handler<HttpServerRequest>
进行自定义的,通过request.bodyHandler异步处理请求
/**
* 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服务器绑定请求处理器(即VertxServer中绑定自定义的请求处理器HttpServerHandler)
VertHttpServer中,调整为自定义的请求处理器
基于上述步骤,引入RPC模块的服务提供者模块,能够接收请求并完成服务调用
7.代理(sample-consumer):消费者发起调用
在项目准备阶段(代码构建部分),预留一段调用服务的代码,只要能够获取到UserService对象(实现类),就能跑通整个流程。
❓此处思考一个问题:UserService的实现类从哪来呢?
如果把服务提供者的UserServicelmpl复制粘贴到消费者模块,这样RPC框架则失去了其构建意义。
在分布式系统中,调用其他项目或团队提供的接口时,一般只关注请求参数和响应结果,而不关注具体实现。因此此处可以通过生成代理对象来简化消费方的调用。代理的实现方式大致分为2类:静态代理和动态代理
静态代理实现
静态代理是指为每一个特定类型的接口或者对象提供一个代理类进行代理操作,此处创建UserServiceProxy,实现代理方法。
此处代理方法的实现,不是直接复制那段UserServicelmpl相关的代码,而是通过Http请求去调用接口服务(此处需注意参数的序列化处理)
构建步骤说明:
【1】pom.xml中引入noob-rpc-easy依赖(需用到序列化器)
【2】自定义UserServiceProxy代理类实现代理
【3】修改调用方法:将UserService实现交由UserServiceProxy代理处理
pom.xml
<dependency>
<groupId>com.noob.rpc</groupId>
<artifactId>noob-rpc-easy</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
自定义UserServiceProxy代理类实现代理
public class UserServiceProxy implements UserService {
@Override
public User getUser(User user) {
// 指定序列化器
final 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 {
// 序列化(Java 对象 => 字节数组)
byte[] bodyBytes = serializer.serialize(rpcRequest);
// 发送请求
try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080")
.body(bodyBytes)
.execute()) {
byte[] result = httpResponse.bodyBytes();
// 反序列化(字节数组 => Java 对象)
RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);
return (User) rpcResponse.getData();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
修改调用方法:将UserService实现交由UserServiceProxy代理处理
public class EasyConsumerSample {
public static void main(String[] args) {
UserService userService = new UserServiceProxy();
......
}
}
总结:静态代理其实就是变相实现UserService接口,只不过其代理操作核心是通过http调用对应的服务接口。但是基于这种方式,如果后续有多个接口需要调用,则需要相应编写不同的代理类,这种代理方式的灵活性会非常差,因此引入动态代理进行构建
动态代理实现
动态代理的作用:根据要生成的对象的类型,自动生成一个代理对象。此处使用JDK动态代理实现
常用的动态代理实现方式有JDK动态代理和基于字节码生成的动态代理(比如CGLIB)。前者简单易用、无需引入额外的库,但缺点是只能对接口进行代理;后者更灵活、可以对任何类进行代理,但性能略低于JDK动态代理。
构建步骤说明:
【1】在noob-easy-rpc模块中编写动态代理类ServiceProxy(实现InvocationHandler的invoke方法,此处对比静态代理的实现内容基本大同小异)
【2】使用工厂设计模式,简化对象创建过程
【3】在sample-consumer中EasyConsumerSample修改代理模式
在noob-easy-rpc模块中编写动态代理类ServiceProxy
/**
* 自定义服务代理类:当用户调用某个接口方法时,会改为调用invoke方法,在invoke方法中获取到要调用的方法信息、参数列表等,通过这些参数构造请求参数进而完成调用
*/
public class ServiceProxy implements InvocationHandler {
@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[] bodyBytes = serializer.serialize(rpcRequest);
// 发送请求
// todo 注意,这里地址被硬编码了(需要使用注册中心和服务发现机制解决)
try (HttpResponse httpResponse = HttpRequest.post("http://localhost:8080")
.body(bodyBytes)
.execute()) {
byte[] result = httpResponse.bodyBytes();
// 反序列化
RpcResponse rpcResponse = serializer.deserialize(result, RpcResponse.class);
return rpcResponse.getData();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
使用工厂设计模式,简化对象创建过程
/**
* 服务代理工厂(用于创建代理对象)
*/
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());
}
}
在sample-consumer中EasyConsumerSample修改代理模式
public class EasyConsumerSample {
public static void main(String[] args) {
// 动态代理模式
UserService userService = ServiceProxyFactory.getProxy(UserService.class);
.....
}
简易版RPC流程跑通
流程测试步骤说明
【1】debug模式启动服务提供者,运行main方法
【2】debug模式启动服务消费者,执行main方法
【3】设置断点,查看调用情况(ServiceProxy代理类、服务提供者模块请求处理器分别设置断点查看)
【4】查看结果
- debug模式启动服务提供者
- debug模式启动服务消费者(断点设置在ServiceProxy查看代理情况,HttpServerHandler请求处理器处理调用)
最终会在控制台输出获取到的用户名称
简易版RPC框架核心
核心构建思路说明:
【1】sample-common模块:提供调用接口统一定义、公共model相关定义
【2】sample-consumer模块:模拟调用请求(消费者模块构建:通过代理模式调用接口请求)
【3】sample-provider模块:提供接口调用服务(提供者模块构建:实现具体接口服务方法、依托于noob-rpc-easy构建调用服务流程)
【4】noob-rpc-easy模块:协同构建提供者模块完成接口调用服务,例如提供自定义web服务器(VertxHttpServer调用自定义处理器HttpServerHandler)、本地服务注册器(LocalRegistry提供服务注册存储空间)、序列化器(基于JDK序列化器实现序列化和反序列化方法)