SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

时间:2021-7-4 作者:qvyue

关注公众号“:Java架构师联盟,每日更新技术好文

代理模式与RPC客户端实现类

本节首先介绍客户端RPC远程调用实现类的职责,然后从基础原理讲起,依次介绍代理模式的原理、使用静态代理模式实现RPC客户端类、使用动态代理模式实现RPC客户端类,一步一步地接近Feign RPC的核心原理知识。

客户端RPC远程调用实现类的职责

客户端RPC实现类位于远程调用Java接口和Provider微服务实例之间,承担了以下职责:

(1)拼装REST请求:根据Java接口的参数,拼装目标REST接口的URL。

(2)发送请求和获取结果:通过Java HTTP组件(如HttpClient)调用Provider微服务实例的REST接口,并且获取REST响应。

(3)结果解码:解析REST接口的响应结果,封装成目标POJO对象(Java接口的返回类型)并且返回。

RPC远程调用客户端实现类的职责如图3-1所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类
SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-1 RPC远程调用客户端实现类的职责

使用Feign进行RPC远程调用时,对于每一个Java远程调用接口,Feign都会生成一个RPC远程调用客户端实现类,只是对于开发者来说这个实现类是透明的,感觉不到这个实现类的存在。

Feign为DemoClient接口生成的RPC客户端实现类大致如图3-2所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类
SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-2 Feign为DemoClient接口生成的RPC客户端实现类参考图

由于看不到Feign的RPC客户端实现类的任何源码,初学者会感觉到很神奇,感觉这就是一个黑盒子。下面从原始的、简单的RPC远程调用客户端实现类开始为大家逐步地揭开Feign的RPC客户端实现类的神秘面纱。

在一点点揭开RPC远程调用客户端实现类的面纱之前,先模拟一个Feign远程调用Java接口,对应demo-provider服务的两个REST接口。

模拟的远程调用Java接口为MockDemoClient,它的代码如下:

package com.crazymaker.demo.proxy.FeignMock;

@RestController(value = TestConstants.DEMO_CLIENT_PATH)
public interface MockDemoClient
{ /**
*远程调用接口的方法,完成REST接口api/demo/hello/v1的远程调用
*REST接口功能:返回hello world
@return JSON响应实例
/
@GetMapping(name = “api/demo/hello/v1”)
RestOut hello();
/

*远程调用接口的方法,完成REST接口api/demo/echo/{0}/v1的远程调用
*REST接口功能:回显输入的信息
*@return echo回显消息JSON响应实例
*/
@GetMapping(name = “api/demo/echo/{0}/v1”)
RestOut echo(String word);
}

接下来层层递进,为大家演示以下3种RPC远程调用客户端:

(1)简单的RPC客户端实现类。

(2)静态代理模式的RPC客户端实现类。

(3)动态代理模式的RPC客户端实现类。

最后的动态代理模式的RPC客户端实现类在实现原理上已经非常接近Feign的RPC客户端实现类。

简单的RPC客户端实现类

简单的RPC客户端实现类的主要工作如下:

(1)组装REST接口URL。

(2)通过HttpClient组件调用REST接口并获得响应结果。

(3)解析REST接口的响应结果,封装成JSON对象,并且返回给调用者。

简单的RPC客户端实现类的参考代码如下:

package com.crazymaker.demo.proxy.basic;
//省略import
@AllArgsConstructor
@Slf4j
class RealRpcDemoClientImpl implements MockDemoClient
{
final String contextPath = TestConstants.DEMO_CLIENT_PATH;
//完成对REST接口api/demo/hello/v1的调用
public RestOut hello()
{
/**
*远程调用接口的方法,完成demo-provider的REST API远程调用
REST API功能:返回hello world
/
String uri = “api/demo/hello/v1”;
/

组装REST接口URL
/
String restUrl = contextPath + uri;
log.info(“restUrl={}”, restUrl);
/

通过HttpClient组件调用REST接口
/
String responseData = null;
try
{
responseData = HttpRequestUtil.simpleGet(restUrl);
} catch (IOException e)
{
e.printStackTrace();
}
/

解析REST接口的响应结果,解析成JSON对象并且返回给调用者
/
RestOut result = JsonUtil.jsonToPojo(responseData,
new TypeReference>() {});
return result;
}
//完成对REST接口api/demo/echo/{0}/v1的调用
public RestOut echo(String word)
{
/

*远程调用接口的方法,完成demo-provider的REST API远程调用
REST API功能:回显输入的信息
/
String uri = “api/demo/echo/{0}/v1”;
/

组装REST接口URL
/
String restUrl = contextPath + MessageFormat.format(uri, word);
log.info(“restUrl={}”, restUrl);
/

通过HttpClient组件调用REST接口
/
String responseData = null;
try
{
responseData = HttpRequestUtil.simpleGet(restUrl);
} catch (IOException e)
{
e.printStackTrace();
}
/

解析

的响应结果
解析成
对象
并且返回给调用者 *解析REST接口的响应结果,解析成JSON对象,并且返回给调用者
*/
RestOut result = JsonUtil.jsonToPojo(responseData,
new TypeReference>() { });
return result;
}
}

以上简单的RPC实现类RealRpcDemoClientImpl的测试用例如下:

package com.crazymaker.demo.proxy.basic;

/**
测试用例
/
@Slf4j
public class ProxyTester
{
/

不用代理,进行简单的远程调用
/
@Test
public void simpleRPCTest()
{
/

简单的RPC调用类
/
MockDemoClient realObject = new RealRpcDemoClientImpl();
/

调用demo-provider的REST接口api/demo/hello/v1
/
RestOut result1 = realObject.hello();
log.info(“result1={}”, result1.toString());
/

*调用demo-provider的REST接口api/demo/echo/{0}/v1
*/
RestOut result2 = realObject.echo(“回显内容”);
log.info(“result2={}”, result2.toString());
}
}

运行测试用例之前,需要提前启动demo-provider微服务实例,然后将主机名称crazydemo.com通过hosts文件绑定到demo-provider实例所在机器的IP地址(这里为127.0.0.1),并且需要确保两个REST接口/api/demo/hello/v1、/api/demo/echo/{word}/v1可以正常访问。

运行测试用例,部分输出结果如下:

[main] INFO c.c.d.p.b.RealRpcDemoClientImpl – restUrl=http://crazydemo.com:7700/demo-provider/ api/demo/hello/v1
[main] INFO c.c.d.proxy.basic.ProxyTester – result1=RestOut{datas={“hello”:”world”}, respCode=0, respMsg=’操作成功}
[main] INFO c.c.d.p.b.RealRpcDemoClientImpl – restUrl=http://crazydemo.com:7700/demo-provider/ api/demo/echo/回显内容/v1
[main] INFO c.c.d.proxy.basic.ProxyTester – result2=RestOut{datas={“echo”:”回显内容”}, respCode=0, respMsg=’操作成功}

以上的RPC客户端实现类很简单,但是实际开发中不可能为每一个远程调用Java接口都编写一个RPC客户端实现类。如何自动生成RPC客户端实现类呢?这就需要用到代理模式。接下来为大家介绍简单一点的代理模式实现类——静态代理模式的RPC客户端实现类。

从基础原理讲起:代理模式与RPC客户端实现类

首先来看一下代理模式的基本概念。代理模式的定义:为委托对象提供一种代理,以控制对委托对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个目标对象,而代理对象可以作为目标对象的委托,在客户端和目标对象之间起到中介的作用。

代理模式包含3个角色:抽象角色、委托角色和代理角色,如图3-3所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类
SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-3 代理模式角色之间的关系图

(1)抽象角色:通过接口或抽象类的方式声明委托角色所提供的业务方法。

(2)代理角色:实现抽象角色的接口,通过调用委托角色的业务逻辑方法来实现抽象方法,并且可以附加自己的操作。

(3)委托角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。

代理模式分为静态代理和动态代理。

(1)静态代理:在代码编写阶段由工程师提供代理类的源码,再编译成代理类。所谓静态,就是在程序运行前就已经存在代理类的字节码文件,代理类和被委托类的关系在运行前就确定了。

(2)动态代理:在代码编写阶段不用关心具体的代理实现类,而是在运行阶段直接获取具体的代理对象,代理实现类由JDK负责生成。

静态代理模式的实现主要涉及3个组件:(1)抽象接口类(Abstract Subject):该类的主要职责是声明目标类与代理类的共同接口方法。该类既可以是一个抽象类,又可以是一个接口。

(2)真实目标类(Real Subject):该类也称为被委托类或被代理类,该类定义了代理所表示的真实对象,由其执行具体业务逻辑方法,而客户端通过代理类间接地调用真实目标类中定义的方法。

(3)代理类(Proxy Subject):该类也称为委托类或代理类,该类持有一个对真实目标类的引用,在其抽象接口方法的实现中需要调用真实目标类中相应的接口实现方法,以此起到代理的作用。

使用静态代理模式实现RPC远程接口调用大致涉及以下3个类:

(1)一个远程接口,比如前面介绍的模拟远程调用Java接口MockDemoClient。

(2)一个真实被委托类,比如前面介绍的RealRpcDemoClientImpl,负责完成真正的RPC调用。

(3)一个代理类,比如本小节介绍的DemoClientStaticProxy,通过调用真实目标类(委托类)负责完成RPC调用。

通过静态代理模式实现MockDemoClient接口的RPC调用实现类,类之间的关系如图3-4所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类
SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-4 静态代理模式的RPC调用UML类图

静态代理模式的RPC实现类DemoClientStaticProxy的代码如下:

package com.crazymaker.demo.proxy.basic;
//省略import
@AllArgsConstructor
@Slf4j
class DemoClientStaticProxy implements DemoClient
{
/**
*被代理的真正实例
*/
private MockDemoClient realClient; @Override
public RestOut hello()
{
log.info(“hello方法被调用” );
return realClient.hello();
}
@Override
public RestOut echo(String word)
{
log.info(“echo方法被调用” );
return realClient.echo(word);
}
}

在静态代理类DemoClientStaticProxy的hello()和echo()两个方法中,调用真实委托类实例realClient的两个对应的委托方法,完成对远程REST接口的请求。

以上静态代理类DemoClientStaticProxy的使用代码(测试用例)大致如下:
package com.crazymaker.demo.proxy.basic;
//省略import
/**
静态代理和动态代理,测试用例
/
@Slf4j
public class ProxyTester
{
/

静态代理测试
/
@Test
public void staticProxyTest()
{
/

被代理的真实RPC调用类
/
MockDemoClient realObject = new RealRpcDemoClientImpl();
/

*静态的代理类
*/
DemoClient proxy = new DemoClientStaticProxy(realObject);
RestOut result1 = proxy.hello();
log.info(“result1={}”, result1.toString());
RestOut result2 = proxy.echo(“回显内容”);
log.info(“result2={}”, result2.toString());
}
}

运行测试用例前,需要提前启动demo-provider微服务实例,并且需要将主机名称crazydemo.com通过hosts文件绑定到demo-provider实例所在机器的IP地址(这里为127.0.0.1),并且需要确保两个REST接口/api/demo/hello/v1、/api/demo/echo/{word}/v1可以正常访问。

一切准备妥当,运行测试用例,输出如下结果:

[main] INFO c.c.d.p.b.DemoClientStaticProxy – hello方法被调用
[main] INFO c.c.d.p.b.RealRpcDemoClientImpl – restUrl= http://crazydemo.com:7700/demo-provider/ api/demo/hello/v1
[main] INFO c.c.d.proxy.basic.ProxyTester – result1=RestOut{datas={“hello”:”world”}, respCode=0, respMsg=’操作成功}
[main] INFO c.c.d.p.b.DemoClientStaticProxy – echo方法被调用
[main] INFO c.c.d.p.b.RealRpcDemoClientImpl – restUrl=http://crazydemo.com:7700/demo-provider/ api/demo/echo/回显内容/v1
[main] INFO c.c.d.proxy.basic.ProxyTester – result2=RestOut{datas={“echo”:”回显内容”}, respCode=0, respMsg=’操作成功}

静态代理的RPC实现类看上去是一堆冗余代码,发挥不了什么作用。为什么在这里一定要先介绍静态代理模式的RPC实现类呢?原因有以下两点:

(1)上面的RPC实现类是出于演示目的而做了简化,对委托类并没有做任何扩展。而实际的远程调用代理类会对委托类进行很多扩展,比如远程调用时的负载均衡、熔断、重试等。

(2)上面的RPC实现类是动态代理实现类的学习铺垫。Feign的RPC客户端实现类是一个JDK动态代理类,是在运行过程中动态生成的。大家知道,动态代理的知识对于很多读者来说不是太好理解,所以先介绍一下代理模式和静态代理的基础知识,作为下一步的学习铺垫。

使用动态代理模式实现RPC客户端类

为什么需要动态代理呢?需要从静态代理的缺陷开始介绍。静态代理实现类在编译期就已经写好了,代码清晰可读,缺点也很明显:

(1)手工编写代理实现类会占用时间,如果需要实现代理的类很多,那么代理类一个一个地手工编码根本写不过来。

(2)如果更改了抽象接口,那么还得去维护这些代理类,维护上容易出纰漏。

动态代理与静态代理相反,不需要手工实现代理类,而是由JDK通过反射技术在执行阶段动态生成代理类,所以也叫动态代理。使用的时候可以直接获取动态代理的实例,获取动态代理实例大致需要如下3步:

(1)需要明确代理类和被委托类共同的抽象接口,JDK生成的动态代理类会实现该接口。

(2)构造一个调用处理器对象,该调用处理器要实现InvocationHandler接口,实现其唯一的抽象方法invoke(…)。而InvocationHandler接口由JDK定义,位于java.lang.reflect包中。

(3)通过java.lang.reflect.Proxy类的newProxyInstance(…)方法在运行阶段获取JDK生成的动态代理类的实例。注意,这一步获取的是对象而不是类。该方法需要三个参数,其中的第一个参数为类装载器,第二个参数为抽象接口的class对象,第三个参数为调用处理器对象。

举一个例子,创建抽象接口MockDemoClient的一个动态代理实例,大致的代码如下:

//参数1:类装载器
ClassLoader classLoader = ProxyTester.class.getClassLoader();
//参数2:代理类和被委托类共同的抽象接口
Class[] clazz = new Class[]{MockDemoClient.class};
//参数3:动态代理的调用处理器
InvocationHandler invocationHandler = new DemoClientInocationHandler (realObject);
/**
*使用以上3个参数创建JDK动态代理类
*/
MockDemoClient proxy = (MockDemoClient)Proxy.newProxyInstance(classLoader, clazz, invocationHandler);

创建动态代理实例的核心是创建一个JDK调用处理器InvocationHandler的实现类。该实现类需要实现其唯一的抽象方法invoke(…),并且在该方法中调用被委托类的方法。一般情况下,调用处理器需要能够访问到被委托类,一般的做法是将被委托类实例作为其内部的成员。

例子中所获取的动态代理实例涉及3个类,具体如下:

(1)一个远程接口,使用前面介绍的模拟远程调用Java接口MockDemoClient。

(2)一个真实目标类,使用前面介绍的RealRpcDemoClientImpl类,该类负责完成真正的RPC调用,作为动态代理的被委托类。

(3)一个InvocationHandler的实现类,本小节将实现 DemoClientInocationHandler调用处理器类,该类通过调用内部成员被委托类的对应方法完成RPC调用。模拟远程接口MockDemoClient的RPC动态代理模式实现,类之间的关系如图3-5所示。

SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类
SpringCloudRPC远程调用核心原理:代理模式与RPC客户端实现类

图3-5 动态代理模式实现RPC远程调用UML类图

通过动态代理模式实现模拟远程接口MockDemoClient的RPC调用,关键的类为调用处理器,调用处理器 DemoClientInocationHandler的代码如下:

package com.crazymaker.demo.proxy.basic;
//省略import
/**
动态代理的调用处理器
/
@Slf4j
public class DemoClientInocationHandler implements InvocationHandler
{
/

被代理的被委托类实例
/
private MockDemoClient realClient;
public DemoClientInocationHandler(MockDemoClient realClient)
{
this.realClient = realClient;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
String name = method.getName();
log.info(“{} 方法被调用”, method.getName());
/

直接调用被委托类的方法:调用其hello方法
/
if (name.equals(“hello”))
{
return realClient.hello();
}
/

通过Java反射调用被委托类的方法:调用其echo方法
/
if (name.equals(“echo”))
{
return method.invoke(realClient, args);
}
/

*通过Java反射调用被委托类的方法
*/
Object result = method.invoke(realClient, args);
return result;
}
}

调用处理器 DemoClientInocationHandler既实现了InvocationHandler接口,又拥有一个内部被委托类成员,负责完成实际的RPC请求。调用处理器有点儿像静态代理模式中的代理角色,但是在这里却不是,仅仅是JDK所生成的代理类的内部成员。

以上调用处理器 DemoClientInocationHandler的代码(测试用例)如下:

package com.crazymaker.demo.proxy.basic;
//省略import
@Slf4j
public class StaticProxyTester {
/**
*动态代理测试
*/
@Test
public void dynamicProxyTest() {
DemoClient client = new DemoClientImpl();
//参数1:类装载器
ClassLoader classLoader = StaticProxyTester.class.getClassLoader();
//参数2:被代理的实例类型
Class[] clazz = new Class[]{DemoClient.class};
//参数3:调用处理器
InvocationHandler invocationHandler =
new DemoClientInocationHandler(client);
//获取动态代理实例
DemoClient proxy = (DemoClient)
Proxy.newProxyInstance(classLoader, clazz, invocationHandler);
//执行RPC远程调用方法
Result result1 = proxy.hello();
log.info(“result1={}”, result1.toString());
Result result2 = proxy.echo(“回显内容”);
log.info(“result2={}”, result2.toString());
}
}

运行测试用例前需要提前启动demo-provider微服务实例,并且需要确保其两个REST接口/api/demo/hello/v1、/api/demo/echo/{word}/v1可以正常访问。

一切准备妥当,运行测试用例,输出的结果如下:

18:36:32.499 [main] INFO c.c.d.p.b.DemoClientInocationHandler – hello方法被调用
18:36:32.621 [main] INFO c.c.d.p.b.StaticProxyTester – result1=Result{data={“hello”:”world”}, status=200, msg=’操作成功, reques
18:36:32.622 [main] INFO c.c.d.p.b.DemoClientInocationHandler – echo方法被调用
18:36:32.622 [main] INFO c.c.d.p.b.StaticProxyTester – result2=Result{data={“echo”:”回显内容”}, status=200, msg=’操作成功, reques

JDK动态代理机制的原理

动态代理的实质是通过java.lang.reflect.Proxy的newProxyInstance(…)方法生成一个动态代理类的实例,该方法比较重要,下面对该方法进行详细介绍,其定义如下:

public static Object newProxyInstance(ClassLoader loader,//类加载器
Class>[] interfaces,//动态代理类需要实现的接口
InvocationHandler h) //调用处理器
throws IllegalArgumentException
{

}

此方法的三个参数介绍如下:

第一个参数为ClassLoader类加载器类型,此处的类加载器和被委托类的类加载器相同即可。

第二个参数为Class[]类型,代表动态代理类将会实现的抽象接口,此接口是被委托类所实现的接口。

第三个参数为InvocationHandler类型,它的调用处理器实例将作为JDK生成的动态代理对象的内部成员,在对动态代理对象进行方法调用时,该处理器的invoke(…)方法会被执行。

InvocationHandler处理器的invoke(…)方法如何实现由大家自己决定。对被委托类(真实目标类)的扩展或者定制逻辑一般都会定义在此InvocationHandler处理器的invoke(…)方法中。

JVM在调用Proxy.newProxyInstance(…)方法时会自动为动态代理对象生成一个内部的代理类,那么是否能看到该动态代理类的class字节码呢?

答案是肯定的,可以通过如下方式获取其字节码,并且保存到文件中:

/**
获取动态代理类的class字节码
/
byte[] classFile = ProxyGenerator.generateProxyClass(“Proxy0”,
RealRpcDemoClientImpl.class.getInterfaces());
/

*在当前的工程目录下保存文件
*/
FileOutputStream fos =new FileOutputStream(new File(“Proxy0.class”));
fos.write(classFile);
fos.flush();
fos.close();

运行3.1.4节的dynamicProxyTest()测试用例,在demo-provider模块的根路径可以发现被新创建的Proxy0.class字节码文件。如果IDE有反编译的能力,就可以在IDE中打开该文件,然后可以看到其反编译的源码:

import com.crazymaker.demo.proxy.MockDemoClient;
import com.crazymaker.springcloud.common.result.RestOut;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class Proxy0 extends Proxy implements MockDemoClient {
private static Method m1;
private static Method m4;
private static Method m3;
private static Method m2;
private static Method m0;
public Proxy0(InvocationHandler var1) throws {
super(var1);
}

public final RestOut echo(String var1) throws {
try {
return (RestOut)super.h.invoke(this, m4, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
public final RestOut hello() throws {
try {
return (RestOut)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName(“java.lang.Object”).getMethod(“equals”, Class.forName(“java.lang.Object”));
m4 = Class.forName(“com.crazymaker.demo.proxy.MockDemoClient”)
.getMethod(“echo”, Class.forName(“java.lang.String”));
m3 = Class.forName(“com.crazymaker.demo.proxy.MockDemoClient”)
.getMethod(“hello”);
m2 = Class.forName(“java.lang.Object”).getMethod(“toString”);
m0 = Class.forName(“java.lang.Object”).getMethod(“hashCode”);
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

通过代码可以看出,这个动态代理类其实只做了两件简单的事情:

(1)该动态代理类实现了接口类的抽象方法。动态代理类Proxy0实现了MockDemoClient接口的echo(String)、hello()两个方法。此外,Proxy0还继承了java.lang.Object的equals()、hashCode()、toString()方法。

(2)该动态代理类将对自己的方法调用委托给了InvocationHandler调用处理器内部成员。以上代理类Proxy0的每一个方法实现的代码其实非常简单,并且逻辑大致一样:将方法自己的Method反射对象和调用参数进行二次委托,委托给内部成员InvocationHandler调用处理器的invoke(…)方法。至于该内部InvocationHandler调用处理器的实例,则由大家自己编写,在通过java.lang.reflect.Proxy的newProxyInstance(…)创建动态代理对象时作为第三个参数传入。

至此,JDK动态代理机制的核心原理和动态代理类的神秘面纱已经彻底地揭开了。

Feign的RPC客户端正是通过JDK的动态代理机制来实现的,Feign对RPC调用的各种增强处理主要是通过调用处理器InvocationHandler来实现的。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。