3.1 代理模式与RPC客户端实现类
3.1.1 客户端RPC远程调用实现类的职责
- 拼装 REST 请求
- 发送请求和获取结果
- 结果解码
模拟一个Feign远程调用java接口:
@RestController(value = TestConstants.DEMO_CLIENT_PATH)
public interface MockDemoClient
{
/**
* 远程调用接口的方法:
* 调用 demo-provider 的 REST 接口 api/demo/hello/v1
* REST 接口 功能:返回 hello world
*
* @return JSON 响应实例
*/
@GetMapping(name = "api/demo/hello/v1")
RestOut<JSONObject> hello();
/**
* 远程调用接口的方法:
* 调用 demo-provider 的 REST 接口 api/demo/echo/{0}/v1
* REST 接口 功能: 回显输入的信息
*
* @return echo 回显消息 JSON 响应实例
*/
@GetMapping(name = "api/demo/echo/{0}/v1")
RestOut<JSONObject> echo(String word);
}
3.1.2 简单的RPC客户端实现类
- 组装 REST 接口 URL。
- 通过 HttpClient 组件调用 REST 接口并获得响应结果
解析 REST 接口的响应结果,封装成 JSON 对象,并且返回给调用者。 ```java @AllArgsConstructor @Slf4j class RealRpcDemoClientImpl implements MockDemoClient { final String contextPath = TestConstants.DEMO_CLIENT_PATH;
//hello简单实现 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<JSONObject> result = JsonUtil.jsonToPojo(responseData, new TypeReference<RestOut<JSONObject>>()
{
});
return result;
}
//echo简单实现
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;
}
}
<a name="mGh87"></a>
## 3.1.3 基础原理:代理模式与RPC客户端实现类
![image.png](https://cdn.nlark.com/yuque/0/2022/png/27178439/1657236409667-94e51a43-c0e9-4a6c-a25b-82590a70cdc6.png#clientId=uf87bba08-9fa8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=644&id=u6d269daf&margin=%5Bobject%20Object%5D&name=image.png&originHeight=644&originWidth=871&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36599&status=done&style=none&taskId=u7ffd21c5-47a1-438c-a11e-207c33d6c2d&title=&width=871)<br />代理模式分为静态代理和动态代理
1. 静态代理: 在代码编写阶段由工程师提供代理类的源码,再编译成代理类。所谓静态,就是在程序运行前就已经存在代理类的字节码文件,代理类和被委托类的关系在运行前就确定了。
1. 动态代理: 在代码编写阶段不用关心具体的代理实现类,而是在运行阶段直接获取具体的代理对象,代理实现类由 JDK 负责生成。
通过静态代理模式实现 MockDemoClient 接口的 RPC 调用实现类,类之间的关系如图 <br /> ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27178439/1657236570925-4c4e0951-1318-4e6f-8ca2-d9b1de93b77d.png#clientId=uf87bba08-9fa8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=602&id=ufb4fc731&margin=%5Bobject%20Object%5D&name=image.png&originHeight=602&originWidth=1105&originalType=binary&ratio=1&rotation=0&showTitle=false&size=92618&status=done&style=none&taskId=u4c74a360-ac06-4509-afc4-dd640a77081&title=&width=1105)
```java
@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);
}
}
静态代理的 RPC 实现类看上去是一堆冗余代码,发挥不了什么作用。 为什么在这里一定要先介绍静态代理模式的 RPC 实现类呢?原因有以下两点:
- 上面的 RPC 实现类是出于演示目的而做了简化,对委托类并没有做任何扩展。 而实际的远程调用代理类会对委托类进行很多扩展,比如远程调用时的负载均衡、熔断、重试等。
上面的 RPC 实现类是动态代理实现类的学习铺垫。 Feign 的 RPC 客户端实现类是一个JDK 动态代理类,是在运行过程中动态生成的。
3.1.4 使用动态代理模式实现RPC客户端类
静态代理的缺陷
手工编写代理实现类会占用时间, 如果需要实现代理的类很多, 那么代理类一个一个地手工编码根本写不过来。
- 如果更改了抽象接口, 那么还得去维护这些代理类,维护上容易出纰漏。
动态代理是由JDK通过反射技术在执行阶段动态生成代理类,所以叫动态代理,获取动态代理实例的大致3个步骤:
- 明确代理类和被委托类共同的抽象接口,JDK生成的动态代理类会实现该接口
- 构造调用处理器对象,该对象要实现InvocationHandler接口,实现其唯一的抽象方法invoke
- 通过Proxy.newProxyInstance()方法在运行阶段获取JDK生成的动态代理类的实例,该方法需要三个参数
- 类装载器
- 抽象接口的class对象
- 调用处理器对象
例子:抽象接口MockDemoClient的一个动态代理实例:
//参数1:类装载器
ClassLoader classLoader = ProxyTester.class.getClassLoader();
//参数2:代理类和委托类共同的抽象接口
Class[] clazz = new Class[]{MockDemoClient.class};
//参数3:动态代理的调用处理器
InvocationHandler invocationHandler = new DemoClientInocationHandler(realObject);
/**
* 使用以上三个参数,创建 JDK 动态代理类
*/
MockDemoClient proxy = (MockDemoClient) Proxy.newProxyInstance(classLoader, clazz, invocationHandler);
创建动态代理实例的核心是创建一个JDK调用处理器InvocationHandler的实现类,调用处理器DemoClientInvocationHandler的代码如下:
/**
* 动态代理的调用处理器
*/
@Slf4j
public class DemoClientInocationHandler implements InvocationHandler
{
/**
* 被代理的委托类实例
*/
private MockDemoClient realClient;
public DemoClientInocationHandler(MockDemoClient realClient)
{
this.realClient = realClient;
}
@Override
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;
}
}
测试代码
@Test
public void dynamicProxyTest() throws IOException {
/**
* 被代理的真实 PRC 调用类
*/
MockDemoClient realObject = new RealRpcDemoClientImpl();
//参数1:类装载器
ClassLoader classLoader = ProxyTester.class.getClassLoader();
//参数2:代理类和委托类共同的抽象接口
Class[] clazz = new Class[]{MockDemoClient.class};
//参数3:动态代理的调用处理器
InvocationHandler invocationHandler = new DemoClientInocationHandler(realObject);
/**
* 使用以上三个参数,创建 JDK 动态代理类
*/
MockDemoClient proxy = (MockDemoClient)
Proxy.newProxyInstance(classLoader, clazz, invocationHandler);
RestOut<JSONObject> result1 = proxy.hello();
log.info("result1={}", result1.toString());
RestOut<JSONObject> result2 = proxy.echo("回显内容");
log.info("result2={}", result2.toString());
/**
* 将动态代理类的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.5 JDK动态代理机制的原理
动态代理的实质是通过Proxy.newProxyInstance()方法生成一个动态代理的实例
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
...
}
三个参数的介绍:
- ClassLoader类加载器,此处的类加载器和被委托的类加载器相同即可
- Class[]类型,代表动态代理类将会实现的抽象接口,也是被委托类所实现的接口
InvocationHandler类型,它的调用处理器实例将作为JDK生成的动态代理对象的内部成员,在对动态代理对象进行方法调用时,invoke方法会被执行
3.2 模拟Feign RPC动态代理的实现
3.2.1 模拟Feign的方法处理器MethodHandler
由于每个 RPC 客户端类一般会包含多个远程调用方法, 因此 Feign 为远程调用方法封装了一个专门的接口—MethodHandler(方法处理器),此接口很简单,仅仅包含一个 invoke(…)抽象方法
/**
* 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 实例, 然后返回。 ```java @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;
}
}
<a name="OkFmZ"></a>
## 3.2.2 模拟Feign的调用处理器InvocationHandler
调用处理器 FeignInvocationHandler 是一个相对简单的类,拥有一个非常重要的 Map 类型的成员 dispatch,保存着 RPC 方法反射实例到其 MethodHandler 方法处理器的映射。 <br /> ![image.png](https://cdn.nlark.com/yuque/0/2022/png/27178439/1657237688436-5a757740-6d3c-4345-86f3-c3e423857e6b.png#clientId=uf87bba08-9fa8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=319&id=ub49f0497&margin=%5Bobject%20Object%5D&name=image.png&originHeight=319&originWidth=986&originalType=binary&ratio=1&rotation=0&showTitle=false&size=107355&status=done&style=none&taskId=u72a73edb-46ea-4518-b397-28ce89c826d&title=&width=986)
```java
@Slf4j
class MockInvocationHandler implements InvocationHandler
{
/**
* 远程调用的分发映射:根据方法名称,分发方法处理器
* key: 远程调用 Feign 接口的方法名称
* value: 方法处理器 handler
*/
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 映射中
*/
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();
}
log.info("远程方法 {} 被调用", method.getName());
/**
* 从 dispatch 映射中,根据方法名称,获取方法处理器
*/
RpcMethodHandler rpcMethodHandler = dispatch.get(method);
/**
* 方法处理器组装 url,完成 REST RPC 远程调用,并且返回 JSON结果
*/
return rpcMethodHandler.invoke(args);
}
}
3.2.3 模拟Feign的动态代理RPC的执行流程
3.2.4 模拟动态代理RPC远程调用的测试
@Slf4j
public class FeignProxyMockTester
{
/**
* 测试用例
*/
@Test
public void test() throws IOException
{
/**
* 创建远程调用接口的本地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());
}
}
3.2.5 Feign弹性RPC客户端实现类
Feign 通过调用处理器、方法处理器完成了对 RPC 被委托类的增强,其调用处理器 InvocationHandler 通过对第三方组件如 Ribbon、Hystrix 的使用, 使 Feign 动态代理 RPC 客户端类具备了客户端负载均衡、失败回退、 熔断器、舱壁隔离等一系列的 RPC 保护能力。
- 失败回退: 当 RPC 远程调用失败时将执行回退代码,尝试通过其他方式来规避处理,而不是产生一个异常。
- 熔断器熔断: 当 RPC 远程服务被调用时, 熔断器将监视这个调用。如果调用的时间太长, 那么熔断器将介入并中断调用。如果 RPC 调用失败的次数达到某个阈值, 那么将会采取快速失败策略终止持续的调用失败
- 舱壁隔离: 如果所有 RPC 调用都使用同一个线程池,那么很有可能一个缓慢的远程服务将拖垮整个应用程序。弹性客户端应该能够隔离每个远程资源,并分配各自的舱壁线程池,使之相互隔离, 互不影响。
客户端负载均衡: RPC 客户端可以在服务提供者的多个实例之间实现多种方式的负载均衡,比如轮询、随机、权重等。
3.3 Feign弹性RPC客户端的重要组件
在微服务启动时, Feign 会进行包扫描,对加@FeignClient 注解的 RPC 接口创建远程接口的本地 JDK 动态代理实例。之后这些本地 Proxy 动态代理实例会注入 Spring IOC 容器中。当远程接口的方法被调用时,由 Proxy 动态代理实例负责完成真正的远程访问并返回结果。
3.3.2 Feign的动态代理RPC客户端实例
```java @FeignClient(
value = "demo-provider", path = "/demo-provider/api/demo/",
fallback = DemoDefaultFallback.class
) public interface DemoClient { /**
- 远程调用接口的方法:
- 调用 demo-provider 的 REST 接口 api/demo/hello/v1
- REST 接口 功能:返回 hello world
@return JSON 响应实例 */ @GetMapping(“/hello/v1”) RestOut
hello(); /**
- 远程调用接口的方法:
- 调用 demo-provider 的 REST 接口 api/demo/echo/{0}/v1
- REST 接口 功能: 回显输入的信息
- @return echo 回显消息 JSON 响应实例
*/
@RequestMapping(value = “/echo/{word}/v1”,
RestOutmethod = RequestMethod.GET)
echo( @PathVariable(value = "word") String word);
3.3.3 Feign的调用处理器InvocationHandler
Feign 提供了一个默认的调用处理器, 名为 FeignInvocationHandler 类,该类完成基本的调用处理逻辑,处于 feign-core 核心 JAR 包中。
Feign 的调用处理器可以进行替换,如果 Feign是与 Hystrix 结合使用的, 就会被替换成 HystrixInvocationHandler 调用处理器类,而该类处于 feignhystrix 的 JAR 包中。
3.3.4 Feign的方法处理器MethodHandler
3.3.5 Feign的客户端组件
不同的 feign.Client 客户端实现类其内部提交 HTTP 请求的技术是不同的。常用的 Feign 客户端实现类如下:
- Client.Default 类:默认的实现类,使用 JDK 的 HttpURLConnnection 类提交 HTTP 请求。
- ApacheHttpClient 类:该客户端类在内部使用 Apache HttpClient 开源组件提交 HTTP 请求
- OkHttpClient 类:该客户端类在内部使用 OkHttp3 开源组件提交 HTTP 请求。
- LoadBalancerFeignClient 类:内部使用 Ribbon 负载均衡技术完成 HTTP 请求处理。