编写测试方法:
package com.info.consumer;
import com.info.api.Animal;
public class ConsumerApplication {
public static void main(String[] args) {
ClientProxy proxy = new ClientProxy();
Animal animal = proxy.clientProxy(Animal.class, "localhost", 9000);
final String runResult = animal.run();
final String speakResult = animal.speak();
System.out.println("runResult = " + runResult);
System.out.println("speakResult = " + speakResult);
}
}
一切看似都很完美,但是,当你执行main
方法的时候,oh my god!!!
为什么解码了两次然后就没有然后了?经过仔细排查,定位到问题是由于LengthFieldBasedFrameDecoder
未能按照规定要求读取数据(读取完所有数据还不满足我们代码的限制)
// 如果可读字节数小于消息长度,说明消息还不完整。
if (in.readableBytes() < messageLength) {
// 消息不够 重置读标识
in.resetReaderIndex();
return;
}
最终LengthFieldBasedFrameDecoder
的参数设置对应如下:
new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 4, 4, 9, 0)
对应的CustomClientInitializer
和CustomServerInitializer
都需要进行调整。
关于LengthFieldBasedFrameDecoder
不得不说的事
处理了LengthFieldBasedFrameDecoder
的问题,再次运行代码,不出意外你会得到一个实例化对象失败的异常!
继续排查代码,发现是服务端在收到客户端请求并且解码完成以后,在反射调用方法时实例化对象失败,debug发现是因为传进来的是接口类型,而接口类型不能被实例化,因此在调用clz.newInstance()
时抛出异常。
怎么去获取对应接口的示例呢?这里通过ServiceLoader
来实现,并且ServiceLoader
需要依赖相关的配置,rpc-provider
模块resource目录下新建META-INF/services/com.info.api.Animal
文件,文件内添加 com.info.provider.service.impl.Dog
,这里需要注意的是文件名com.info.api.Animal
是需要对应接口的全限定名(别问为什么,这是规定!),文件的内容是对应接口的实现类的全限定名,多个实现类的情况时,每个实现类单独写一行。配置完成以后通过ServiceLoader.load(clz)
即可获取到对应接口的实现类。
修改代码CustomServerHandler
中的invoke
方法
// 调用目标方法
private Object invoke(Request request) {
final String clzName = request.getClassName();
try {
final Class<?> clz = Class.forName(clzName);
final ServiceLoader<?> serviceLoader = ServiceLoader.load(clz);
final Iterator<?> iterator = serviceLoader.iterator();
Object instance = null;
if (iterator.hasNext()) {
instance = iterator.next();
} else {
log.error("获取 {} 实现类失败", clz);
return null;
}
final Method method = clz.getDeclaredMethod(request.getMethodName(), request.getParameterTypes());
return method.invoke(instance, request.getParams());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return null;
}
修改完成再次运行代码,反序列化失败又是什么鬼?再次排查代码,无果,对了,我们不是可以至此多种序列化方式么?试试 json
!说时迟那时快,
rpc-portocol
模块pom.xml
文件添加依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
serial
包下添加json
的序列化支持
package com.info.protocol.serial;
import com.alibaba.fastjson.JSON;
import com.info.protocol.enums.SerializeTypeEnum;
import java.nio.charset.StandardCharsets;
public class JsonSerializer implements Serializer {
@Override
public <T> byte[] serialize(T obj) {
return JSON.toJSONString(obj).getBytes(StandardCharsets.UTF_8);
}
@Override
public <T> T deserialize(byte[] data, Class<T> clz) {
final String content = new String(data);
return JSON.parseObject(content, clz);
}
@Override
public byte getType() {
return SerializeTypeEnum.JSON.getCode();
}
}
SerializerManager.java
类添加对json序列化的管理
static {
Serializer jdkSerializer = new JdkSerializer();
Serializer jsonSerializer = new JsonSerializer();
serializerMap.put((int) jdkSerializer.getType(), jdkSerializer);
serializerMap.put((int) jsonSerializer.getType(), jsonSerializer);
}
SerializeTypeEnum.java
添加 json 的序列化方式枚举
JSON((byte) 2);
最后把我们使用的序列化方式改为 json,CustomInvocationHandler
的invoke
方法,生成header
对象时,使用 json 序列化方式
Header header = new Header(CommonConstant.MAGIC, CommonConstant.PROTOCOL_VERSION,
SerializeTypeEnum.JSON.getCode(), 0, MessageTypeEnum.REQUEST.getCode(), requestId);
打完收工!so easy!
再次运行代码,刚刚真是 一顿操作猛如虎回头一看原地杵!
,反序列化失败的异常依然存在。
最终debug定位到代码,服务端在完成反射调用方法后组装结果时,portocol
对象中conent
的内容应该是一个Response
对象而不是反射调用方法返回的结果,修改CustomServerHandler.java
的 channelRead0
方法
@Override
protected void channelRead0(ChannelHandlerContext ctx, Protocol<Request> msg) throws Exception {
Protocol protocol = new Protocol<>();
final Header header = msg.getHeader();
header.setMessageType(MessageTypeEnum.RESPONSE.getCode());
// 反射调用目标方法
Object result = invoke(msg.getContent());
protocol.setHeader(header);
protocol.setContent(new Response(result,"success"));
ctx.writeAndFlush(protocol);
}
事实证明,写代码一定要细心!
再次运行代码,不出意外的话控制台打印出了如下的结果:
runResult = 小狗跑起来一蹦一跳…
speakResult = 小狗叫起来汪汪汪…
再出意外的话,需要你细细调试了(当然也可以留言,大家一起学习),相信我,当你得到上面的结果,一定会收货颇多。
系列文章传送门如下:
手写RPC(一) 絮絮叨叨
手写RPC(二) 碎碎念
手写RPC(三) 基础结构搭建
手写RPC(四) 核心模块网络协议模块编写 ---- netty服务端
手写RPC(五) 核心模块网络协议模块编写 ---- 自定义协议
手写RPC(六) 核心模块网络协议模块编写 ---- 实现编解码器
手写RPC(七) 核心模块网络协议模块编写 ---- 实现客户端
手写RPC(八) provider、consumer 实现
手写RPC(十) 优化
关于 LengthFieldBasedFrameDecoder 不得不说的事