关注公众号“: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 所示。
图 3-1 RPC 远程调用客户端实现类的职责
使用 Feign 进行 RPC 远程调用时,对于每一个 Java 远程调用接口,Feign 都会生成一个 RPC 远程调用客户端实现类,只是对于开发者来说这个实现类是透明的,感觉不到这个实现类的存在。
Feign 为 DemoClient 接口生成的 RPC 客户端实现类大致如图 3-2 所示。
图 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<JSONObject> hello();
/**
*远程调用接口的方法,完成REST接口api/demo/echo/{0}/v1的远程调用
*REST接口功能:回显输入的信息
*@return echo回显消息JSON响应实例
*/
@GetMapping(name = "api/demo/echo/{0}/v1")
RestOut<JSONObject> 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<JSONObject> 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<JSONObject> result = JsonUtil.jsonToPojo(responseData,
new TypeReference<RestOut<JSONObject>>() {});
return result;
}
//完成对REST接口api/demo/echo/{0}/v1的调用
public RestOut<JSONObject> 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<JSONObject> result = JsonUtil.jsonToPojo(responseData,
new TypeReference<RestOut<JSONObject>>() { });
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<JSONObject> result1 = realObject.hello();
log.info("result1={}", result1.toString());
/**
*调用demo-provider的REST接口api/demo/echo/{0}/v1
*/
RestOut<JSONObject> 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 所示。
图 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 所示。
图 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<JSONObject> hello()
{
log.info("hello方法被调用" );
return realClient.hello();
}
@Override
public RestOut<JSONObject> 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<JSONObject> result1 = proxy.hello();
log.info("result1={}", result1.toString());
RestOut<JSONObject> 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 所示。
图 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<JSONObject> result1 = proxy.hello();
log.info("result1={}", result1.toString());
Result<JSONObject> 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 来实现的。
评论