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

{

e.printStackTrace();

}

/**

*解析REST接口的响应结果,解析成JSON对象并且返回给调用者

*/

RestOut result = JsonUtil.jsonToPojo(responseData,

new TypeReference<RestOut>() {});

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<RestOut>() { });

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客户端实现类

图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客户端实现类

图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客户端实现类

图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;

总结:绘上一张Kakfa架构思维大纲脑图(xmind)

image

其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?

若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理

梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。

  • Kafka入门

  • 为什么选择Kafka

  • Kafka的安装、管理和配置

  • Kafka的集群

  • 第一个Kafka程序

  • Kafka的生产者

  • Kafka的消费者

  • 深入理解Kafka

  • 可靠的数据传递

  • Spring和Kafka的整合

  • SpringBoot和Kafka的整合

  • Kafka实战之削峰填谷

  • 数据管道和流式处理(了解即可)

image

image

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

import com.crazymaker.demo.proxy.MockDemoClient;

import com.crazymaker.springcloud.common.result.RestOut;

总结:绘上一张Kakfa架构思维大纲脑图(xmind)

[外链图片转存中…(img-UdvSLzYS-1718717935056)]

其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?

若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理

梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。

  • Kafka入门

  • 为什么选择Kafka

  • Kafka的安装、管理和配置

  • Kafka的集群

  • 第一个Kafka程序

  • Kafka的生产者

  • Kafka的消费者

  • 深入理解Kafka

  • 可靠的数据传递

  • Spring和Kafka的整合

  • SpringBoot和Kafka的整合

  • Kafka实战之削峰填谷

  • 数据管道和流式处理(了解即可)

[外链图片转存中…(img-zPaBF57w-1718717935056)]

[外链图片转存中…(img-cc7gpaxD-1718717935057)]

  • 13
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值