java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端
通用调用
java 从零开始手写 RPC (01) 基于 socket 实现
java 从零开始手写 RPC (02)-netty4 实现客户端和服务端
java 从零开始手写 RPC (03) 如何实现客户端调用服务端?
前面我们的例子是一个固定的出参和入参,固定的方法实现。
本节将实现通用的调用,让框架具有更广泛的实用性。
基本思路
所有的方法调用,基于反射进行相关处理实现。
服务端
核心类
- RpcServer
调整如下:
serverBootstrap.group(workerGroup, bossGroup)
.channel(NioServerSocketChannel.class)
// 打印日志
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline()
// 解码 bytes=>resp
.addLast(new ObjectDecoder(Integer.MAX_VALUE, ClassResolvers.cacheDisabled(null)))
// request=>bytes
.addLast(new ObjectEncoder())
.addLast(new RpcServerHandler());
}
})
// 这个参数影响的是还没有被accept 取出的连接
.option(ChannelOption.SO_BACKLOG, 128)
// 这个参数只是过一段时间内客户端没有响应,服务端会发送一个 ack 包,以判断客户端是否还活着。
.childOption(ChannelOption.SO_KEEPALIVE, true);
其中 ObjectDecoder 和 ObjectEncoder 都是 netty 内置的实现。
RpcServerHandler
package com.github.houbb.rpc.server.handler;
import com.github.houbb.log.integration.core.Log;
import com.github.houbb.log.integration.core.LogFactory;
import com.github.houbb.rpc.common.rpc.domain.RpcRequest;
import com.github.houbb.rpc.common.rpc.domain.impl.DefaultRpcResponse;
import com.github.houbb.rpc.server.service.impl.DefaultServiceFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
* @author binbin.hou
* @since 0.0.1
*/
public class RpcServerHandler extends SimpleChannelInboundHandler {
private static final Log log = LogFactory.getLog(RpcServerHandler.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
final String id = ctx.channel().id().asLongText();
log.info("[Server] channel {} connected " + id);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
final String id = ctx.channel().id().asLongText();
log.info("[Server] channel read start: {}", id);
// 接受客户端请求
RpcRequest rpcRequest = (RpcRequest)msg;
log.info("[Server] receive channel {} request: {}", id, rpcRequest);
// 回写到 client 端
DefaultRpcResponse rpcResponse = handleRpcRequest(rpcRequest);
ctx.writeAndFlush(rpcResponse);
log.info("[Server] channel {} response {}", id, rpcResponse);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
/**
* 处理请求信息
* @param rpcRequest 请求信息
* @return 结果信息
* @since 0.0.6
*/
private DefaultRpcResponse handleRpcRequest(final RpcRequest rpcRequest) {
DefaultRpcResponse rpcResponse = new DefaultRpcResponse();
rpcResponse.seqId(rpcRequest.seqId());
try {
// 获取对应的 service 实现类
// rpcRequest=>invocationRequest
// 执行 invoke
Object result = DefaultServiceFactory.getInstance()
.invoke(rpcRequest.serviceId(),
rpcRequest.methodName(),
rpcRequest.paramTypeNames(),
rpcRequest.paramValues());
rpcResponse.result(result);
} catch (Exception e) {
rpcResponse.error(e);
log.error("[Server] execute meet ex for request", rpcRequest, e);
}
// 构建结果值
return rpcResponse;
}
}
和以前类似,不过 handleRpcRequest 要稍微麻烦一点。
这里需要根据发射,调用对应的方法。
pojo
其中使用的出参、入参实现如下:
RpcRequest
package com.github.houbb.rpc.common.rpc.domain;
import java.util.List;
/**
* 序列化相关处理
* (1)调用创建时间-createTime
* (2)调用方式 callType
* (3)超时时间 timeOut
*
* 额外信息:
* (1)上下文信息
*
* @author binbin.hou
* @since 0.0.6
*/
public interface RpcRequest extends BaseRpc {
/**
* 创建时间
* @return 创建时间
* @since 0.0.6
*/
long createTime();
/**
* 服务唯一标识
* @return 服务唯一标识
* @since 0.0.6
*/
String serviceId();
/**
* 方法名称
* @return 方法名称
* @since 0.0.6
*/
String methodName();
/**
* 方法类型名称列表
* @return 名称列表
* @since 0.0.6
*/
List<String> paramTypeNames();
// 调用参数信息列表
/**
* 调用参数值
* @return 参数值数组
* @since 0.0.6
*/
Object[] paramValues();
}
RpcResponse
package com.github.houbb.rpc.common.rpc.domain;
/**
* 序列化相关处理
* @author binbin.hou
* @since 0.0.6
*/
public interface RpcResponse extends BaseRpc {
/**
* 异常信息
* @return 异常信息
* @since 0.0.6
*/
Throwable error();
/**
* 请求结果
* @return 请求结果
* @since 0.0.6
*/
Object result();
}
BaseRpc
package com.github.houbb.rpc.common.rpc.domain;
import java.io.Serializable;
/**
* 序列化相关处理
* @author binbin.hou
* @since 0.0.6
*/
public interface BaseRpc extends Serializable {
/**
* 获取唯一标识号
* (1)用来唯一标识一次调用,便于获取该调用对应的响应信息。
* @return 唯一标识号
*/
String seqId();
/**
* 设置唯一标识号
* @param traceId 唯一标识号
* @return this
*/
BaseRpc seqId(final String traceId);
}
ServiceFactory-服务工厂
为了便于对所有的 service 实现类统一管理,这里定义 service 工厂类。
ServiceFactory
package com.github.houbb.rpc.server.service;
import com.github.houbb.rpc.server.config.service.ServiceConfig;
import com.github.houbb.rpc.server.registry.ServiceRegistry;
import java.util.List;
/**
* 服务方法类仓库管理类-接口
*
*
* (1)对外暴露的方法,应该尽可能的少。
* (2)对于外部的调用,后期比如 telnet 治理,可以使用比如有哪些服务列表?
* 单个服务有哪些方法名称?
*
* 等等基础信息的查询,本期暂时全部隐藏掉。
*
* (3)前期尽可能的少暴露方法。
* @author binbin.hou
* @since 0.0.6
* @see ServiceRegistry 服务注册,将服务信息放在这个类中,进行统一的管理。
* @see ServiceMethod 方法信息
*/
public interface ServiceFactory {
/**
* 注册服务列表信息
* @param serviceConfigList 服务配置列表
* @return this
* @since 0.0.6
*/
ServiceFactory registerServices(final List<ServiceConfig> serviceConfigList);
/**
* 直接反射调用
* (1)此处对于方法反射,为了提升性能,所有的 class.getFullName() 进行拼接然后放进 key 中。
*
* @param serviceId 服务名称
* @param methodName 方法名称
* @param paramTypeNames 参数类型名称列表
* @param paramValues 参数值
* @return 方法调用返回值
* @since 0.0.6
*/
Object invoke(final String serviceId, final String methodName,
List<String> paramTypeNames, final Object[] paramValues);
}
DefaultServiceFactory
作为默认实现,如下:
package com.github.houbb.rpc.server.service.impl;
import com.github.houbb.heaven.constant.PunctuationConst;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.heaven.util.lang.reflect.ReflectMethodUtil;
import com.github.houbb.heaven.util.util.CollectionUtil;
import com.github.houbb.rpc.common.exception.RpcRuntimeException;
import com.github.houbb.rpc.server.config.service.ServiceConfig;
import com.github.houbb.rpc.server.service.ServiceFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 默认服务仓库实现
* @author binbin.hou
* @since 0.0.6
*/
public class DefaultServiceFactory implements ServiceFactory {
/**
* 服务 map
* @since 0.0.6
*/
private Map<String, Object> serviceMap;
/**
* 直接获取对应的 method 信息
* (1)key: serviceId:methodName:param1@param2@param3
* (2)value: 对应的 method 信息
*/
private Map<String, Method> methodMap;
private static final DefaultServiceFactory INSTANCE = new DefaultServiceFactory();
private DefaultServiceFactory(){}
public static DefaultServiceFactory getInstance() {
return INSTANCE;
}
/**
* 服务注册一般在项目启动的时候,进行处理。
* 属于比较重的操作,而且一个服务按理说只应该初始化一次。
* 此处加锁为了保证线程安全。
* @param serviceConfigList 服务配置列表
* @return this
*/
@Override
public synchronized ServiceFactory registerServices(List<ServiceConfig> serviceConfigList) {
ArgUtil.notEmpty(serviceConfigList, "serviceConfigList");
// 集合初始化
serviceMap = new HashMap<>(serviceConfigList.size());
// 这里只是预估,一般为2个服务。
methodMap = new HashMap<>(serviceConfigList.size()*2);
for(ServiceConfig serviceConfig : serviceConfigList) {
serviceMap.put(serviceConfig.id(), serviceConfig.reference());
}
// 存放方法名称
for(Map.Entry<String, Object> entry : serviceMap.entrySet()) {
String serviceId = entry.getKey();
Object reference = entry.getValue();
//获取所有方法列表
Method[] methods = reference.getClass().getMethods();
for(Method method : methods) {
String methodName = method.getName();
if(ReflectMethodUtil.isIgnoreMethod(methodName)) {
continue;
}
List<String> paramTypeNames = ReflectMethodUtil.getParamTypeNames(method);
String key = buildMethodKey(serviceId, methodName, paramTypeNames);
methodMap.put(key, method);
}
}
return this;
}
@Override
public Object invoke(String serviceId, String methodName, List<String> paramTypeNames, Object[] paramValues) {
//参数校验
ArgUtil.notEmpty(serviceId, "serviceId");
ArgUtil.notEmpty(methodName, "methodName");
// 提供 cache,可以根据前三个值快速定位对应的 method
// 根据 method 进行反射处理。
// 对于 paramTypes 进行 string 连接处理。
final Object reference = serviceMap.get(serviceId);
final String methodKey = buildMethodKey(serviceId, methodName, paramTypeNames);
final Method method = methodMap.get(methodKey);
try {
return method.invoke(reference, paramValues);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RpcRuntimeException(e);
}
}
/**
* (1)多个之间才用 : 分隔
* (2)参数之间采用 @ 分隔
* @param serviceId 服务标识
* @param methodName 方法名称
* @param paramTypeNames 参数类型名称
* @return 构建完整的 key
* @since 0.0.6
*/
private String buildMethodKey(String serviceId, String methodName, List<String> paramTypeNames) {
String param = CollectionUtil.join(paramTypeNames, PunctuationConst.AT);
return serviceId+PunctuationConst.COLON+methodName+PunctuationConst.COLON
+param;
}
}
ServiceRegistry-服务注册类
接口
package com.github.houbb.rpc.server.registry;
/**
* 服务注册类
* (1)每个应用唯一
* (2)每个服务的暴露协议应该保持一致
* 暂时不提供单个服务的特殊处理,后期可以考虑添加
*
* @author binbin.hou
* @since 0.0.6
*/
public interface ServiceRegistry {
/**
* 暴露的 rpc 服务端口信息
* @param port 端口信息
* @return this
* @since 0.0.6
*/
ServiceRegistry port(final int port);
/**
* 注册服务实现
* @param serviceId 服务标识
* @param serviceImpl 服务实现
* @return this
* @since 0.0.6
*/
ServiceRegistry register(final String serviceId, final Object serviceImpl);
/**
* 暴露所有服务信息
* (1)启动服务端
* @return this
* @since 0.0.6
*/
ServiceRegistry expose();
}
实现
package com.github.houbb.rpc.server.registry.impl;
import com.github.houbb.heaven.util.common.ArgUtil;
import com.github.houbb.rpc.common.config.protocol.ProtocolConfig;
import com.github.houbb.rpc.server.config.service.DefaultServiceConfig;
import com.github.houbb.rpc.server.config.service.ServiceConfig;
import com.github.houbb.rpc.server.core.RpcServer;
import com.github.houbb.rpc.server.registry.ServiceRegistry;
import com.github.houbb.rpc.server.service.impl.DefaultServiceFactory;
import java.util.ArrayList;
import java.util.List;
/**
* 默认服务端注册类
* @author binbin.hou
* @since 0.0.6
*/
public class DefaultServiceRegistry implements ServiceRegistry {
/**
* 单例信息
* @since 0.0.6
*/
private static final DefaultServiceRegistry INSTANCE = new DefaultServiceRegistry();
/**
* rpc 服务端端口号
* @since 0.0.6
*/
private int rpcPort;
/**
* 协议配置
* (1)默认只实现 tcp
* (2)后期可以拓展实现 web-service/http/https 等等。
* @since 0.0.6
*/
private ProtocolConfig protocolConfig;
/**
* 服务配置列表
* @since 0.0.6
*/
private List<ServiceConfig> serviceConfigList;
private DefaultServiceRegistry(){
// 初始化默认参数
this.serviceConfigList = new ArrayList<>();
this.rpcPort = 9527;
}
public static DefaultServiceRegistry getInstance() {
return INSTANCE;
}
@Override
public ServiceRegistry port(int port) {
ArgUtil.positive(port, "port");
this.rpcPort = port;
return this;
}
/**
* 注册服务实现
* (1)主要用于后期服务调用
* (2)如何根据 id 获取实现?非常简单,id 是唯一的。
* 有就是有,没有就抛出异常,直接返回。
* (3)如果根据 {@link com.github.houbb.rpc.common.rpc.domain.RpcRequest} 获取对应的方法。
*
* 3.1 根据 serviceId 获取唯一的实现
* 3.2 根据 {@link Class#getMethod(String, Class[])} 方法名称+参数类型唯一获取方法
* 3.3 根据 {@link java.lang.reflect.Method#invoke(Object, Object...)} 执行方法
*
* @param serviceId 服务标识
* @param serviceImpl 服务实现
* @return this
* @since 0.0.6
*/
@Override
@SuppressWarnings("unchecked")
public synchronized DefaultServiceRegistry register(final String serviceId, final Object serviceImpl) {
ArgUtil.notEmpty(serviceId, "serviceId");
ArgUtil.notNull(serviceImpl, "serviceImpl");
// 构建对应的其他信息
ServiceConfig serviceConfig = new DefaultServiceConfig();
serviceConfig.id(serviceId).reference(serviceImpl);
serviceConfigList.add(serviceConfig);
return this;
}
@Override
public ServiceRegistry expose() {
// 注册所有服务信息
DefaultServiceFactory.getInstance()
.registerServices(serviceConfigList);
// 暴露 netty server 信息
new RpcServer(rpcPort).start();
return this;
}
}
ServiceConfig 是一些服务的配置信息,接口定义如下:
package com.github.houbb.rpc.server.config.service;
/**
* 单个服务配置类
*
* 简化用户使用:
* 在用户使用的时候,这个类应该是不可见的。
* 直接提供对应的服务注册类即可。
*
* 后续拓展
* (1)版本信息
* (2)服务端超时时间
*
* @author binbin.hou
* @since 0.0.6
* @param <T> 实现类泛型
*/
public interface ServiceConfig<T> {
/**
* 获取唯一标识
* @return 获取唯一标识
* @since 0.0.6
*/
String id();
/**
* 设置唯一标识
* @param id 标识信息
* @return this
* @since 0.0.6
*/
ServiceConfig<T> id(String id);
/**
* 获取引用实体实现
* @return 实体实现
* @since 0.0.6
*/
T reference();
/**
* 设置引用实体实现
* @param reference 引用实现
* @return this
* @since 0.0.6
*/
ServiceConfig<T> reference(T reference);
}
测试
maven 引入
引入服务端的对应 maven 包:
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>rpc-server</artifactId>
<version>0.0.6</version>
</dependency>
服务端启动
// 启动服务
DefaultServiceRegistry.getInstance()
.register(ServiceIdConst.CALC, new CalculatorServiceImpl())
.expose();
这里注册了一个计算服务,并且设置对应的实现。
和以前实现类似,此处不再赘述。
启动日志:
[DEBUG] [2021-10-05 13:39:42.638] [main] [c.g.h.l.i.c.LogFactory.setImplementation] - Logging initialized using 'class com.github.houbb.log.integration.adaptors.stdout.StdOutExImpl' adapter.
[INFO] [2021-10-05 13:39:42.645] [Thread-0] [c.g.h.r.s.c.RpcServer.run] - RPC 服务开始启动服务端
十月 05, 2021 1:39:43 下午 io.netty.handler.logging.LoggingHandler channelRegistered
信息: [id: 0xec4dc74f] REGISTERED
十月 05, 2021 1:39:43 下午 io.netty.handler.logging.LoggingHandler bind
信息: [id: 0xec4dc74f] BIND: 0.0.0.0/0.0.0.0:9527
十月 05, 2021 1:39:43 下午 io.netty.handler.logging.LoggingHandler channelActive
信息: [id: 0xec4dc74f, L:/0:0:0:0:0:0:0:0:9527] ACTIVE
[INFO] [2021-10-05 13:39:43.893] [Thread-0] [c.g.h.r.s.c.RpcServer.run] - RPC 服务端启动完成,监听【9527】端口
ps: 写到这里忽然发现忘记添加对应的 register 日志了,这里可以添加对应的 registerListener 拓展。
小结
为了便于大家学习,以上源码已经开源:
希望本文对你有所帮助,如果喜欢,欢迎点赞收藏转发一波。
我是老马,期待与你的下次重逢。
java 从零开始手写 RPC (05) reflect 反射实现通用调用之服务端的更多相关文章
- java 从零开始手写 RPC (07)-timeout 超时处理
<过时不候> 最漫长的莫过于等待 我们不可能永远等一个人 就像请求 永远等待响应 超时处理 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RP ...
- java 从零开始手写 RPC (03) 如何实现客户端调用服务端?
说明 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 写完了客户端和服务端,那么如何实现客户端和服务端的 ...
- java 从零开始手写 RPC (04) -序列化
序列化 java 从零开始手写 RPC (01) 基于 socket 实现 java 从零开始手写 RPC (02)-netty4 实现客户端和服务端 java 从零开始手写 RPC (03) 如何实 ...
- java 从零开始手写 RPC (01) 基于 websocket 实现
RPC 解决的问题 RPC 主要是为了解决的两个问题: 解决分布式系统中,服务之间的调用问题. 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑. 这一节我们来学习下如何基于 we ...
- 手写RPC框架指北另送贴心注释代码一套
Angular8正式发布了,Java13再过几个月也要发布了,技术迭代这么快,框架的复杂度越来越大,但是原理是基本不变的.所以沉下心看清代码本质很重要,这次给大家带来的是手写RPC框架. 完整代码以及 ...
- 看了这篇你就会手写RPC框架了
一.学习本文你能学到什么? RPC的概念及运作流程 RPC协议及RPC框架的概念 Netty的基本使用 Java序列化及反序列化技术 Zookeeper的基本使用(注册中心) 自定义注解实现特殊业务逻 ...
- 手写RPC框架(六)整合Netty
手写RPC框架(六)整合Netty Netty简介: Netty是一个基于NIO的,提供异步,事件驱动的网络应用工具,具有高性能高可靠性等特点. 使用传统的Socket来进行网络通信,服务端每一个连接 ...
- 从零开始手写 dubbo rpc 框架
rpc rpc 是基于 netty 实现的 java rpc 框架,类似于 dubbo. 主要用于个人学习,由渐入深,理解 rpc 的底层实现原理. 前言 工作至今,接触 rpc 框架已经有很长时间. ...
- 手写RPC
服务端代码 package com.peiyu.rpcs.bios; import java.io.IOException; public interface IRpcServers { void s ...
随机推荐
- jdbc操作mysql(一)
java.sql包 在使用jdbc之前,我们先看看有关操作jdbc会用到的几个类和接口,通过查看官方文档可知 DriverManager:用于管理一组JDBC驱动程序的基本服务,即管理数据库中的所有驱 ...
- 高性能利器:CDN我建议你好好学一下!
硬核干货分享,欢迎关注[Java补习课]成长的路上,我们一起前行 ! <高可用系列文章> 已收录在专栏,欢迎关注! CDN 概述 CDN 全称 Content Delivery Netwo ...
- LeetCode《买卖股票的最佳时机》系列题目,最详解
目录 说在前面 引例:只能交易一次 一.动态数组定义 二.状态转移方程 三.初始化 四.优化 无限制买卖 一.动态数组定义 二.状态转移方程 三.初始化 四.优化 交易 2 次,最大利润? 一.动态数 ...
- 100个裁判对n个选手做无并列排名问题探析
原题:n 个选手(n ≥ 3)参加花样自行车比赛,100 个裁判独立对各选手的表现给出无并列排名.已知对任意三个选手 A.B.C 和任意三个裁判 X.Y.Z 均不会出现如下的情形:X 给出 A > ...
- 编写你的第一个Django应用
安装 Python 作为一个 Python Web 框架,Django 需要 Python.更多细节请参见 我应该使用哪个版本的 Python 来配合 Django?. Python 包含了一个名为 ...
- 10分钟学会Visual Studio将自己创建的类库打包到NuGet进行引用(net,net core,C#)
前言 NuGet就是一个包(package)管理平台,确切的说是 .net平台的包管理工具,它提供了一系列客户端用于生成,上传和使用包(package),以及一个用于存储所有包的中心库. 对于一个现代 ...
- Redis详解(一)——
Redis详解1 https://www.cnblogs.com/MoYu-zc/p/14985250.html https://www.cnblogs.com/xiaoxiaotank/p/1498 ...
- DH算法图解+数学证明
前几天和同事讨论IKE密钥交换流程时,提到了Diffie-Hellman交换.DH算法最主要的作用便是在不安全的网络上成功公共密钥(并未传输真实密钥).但由于对于DH算法的数学原理则不清楚,因此私下对 ...
- 第09课:GDB 实用调试技巧(下)
本节课的核心内容: 多线程下禁止线程切换 条件断点 使用 GDB 调试多进程程序 多线程下禁止线程切换 假设现在有 5 个线程,除了主线程,工作线程都是下面这样的一个函数: void thread_p ...
- JDK7&JDK9处理异常新特性
1.JDK7新特性是在 try (定义对象,作用域就是try方法体) 复制一个文件实例: 复制文件的原理: 先从硬盘写出到内存中,创建文件输入流对象 FileInputStream fis; 中间是在 ...