现在分布式系统大行其道的年代,RPC 有着无足轻重的位置。风靡的 Duboo、Thrift、gRpc 等框架各领风骚,深刻理解RPC是老手也是老鸟的必修课。你晓得 RPC 的实现原理吗?想入手实现一个简略的 RPC 框架吗?本文将通过一个 RPC 我的项目带你寻找答案,大量代码展现,干货满满,如果你能再钻研该我的项目代码,置信你能播种到包含不限于 RPC 原理、Java 根底(注解、反射、同步器、Future、SPI、动静代理)、Javassist 字节码加强、服务注册与发现、Netty 网络通讯、传输协定、序列化、包压缩、TCP 粘包\拆包、长连贯复用、心跳检测、SpringBoot 主动装载、服务分组、接口版本、客户端连接池、负载平衡、异步调用等等重点常识。
RPC定义
近程服务调用(Remote procedure call)的概念历史已久,1981年就曾经被提出,最后的目标就是为了调用近程办法像调用本地办法一样简略,经验了四十多年的更新与迭代,RPC 的大体思路曾经趋于稳定,现在百家争鸣的 RPC 协定和框架,诸如 Dubbo (阿里)、Thrift(FaceBook)、gRpc(Google)、brpc (百度)等都在不同侧重点去解决最后的目标,有的想极致完满,有的谋求极致性能,有的偏差极致简略。
RPC原理
让咱们回到 RPC 最后的目标,要想实现调用近程办法像调用本地办法一样简略,至多要解决如下问题:
- 如何获取可用的近程服务器
- 如何示意数据
- 如何传递数据
- 服务端如何确定并调用指标办法
上述四点问题,都能与当初分布式系统炽热的术语一一对应,如何获取可用的近程服务器(服务注册与发现)、如何示意数据(序列化与反序列化)、如何传递数据(网络通讯)、服务端如何确定并调用指标办法(调用办法映射)。笔者将通过一个简略 RPC 我的项目来解决这些问题。
首先来看 RPC 的整体零碎架构图:
图中服务端启动时将本人的服务节点信息注册到注册核心,客户端调用近程办法时会订阅注册核心中的可用服务节点信息,拿到可用服务节点之后近程调用办法,当注册核心中的可用服务节点发生变化时会告诉客户端,防止客户端持续调用曾经生效的节点。那客户端是如何调用近程办法的呢,来看一下近程调用示意图:
- 客户端模块代理所有近程办法的调用
- 将指标服务、指标办法、调用指标办法的参数等必要信息序列化
- 序列化之后的数据包进一步压缩,压缩后的数据包通过网络通信传输到指标服务节点
- 服务节点将承受到的数据包进行解压
- 解压后的数据包反序列化成指标服务、指标办法、指标办法的调用参数
- 通过服务端代理调用指标办法获取后果,后果同样须要序列化、压缩而后回传给客户端
通过以上形容,置信读者应该大体上理解了 RPC 是如何工作的,接下来看如何应用代码具体实现上述的流程。鉴于篇幅笔者会抉择重要或者网络上介绍绝对较少的模块来讲述。
RPC实现细节
服务注册与发现
零碎选用 Zookeeper 作为注册核心, ZooKeeper 将数据保留在内存中,性能很高。 在读
多写
少的场景中尤其实用,因为写
操作会导致所有的服务器间同步状态。服务注册与发现是典型的读
多写
少的协调服务场景。 Zookeeper 是一个典型的CP零碎,在服务选举或者集群半数机器宕机时是不可用状态,绝对于服务发现中支流的AP零碎来说,可用性稍低,然而用于了解RPC的实现,也是入不敷出。
ZooKeeper节点
- 长久节点( PERSISENT ):一旦创立,除非被动调用删除操作,否则始终长久化存储。
- 长期节点( EPHEMERAL ):与客户端会话绑定,客户端会话生效,这个客户端所创立的所有长期节点都会被删除除。
- 节点程序( SEQUENTIAL ):创立子节点时,如果设置SEQUENTIAL属性,则会主动在节点名后追加一个整形数字,下限是整形的最大值;同一目录下共享程序,例如(/a0000000001,/b0000000002,/c0000000003,/test0000000004)。
服务注册
在 ZooKeeper 根节点下依据服务名创立长久节点 /rpc/{serviceName}/service
,将该服务的所有服务节点应用长期节点创立在 /rpc/{serviceName}/service
目录下,代码如下(为不便展现,后续展现代码与我的项目代码相比都做了删减):
public void exportService(Service serviceResource) {
String name = serviceResource.getName();
String uri = GSON.toJson(serviceResource);
String servicePath = "rpc/" + name + "/service";
zkClient.createPersistent(servicePath, true);
String uriPath = servicePath + "/" + uri;
//创立一个新的长期节点,当该节点宕机会话生效时,该长期节点会被清理
zkClient.createEphemeral(uriPath);
}
注册成果如图,本地启动两个服务则 service 下有两个服务节点信息:
存储的节点信息包含服务名,服务 IP:PORT ,序列化协定,压缩协定等。
服务发现
客户端启动后,不会立刻从注册核心获取可用服务节点,而是在调用近程办法时获取节点信息(懒加载),并放入本地缓存 MAP 中,供后续调用,当注册核心告诉目录变动时清空服务所有节点缓存,代码如下:
public List<Service> getServices(String name) {
Map<String, List<Service>> SERVER_MAP = new ConcurrentHashMap<>();
String servicePath = "rpc/" + name + "/service";
List<String> children = zkClient.getChildren(servicePath);
List<Service> serviceList = Optional.ofNullable(children).orElse(new ArrayList<>()).stream().map(str -> {
String deCh = null;
deCh = URLDecoder.decode(str, StandardCharsets.UTF_8.toString());
return gson.fromJson(deCh, Service.class);
}).collect(Collectors.toList());
SERVER_MAP.put(name, serviceList);
return serviceList;
}
public class ZkChildListenerImpl implements IZkChildListener {
//监听子节点的删除和新增事件
@Override
public void handleChildChange(String parentPath, List<String> childList) throws Exception {
//有变动就清空服务所有节点缓存
String[] arr = parentPath.split("/");
SERVER_MAP.remove(arr[2]);
}
}
PS:美团分布式 ID 生成零碎Leaf就应用 Zookeeper 的程序节点来注册 WorkerID ,长期节点保留节点 IP:PORT 信息。
Client
客户端调用本地办法一样调用近程办法的完满体验与 Java 动静代理的弱小密不可分。
DefaultRpcBaseProcessor
抽象类实现了 ApplicationListener
, onApplicationEvent
办法在 Spring 我的项目启动结束会收到工夫告诉,获取 ApplicationContext
上下文之后开始注入服务 injectService
(有依赖服务)或者启动服务 startServer
(有服务实现)。
injectService
办法会遍历 ApplicationContext
上下文中的所有 Bean
, Bean
中是否有属性应用了 InjectService
注解。有的话生成代理类,注入到 Bean
的属性中。代码如下:
public abstract class DefaultRpcBaseProcessor implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//Spring启动结束会收到Event
if (Objects.isNull(contextRefreshedEvent.getApplicationContext().getParent())) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
//保留spring上下文 后续应用
Container.setSpringContext(applicationContext);
startServer(applicationContext);
injectService(applicationContext);
}
}
private void injectService(ApplicationContext context) {
String[] names = context.getBeanDefinitionNames();
for (String name : names) {
Object bean = context.getBean(name);
Class<?> clazz = bean.getClass();
if (AopUtils.isCglibProxy(bean)) {
//aop加强的类生成cglib类,须要Superclass能力获取定义的字段
clazz = clazz.getSuperclass();
} else if(AopUtils.isJdkDynamicProxy(bean)) {
//动静代理类,可能也须要
clazz = clazz.getSuperclass();
}
Field[] declaredFields = clazz.getDeclaredFields();
//设置InjectService的代理类
for (Field field : declaredFields) {
InjectService injectService = field.getAnnotation(InjectService.class);
if (injectService == null) {
continue;
}
Class<?> fieldClass = field.getType();
Object object = context.getBean(name);
field.set(object, clientProxyFactory.getProxy(fieldClass, injectService.group(), injectService.version()));
ServerDiscoveryCache.SERVER_CLASS_NAMES.add(fieldClass.getName());
}
}
}
protected abstract void startServer(ApplicationContext context);
}
调用 ClientProxyFactory
类的 getProxy
,依据服务接口、服务分组、服务版本、是否异步调用来创立该接口的代理类,对该接口的所有办法都会应用创立的代理类来调用。办法调用的实现细节都在 ClientInvocationHandler
中的 invoke
办法,次要内容是,获取服务节点信息,抉择调用节点,构建 request 对象,最初调用网络模块发送申请。
public class ClientProxyFactory {
public <T> T getProxy(Class<T> clazz, String group, String version, boolean async) {
if (async) {
return (T) asyncObjectCache.computeIfAbsent(clazz.getName() + group + version, clz -> Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ClientInvocationHandler(clazz, group, version, async)));
} else {
return (T) objectCache.computeIfAbsent(clazz.getName() + group + version, clz -> Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new ClientInvocationHandler(clazz, group, version, async)));
}
}
private class ClientInvocationHandler implements InvocationHandler {
private Class<?> clazz;
private boolean async;
private String group;
private String version;
public ClientInvocationHandler(Class<?> clazz, String group, String version, boolean async) {
this.clazz = clazz;
this.async = async;
this.group = group;
this.version = version;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//1. 取得服务信息
String serviceName = clazz.getName();
List<Service> serviceList = getServiceList(serviceName);
Service service = loadBalance.selectOne(serviceList);
//2. 构建request对象
RpcRequest rpcRequest = new RpcRequest();
rpcRequest.setServiceName(service.getName());
rpcRequest.setMethod(method.getName());
rpcRequest.setGroup(group);
rpcRequest.setVersion(version);
rpcRequest.setParameters(args);
rpcRequest.setParametersTypes(method.getParameterTypes());
//3. 协定编组
RpcProtocolEnum messageProtocol = RpcProtocolEnum.getProtocol(service.getProtocol());
RpcCompressEnum compresser = RpcCompressEnum.getCompress(service.getCompress());
RpcResponse response = netClient.sendRequest(rpcRequest, service, messageProtocol, compresser);
return response.getReturnValue();
}
}
}
网络传输
客户端封装调用申请对象之后须要通过网络将调用信息发送到服务端,在发送申请对象之前还须要经验序列化、压缩两个阶段。
序列化与反序列化
序列化与反序列化的核心作用就是对象的保留与重建,不便客户端与服务端通过字节流传递对象,疾速对接交互。
- 序列化就是指把 Java 对象转换为字节序列的过程。
- 反序列化就是指把字节序列复原为 Java 对象的过程。
Java序列化的形式有很多,诸如 JDK 自带的 Serializable
、 Protobuf
、 kryo
等,上述三种笔者自测性能最高的是 Kryo
、其次是 Protobuf
。 Json
也不失为一种简略且高效的序列化办法,有很多大道至简的框架采纳。序列化接口比较简单,读者能够自行查看实现代码。
public interface MessageProtocol {
byte[] marshallingRequest(RpcRequest request) throws Exception;
RpcRequest unmarshallingRequest(byte[] data) throws Exception;
byte[] marshallingResponse(RpcResponse response) throws Exception;
RpcResponse unmarshallingResponse(byte[] data) throws Exception;
}
压缩与解压
网络通信的老本很高,为了减小网络传输数据包的体积,将序列化之后的字节码压缩不失为一种很好的抉择。Gzip 压缩算法比率在3到10倍左右,能够大大节俭服务器的网络带宽,各种风行的 web 服务器也都反对 Gzip 压缩算法。 Java 接入也比拟容易,接入代码能够查看下方接口的实现。
public interface Compresser {
byte[] compress(byte[] bytes);
byte[] decompress(byte[] bytes);
}
网络通信
万事俱备只欠东风。将申请对象序列化成字节码,并且压缩体积之后,须要应用网络将字节码传输到服务器。罕用网络传输协定有 HTTP 、 TCP 、 WebSocke t等。HTTP、WebSocket 是应用层协定,TCP 是传输层协定。有些谋求简洁、易用的 RPC 框架也有抉择 HTTP 协定的。TCP传输的高可靠性和极致性能是支流RPC框架抉择的最次要起因。谈到 Java 生态的通信畛域,Netty
的领衔位置短时间内无人能及。选用 Netty 作为网络通信模块, TCP 数据流的粘包、拆包不可避免。
粘包、拆包
TCP 传输协定是一种面向连贯的、牢靠的、基于字节流的传输层通信协议。为了最大化传输效率。发送方可能将单个较小数据包合并发送,这种状况就须要接管方来拆包解决数据了。
Netty 提供了3种类型的解码器来解决 TCP 粘包/拆包问题:
- 定长音讯解码器:
FixedLengthFrameDecoder
。发送方和接管方规定一个固定的音讯长度,不够用空格等字符补全,这样接管方每次从承受到的字节流中读取固定长度的字节即可,长度不够就保留本次承受的数据,再在下一个字节流中获取剩下数量的字节数据。 - 分隔符解码器:
LineBasedFrameDecoder
或DelimiterBasedFrameDecoder
。LineBasedFrameDecoder
是行分隔符解码器,分隔符为\n
或\r\n
;DelimiterBasedFrameDecoder
是自定义分隔符解码器,能够定义一个或多个分隔符。接收端在收到的字节流中查找分隔符,而后返回分隔符之前的数据,没找到就持续从下一个字节流中查找。 - 数据长度解码器:
LengthFieldBasedFrameDecoder
。将发送的音讯分为 header 和 body,header 存储音讯的长度(字节数),body 是发送的音讯的内容。同时发送方和接管方要协商好这个 header 的字节数,因为 int 能示意长度,long 也能示意长度。接管方首先从字节流中读取前n(header的字节数)个字节(header),而后依据长度读取等量的字节,不够就从下一个数据流中查找。
不想应用内置的解码器也可自定义解码器,自定传输协定。
网络通信这部分内容比较复杂,说来话长,代码易读,读者可先自行浏览代码。后续有机会细说此节内容。
Server
客户端通过网络传输将申请对象序列化、压缩之后的字节码传输到服务端之后,同样先通过解压、反序列化将字节码重建为申请对象。有了申请对象之后,就能够进行要害的办法调用环节了。
public abstract class RequestBaseHandler {
public RpcResponse handleRequest(RpcRequest request) throws Exception {
//1. 查找指标服务代理对象
ServiceObject serviceObject = serverRegister.getServiceObject(request.getServiceName() + request.getGroup() + request.getVersion());
RpcResponse response = null;
//2. 调用对应的办法
response = invoke(serviceObject, request);
//响应客户端
return response;
}
/**
* 具体代理调用
*/
public abstract RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception;
}
上述抽象类 RequestBaseHandler
是调用服务办法的形象实现 handleRequest
通过申请对象的服务名、服务分组、服务版本在 serverRegister.getServiceObject
获取代理对象。而后调用 invoke
形象办法来真正通过代理对象调用办法取得后果。
- 服务的代理对象怎么产生的?
- 如何通过代理对象调用办法?
生成服务代理对象
带着上述问题来看 DefaultRpcBaseProcessor
抽象类:
public abstract class DefaultRpcBaseProcessor implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//Spring启动结束会收到Event
if (Objects.isNull(contextRefreshedEvent.getApplicationContext().getParent())) {
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
Container.setSpringContext(applicationContext);
startServer(applicationContext);
injectService(applicationContext);
}
}
private void injectService(ApplicationContext context) {}
protected abstract void startServer(ApplicationContext context);
}
DefaultRpcBaseProcessor
抽象类也有两个实现类 DefaultRpcReflectProcessor
和 DefaultRpcJavassistProcessor
,来实现要害的生成代理对象的 startServer
办法。
服务接口实现类的 Bean
作为代理对象
public class DefaultRpcReflectProcessor extends DefaultRpcBaseProcessor {
@Override
protected void startServer(ApplicationContext context) {
Map<String, Object> beans = context.getBeansWithAnnotation(RpcService.class);
if (beans.size() > 0) {
boolean startServerFlag = true;
for (Object obj : beans.values()) {
Class<?> clazz = obj.getClass();
Class<?>[] interfaces = clazz.getInterfaces();
/* 如果只实现了一个接口就用接口的className作为服务名
* 如果该类实现了多个接口,则应用注解里的value作为服务名
*/
RpcService service = clazz.getAnnotation(RpcService.class);
if (interfaces.length != 1) {
String value = service.value();
ServiceObject so = new ServiceObject(value, Class.forName(value), obj, service.group(), service.version());
} else {
Class<?> supperClass = interfaces[0];
ServiceObject so = new ServiceObject(supperClass.getName(), supperClass, obj, service.group(), service.version());
}
serverRegister.register(so);
}
}
}
}
DefaultRpcReflectProcessor
中获取到所有有 RpcService
注解的服务接口实现类 Bean
,而后将该 Bean
作为服务代理对象注册到 serverRegister
中供上述的反射调用中应用。
应用 Javassist
生成新的代理对象
public class DefaultRpcJavassistProcessor extends DefaultRpcBaseProcessor {
@Override
protected void startServer(ApplicationContext context) {
Map<String, Object> beans = context.getBeansWithAnnotation(RpcService.class);
if (beans.size() > 0) {
boolean startServerFlag = true;
for (Map.Entry<String, Object> entry : beans.entrySet()) {
String beanName = entry.getKey();
Object obj = entry.getValue();
Class<?> clazz = obj.getClass();
Class<?>[] interfaces = clazz.getInterfaces();
Method[] declaredMethods = clazz.getDeclaredMethods();
/*
* 如果只实现了一个接口就用接口的className作为服务名
* 如果该类实现了多个接口,则应用注解里的value作为服务名
*/
RpcService service = clazz.getAnnotation(RpcService.class);
if (interfaces.length != 1) {
String value = service.value();
//bean实现多个接口时,javassist代理类中生成的办法只依照注解指定的服务类来生成
declaredMethods = Class.forName(value).getDeclaredMethods();
Object proxy = ProxyFactory.makeProxy(value, beanName, declaredMethods);
ServiceObject so = new ServiceObject(value, Class.forName(value), proxy, service.group(), service.version());
} else {
Class<?> supperClass = interfaces[0];
Object proxy = ProxyFactory.makeProxy(supperClass.getName(), beanName, declaredMethods);
ServiceObject so = new ServiceObject(supperClass.getName(), supperClass, proxy, service.group(), service.version());
}
serverRegister.register(so);
}
}
}
}
DefaultRpcJavassistProcessor
与 DefaultRpcReflectProcessor
的差别在于后者间接将服务实现类对象 Bean
作为服务代理对象,而前者通过 ProxyFactory.makeProxy(value, beanName, declaredMethods)
创立了新的代理对象,将新的代理对象注册到 serverRegister
中供后续调用调用中应用。该办法通过 Javassist
来生成代理类,代码简短,倡议浏览源码。我来通过上面的代码演示实现的代理类。
首先咱们的服务接口是:
public interface HelloService {
String hello(String name);
}
服务的实现类是:
@RpcService
public class HelloServiceImpl implements HelloService {
@Override
public String hello(String name) {
return "a1";
}
}
那最终新生成的代理类是这样的:
public class HelloService$proxy1649315143476 {
private static cn.ppphuang.rpcspringstarter.service.HelloService serviceProxy =
((org.springframework.context.ApplicationContext)cn.ppphuang.rpcspringstarter.server.Container.getSpringContext()).getBean("helloServiceImpl");
public cn.ppphuang.rpcspringstarter.common.model.RpcResponse hello(cn.ppphuang.rpcspringstarter.common.model.RpcRequest request) throws java.lang.Exception {
java.lang.Object[] params = request.getParameters();
if(params.length == 1
&& (params[0] == null||params[0].getClass().getSimpleName().equalsIgnoreCase("String"))){
java.lang.String arg0 = null;
arg0 = cn.ppphuang.rpcspringstarter.util.ConvertUtil.convertToString(params[0]);
java.lang.String returnValue = serviceProxy.hello(arg0);
return new cn.ppphuang.rpcspringstarter.common.model.RpcResponse(returnValue);
}
}
public cn.ppphuang.rpcspringstarter.common.model.RpcResponse invoke(cn.ppphuang.rpcspringstarter.common.model.RpcRequest request) throws java.lang.Exception {
String methodName = request.getMethod();
if(methodName.equalsIgnoreCase("hello")){
java.lang.Object returnValue = hello(request);
return returnValue;
}
}
}
清理全限定类名:
public class HelloService$proxy1649315143476 {
private static HelloService serviceProxy = ((ApplicationContext)Container.getSpringContext()).getBean("helloServiceImpl");
public RpcResponse hello(RpcRequest request) throws Exception {
Object[] params = request.getParameters();
if(params.length == 1
&& (params[0] == null|| params[0].getClass().getSimpleName().equalsIgnoreCase("String"))){
String arg0 = null;
arg0 = ConvertUtil.convertToString(params[0]);
String returnValue = serviceProxy.hello(arg0);
return new RpcResponse(returnValue);
}
}
public RpcResponse invoke(RpcRequest request) throws Exception {
String methodName = request.getMethod();
if(methodName.equalsIgnoreCase("hello")){
Object returnValue = hello(request);
return returnValue;
}
}
}
- 代理类
HelloService$proxy1649315143476
中有一个服务接口类型HelloService
的动态属性serviceProxy
,值就是通过ApplicationContext
上下文获取到的服务接口实现类HelloServiceImpl
这个Bean
(SpringContext
曾经被提前缓存到Container
类中,读者能够自行查找代码理解)。 public RpcResponse invoke(RpcRequest request) throws Exception
该办法判断调用的办法名是hello
来调用代理类中的hello
办法。public RpcResponse hello(RpcRequest request) throws Exception
该办法通过调用serviceProxy.hello()
的办法获取后果。
public interface InvokeProxy {
/**
* invoke调用服务接口
*/
RpcResponse invoke(RpcRequest rpcRequest) throws Exception;
}
HelloService$proxy1649315143476
类实现 InvokeProxy
接口(ProxyFactory.makeProxy
代码中有体现)。InvokeProxy
接口只有一个 invoke
办法。到这里就能了解通过调用代理对象的 invoke
办法就能间接调用到服务接口实现类 HelloServiceImpl
的对应办法了。
调用代理对象办法
理清代理对象的生成之后,开始调用代理对象的办法。
上文中写到的抽象类 RequestBaseHandler
有两个实现类 RequestJavassistHandler
和 RequestReflectHandler
。
Java 反射调用
先看 RequestReflectHandler
:
public class RequestReflectHandler extends RequestBaseHandler {
@Override
public RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception {
Method method = serviceObject.getClazz().getMethod(request.getMethod(), request.getParametersTypes());
Object value = method.invoke(serviceObject.getObj(), request.getParameters());
RpcResponse response = new RpcResponse(RpcStatusEnum.SUCCESS);
response.setReturnValue(value);
return response;
}
}
Object value = method.invoke(serviceObject.getObj(), request.getParameters());
这行代码都很相熟,用 Java 框架中最常见的反射来调用代理类中的办法,大部分 RPC 框架也都是这么来实现的。
通过 Javassists 生成的代理对象 invoke
办法调用
接着看 RequestJavassistHandler
:
public class RequestJavassistHandler extends RequestBaseHandler {
@Override
public RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception {
InvokeProxy invokeProxy = (InvokeProxy) serviceObject.getObj();
return invokeProxy.invoke(request);
}
}
间接将代理对象转为 InvokeProxy
,调用 InvokeProxy.invoke()
办法取得返回值,如果这里不能了解,回头再看一下应用 Javassist
生成新的代理对象这个大节吧。
调用代理对象的办法获取到后果,仍要通过序列化、压缩后,将字节流数据包通过网络传输到客户端,客户端拿到响应的后果再解压,反序列化失去后果对象。
Javassist
Javassist
是一个开源的剖析、编辑和创立Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba(千叶滋)
所创立的。简略来说就是用源码级别的 api 去批改字节码。Duboo
、MyBatis
也都应用了 Javassist
。Duboo 作者也抉择Javassist
作为 Duboo 的代理工具,能够点击这里查看 Duboo 作者也抉择 Javassist
的起因。
Javassist
还能谐和(pojie)Java 编写的商业软件,例如抓包工具 Charles
。代码在这里,供交流学习。
在应用 Javassist
有踩到如下坑,供大家参考:
-
Javassist
是运行时,没有JDK
动态编译过程,JDK
的很多语法糖都是在动态编译过程中解决的,所以须要自行编码解决,例如主动拆装箱。int i = 1; Integer ii = i; //javassist 谬误 JDK会主动装箱,javassist须要自行编码解决 int i = 1; Integer ii = new Integer(i); //javassist 正确
- 自定义的类须要应用类的齐全限定名,这也是为什么生成的代理类中类都是齐全限定名。
抉择哪种代理形式
能够通过配置文件 application.properties
批改 hp.rpc.server-proxy-type
的值来抉择代理模式。
性能测试,机器 Macbook Pro M1 8C 16G, 代码如下:
@Autowired
ClientProxyFactory clientProxyFactory;
@Test
void contextLoads() {
long l1 = System.currentTimeMillis();
HelloService proxy = clientProxyFactory.getProxy(HelloService.class,"group3","version3");
for (int i = 0; i < 1000000; i++) {
String ppphuang = proxy.hello("ppphuang");
}
long l2 = System.currentTimeMillis();
long l3 = l2 - l1;
System.out.println(l3);
}
测试后果(ms):
申请次数 | 反射调用1 | 反射调用2 | 反射调用3 | Javassist1 | Javassist2 | Javassist3 |
---|---|---|---|---|---|---|
10000 | 1303 | 1159 | 1164 | 1126 | 1235 | 1094 |
100000 | 6110 | 6103 | 6065 | 6259 | 5854 | 6178 |
1000000 | 54475 | 51890 | 52329 | 52560 | 52099 | 52794 |
测试后果差别并不大,Javassist
模式下只是稍快了一点点,简直能够疏忽不记。与Duboo作者博客6楼评论的测试后果统一。所以想简略通用性强用反射模式,也能够通过应用 Javassist
模式来学习更多常识,因为 Javassist
须要本人兼容很多非凡的情况,反射调用 JDK 曾经帮你兼容完了。
总结
写到这里咱们理解了 RPC 的基本原理、服务注册与发现、客户端代理、网络传输、重点介绍了服务端的两种代理模式,学习 Javassist
如何实现代理。
还有很多货色没有重点讲述甚至没有提及,例如粘\拆包的解决、自定义数据包协定、Javassist
模式下如何实现办法重载、如何解决一个服务接口类有多个实现、如何解决一个实现类实现了多个服务接口、在 SpringBoot
中如何主动装载、如何开箱即用、怎么实现异步调用、怎么扩大序列化、压缩算法等等…有趣味的读者能够在源码中寻找答案,或者寻找优化项,当然也能够寻找 bug 。如果读者能了解整个我的项目的实现,置信你肯定会有所播种。后续有机会也会再写文章与大家交流学习。因笔者程度无限,不欠缺的中央请大家斧正。感激各位的浏览,谢谢。
附
我的项目地址:https://github.com/ppphuang/r…
测试DEMO:https://github.com/ppphuang/r…
发表回复