源码模拟Feign RPC动态代理的实现

Posted Java架构师联盟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码模拟Feign RPC动态代理的实现相关的知识,希望对你有一定的参考价值。

模拟Feign RPC动态代理的实现

由于Feign的组件依赖多,它的InvocationHandler调用处理器的内部实现比较复杂,为了便于大家理解,这里模拟Feign远程调用的动态代理模式设计一个参考实例,作为正式学习的铺垫。

模拟Feign RPC代理模式涉及的类如图3-6所示。

图3-6 模拟Feign RPC代理模式之UML类图

模拟Feign的方法处理器MethodHandler

由于每个RPC客户端类一般会包含多个远程调用方法,因此Feign为远程调用方法封装了一个专门的接口——MethodHandler(方法处理器),此接口很简单,仅仅包含一个invoke(...)抽象方法。

这里,首先对Feign的方法处理器MethodHandler进行模拟,模拟的RPC方法处理器接口如下:

package com.crazymaker.demo.proxy.FeignMock;
/**
*RPC方法处理器
*/

interface RpcMethodHandler
{
/**
*功能:组装URL,完成REST RPC远程调用,并且返回JSON结果
*
*@param argv RPC方法的参数
*@return REST接口的响应结果
*@throws Throwable异常
*/

Object invoke(Object[] argv) throws Throwable;
}

模拟的RPC方法处理器只有一个抽象方法invoke(Object[]),该方法在进行RPC调用时需要完成URL的组装、执行RPC请求并且将响应封装成Java POJO实例,然后返回。

模拟方法处理器RpcMethodHandler接口的实现类如下:

package com.crazymaker.demo.proxy.FeignMock;
//省略import
@Slf4j
public class MockRpcMethodHandler implements RpcMethodHandler
{
/**
*REST URL的前面部分一般来自于Feign远程调用接口的类级别注解
如 *如 "http://crazydemo.com:7700/demo-provider/";
*/

final String contextPath;
/**
*REST URL的前面部分来自于远程调用Feign接口的方法级别的注解
*如 "api/demo/hello/v1";
*/

final String url;
public MockRpcMethodHandler(String contextPath, String url)
{
this.contextPath = contextPath;
this.url = url;
}
/**
*功能:组装URL,完成REST RPC远程调用,并且返回JSON结果
*
*@param argv RPC方法的参数
*@return REST接口的响应结果
*@throws Throwable异常
*/

@Override
public Object invoke(Object[] argv) throws Throwable
{
/**
*组装REST接口URL
*/

String restUrl = contextPath + MessageFormat.format(url, argv);
log.info("restUrl={}", restUrl);
/**
*通过HttpClient组件调用REST接口
*/

String responseData = HttpRequestUtil.simpleGet(restUrl);
/**
*解析REST接口的响应结果,解析成JSON对象并且返回
*/

RestOut<JSONObject> result = JsonUtil.jsonToPojo(responseData,
new TypeReference<RestOut<JSONObject>>() {});
return result;
}
}

在模拟方法处理器实现类MockRpcMethodHandler的invoke(Object[])完成了以下3个工作:

(1)组装URL,将来自RPC的请求上下文路径(一般来自RPC客户端类级别注解)和远程调用的方法级别的URI路径拼接在一起,组成完整的URL路径。

(2)通过HttpClient组件(也可以是其他组件)发起HTTP请求,调用服务端的REST接口。

(3)解析REST接口的响应结果,解析成POJO对象(这里是JSON对象)并且返回。

模拟Feign的调用处理器InvocationHandler

调用处理器FeignInvocationHandler是一个相对简单的类,拥有一个非常重要的Map类型的成员dispatch,保存着RPC方法反射实例到其MethodHandler方法处理器的映射。

这里设计了一个模拟调用处理器MockInvocationHandler,用于模拟FeignInvocationHandler调用处理器,模拟调用处理器同样拥有一个Map类型的成员dispatch,负责保存RPC方法反射实例到模拟方法处理器MockRpcMethodHandler之间的映射。一个运行时MockInvocationHandler模拟调用处理器实例的dispatch成员的内存结构图如图3-7所示。

源码模拟Feign RPC动态代理的实现

图3-7 一个运行时MockInvocationHandler的dispatch成员的内存结构

MockInvocationHandler通过Java反射扫描模拟RPC远程调用接口MockDemoClient中的每一个方法的反射注解,组装出一个对应的Map映射实例,它的key值为RPC方法的反射实例,value值为MockRpcMethodHandler方法的处理器实例。

MockInvocationHandler的源代码如下:package com.crazymaker.demo.proxy.FeignMock;
//省略import
class MockInvocationHandler implements InvocationHandler
{
/**
*远程调用的分发映射:根据方法名称分发方法处理器
*key:远程调用接口的方法反射实例
*value:模拟的方法处理器实例
*/

private Map<Method, RpcMethodHandler> dispatch;
/**
*功能:代理对象的创建
*@param clazz被代理的接口类型
*@return代理对象
*/

public static <T> T newInstance(Class<T> clazz)
{
/**
*从远程调用接口的类级别注解中获取REST地址的contextPath部分
*/

Annotation controllerAnno =
clazz.getAnnotation (RestController.class);
if (controllerAnno == null)
{
return null;
}
String contextPath = ((RestController) controllerAnno).value();
//创建一个调用处理器实例
MockInvocationHandler invokeHandler = new MockInvocationHandler();
invokeHandler.dispatch = new LinkedHashMap<>();
/**
*通过反射迭代远程调用接口的每一个方法,组装MockRpcMethodHandler处理器
*/

for (Method method : clazz.getMethods())
{
Annotation methodAnnotation =
method.getAnnotation (GetMapping.class);
if (methodAnnotation == null)
{
continue;
}
/**
*从远程调用接口的方法级别注解中获取REST地址的URI部分
*/

String uri = ((GetMapping)methodAnnotation).name();
/**
*组装MockRpcMethodHandler模拟方法处理器
*注入REST地址的contextPath部分和URI部分
*/

MockRpcMethodHandler handler =
new MockRpcMethodHandler (contextPath, uri);
/**
*重点:将模拟方法处理器handler实例缓存到dispatch映射中
*key为方法反射实例,value为方法处理器
*/

invokeHandler.dispatch.put(method, handler);
}
//创建代理对象
T proxy = (T) Proxy.newProxyInstance(clazz.getClassLoader(),
new Class<?>[]{clazz}, invokeHandler);
return proxy;
} /**
*功能:动态代理实例的方法调用
*@param proxy 动态代理实例
*@param method 待调用的方法
*@param args 方法实参
*@return 返回值
*@throws Throwable 抛出的异常
*/

@Override
public Object invoke(Object proxy,
Method method, Object[] args)
throws Throwable
{
if ("equals".equals(method.getName()))
{
Object other = args.length > 0 && args[0] != null ? args[0] : null;
return equals(other);
} else if ("hashCode".equals(method.getName()))
{
return hashCode();
} else if ("toString".equals(method.getName()))
{
return toString();
}
/**
*从dispatch映射中根据方法反射实例获取方法处理器
*/

RpcMethodHandler rpcMethodHandler = dispatch.get(method);
/**
*方法处理器组装URL,完成REST RPC远程调用,并且返回JSON结果
*/

return rpcMethodHandler.invoke(args);
}
}

模拟Feign的动态代理RPC的执行流程

模拟调用处理器MockInvocationHandler的newInstance(...)方法创建一个调用处理器实例,该方法与JDK的动态代理机制的newInstance(...)方法没有任何关系,仅仅是一个模拟Feign的自定义的业务方法,该方法的逻辑如下:

(2)通过迭代扫描RPC接口的每一个方法,组装出对应的MockRpcMethodHandler模拟方法处理器,并且缓存到dispatch映射中。

模拟方法处理器MockRpcMethodHandler实例的创建和映射过程如下:

(3)将新建的方法处理器实例作为value缓存到调用处理器MockInvocationHandler的dispatch映射中,其key为对应的RPC远程调用方法的Method反射实例。

然后,由模拟Feign调用处理器MockInvocationHandler的invoke(...)方法负责完成方法处理器实例的调用,该invoke(...)方法是JDK的InvocationHandler的invoke(...)抽象方法的具体实现。当动态代理实例的RPC方法(如hello)被调用时,MockInvocationHandler的invoke(...)方法会根据RPC方法的反射实例从dispatch映射中取出对应的MockRpcMethodHandler方法处理器实例,由该方法的处理器完成对远程服务的RPC调用。

模拟Feign动态代理RPC调用(以hello方法为例)的执行流程如图3-8所示。

源码模拟Feign RPC动态代理的实现

图3-8 模拟Feign动态代理的RPC执行流程(以hello方法为例)

模拟动态代理RPC远程调用的测试

以下为对模拟Feign动态代理RPC的调用处理器、方法处理器的测试用例,代码如下:

package com.crazymaker.demo.proxy.FeignMock;
//省略import
@Slf4j
public class FeignProxyMockTester
{

/***测试用例*/
@Test
public void test()
{
/**
*创建远程调用接口的本地JDK Proxy代理实例
*/

MockDemoClient proxy =
MockInvocationHandler.newInstance(MockDemoClient.class);
/**
*通过模拟接口完成远程调用
*/

RestOut<JSONObject> responseData = proxy.hello();
log.info(responseData.toString());
/**
*通过模拟接口完成远程调用
*/

RestOut<JSONObject> echo = proxy.echo("proxyTest" );
log.info(echo.toString());
}
}

运行测试用例前,需要提前启动demo-provider微服务实例,并且确保它的两个REST接口/api/demo/hello/v1和/api/demo/echo/{word}/v1可以正常访问。一切准备妥当,运行测试用例,输出的结果如下:

[main] INFO c.c.d.p.F.MockInvocationHandler - 远程方法hello被调用
[main] INFO c.c.d.p.F.MockRpcMethodHandler - restUrl=http://crazydemo.com:7700/demo-provider/api/demo/hello/v1
[main] INFO c.c.d.p.F.FeignProxyMockTester - RestOut{datas={"hello":"world"}, respCode=0, respMsg='操作成功}
[main] INFO c.c.d.p.F.MockInvocationHandler - 远程方法echo被调用
[main] INFO c.c.d.p.F.MockRpcMethodHandler - restUrl=http://crazydemo.com:7700/demo-provider/api/demo/echo/proxyTest/v1
[main] INFO c.c.d.p.F.FeignProxyMockTester - RestOut{datas={"echo":"proxyTest"}, respCode=0, respMsg='操作成功}

本小节模拟的调用处理器、方法处理器在架构设计、执行流程上与实际的Feign已经非常类似了。但是,实际的Feign调用处理器、方法处理器在RPC远程调用的保护机制、编码解码流程等方面比模拟的组件要复杂得多。

Feign弹性RPC客户端实现类

首先,Feign的RPC客户端实现类是一种JDK动态代理类,能完成对简单RPC类(类似本章前面介绍的RealRpcDemoClientImpl)的动态代理;其次,Feign通过调用处理器、方法处理器完成了对RPC被委托类的增强,其调用处理器InvocationHandler通过对第三方组件如Ribbon、Hystrix的使用,使Feign动态代理RPC客户端类具备了客户端负载均衡、失败回退、熔断器、舱壁隔离等一系列的RPC保护能力。

总体来说,Feign通过调用处理器InvocationHandler增强了其动态代理类,使之变成了一个弹性RPC客户端实现类。Feign弹性RPC客户端实现类的功能如图3-9所示。

源码模拟Feign RPC动态代理的实现

图3-9 Feign弹性RPC客户端实现类

Feign弹性RPC客户端实现类的功能介绍如下:

(1)失败回退:当RPC远程调用失败时将执行回退代码,尝试通过其他方式来规避处理,而不是产生一个异常。

(2)熔断器熔断:当RPC远程服务被调用时,熔断器将监视这个调用。如果调用的时间太长,那么熔断器将介入并中断调用。如果RPC调用失败的次数达到某个阈值,那么将会采取快速失败策略终止持续的调用失败。

(3)舱壁隔离:如果所有RPC调用都使用同一个线程池,那么很有可能一个缓慢的远程服务将拖垮整个应用程序。弹性客户端应该能够隔离每个远程资源,并分配各自的舱壁线程池,使之相互隔离,互不影响。

(4)客户端负载均衡:RPC客户端可以在服务提供者的多个实例之间实现多种方式的负载均衡,比如轮询、随机、权重等。

弹性RPC客户端除了是对RPC调用的本地保护之外,也是对远程服务的一种保护。当远程服务发生错误或者表现不佳时,弹性RPC客户端能“快速失败”,不消耗诸如数据库连接、线程池之类的资源,能保护远程服务(微服务Provider实例或者数据库服务等)免于崩溃。

总之,弹性RPC客户端可以避免某个Provider实例的单点问题或者单点故障,在整个微服务节点之间传播,从而避免“雪崩”效应的发生。

本文给大家讲解的内容是SpringCloudRPC远程调用核心原理:模拟Feign RPC动态代理的实现



  1. 源码模拟Feign RPC动态代理的实现

关注我,一个勤奋爱劳动的程序员


END





了解更多Java后端架构、python知识以及最新面试宝典






看完本文记得给作者点赞+在看哦~~~大家的支持,是作者源源不断出文的动力。



以上是关于源码模拟Feign RPC动态代理的实现的主要内容,如果未能解决你的问题,请参考以下文章

一起学源码-微服务Feign 源码三:Feign结合Ribbon实现负载均衡的原理分析

一起学源码-微服务Feign 源码二:Feign动态代理构造过程

Feign远程调用原理

SpringCloud系列之Feign-4.Feign的动态代理

JDK动态代理-超详细源码分析

模拟RPC调用(Feign)