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

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

  1. 拼装 REST 请求
  2. 发送请求和获取结果
  3. 结果解码

image.png
模拟一个Feign远程调用java接口:

  1. @RestController(value = TestConstants.DEMO_CLIENT_PATH)
  2. public interface MockDemoClient
  3. {
  4. /**
  5. * 远程调用接口的方法:
  6. * 调用 demo-provider 的 REST 接口 api/demo/hello/v1
  7. * REST 接口 功能:返回 hello world
  8. *
  9. * @return JSON 响应实例
  10. */
  11. @GetMapping(name = "api/demo/hello/v1")
  12. RestOut<JSONObject> hello();
  13. /**
  14. * 远程调用接口的方法:
  15. * 调用 demo-provider 的 REST 接口 api/demo/echo/{0}/v1
  16. * REST 接口 功能: 回显输入的信息
  17. *
  18. * @return echo 回显消息 JSON 响应实例
  19. */
  20. @GetMapping(name = "api/demo/echo/{0}/v1")
  21. RestOut<JSONObject> echo(String word);
  22. }

3.1.2 简单的RPC客户端实现类

  1. 组装 REST 接口 URL。
  2. 通过 HttpClient 组件调用 REST 接口并获得响应结果
  3. 解析 REST 接口的响应结果,封装成 JSON 对象,并且返回给调用者。 ```java @AllArgsConstructor @Slf4j class RealRpcDemoClientImpl implements MockDemoClient { final String contextPath = TestConstants.DEMO_CLIENT_PATH;

    //hello简单实现 public RestOut hello() {

    1. /**
    2. * 远程调用接口的方法,完成 demo-provider 的 REST API 远程调用
    3. * REST API 功能:返回 hello world
    4. */
    5. String uri = "api/demo/hello/v1";
    6. /**
    7. * 组装 REST 接口 URL
    8. */
    9. String restUrl = contextPath + uri;
    10. log.info("restUrl={}", restUrl);
  1. /**
  2. * 通过 HttpClient 组件调用 REST 接口
  3. */
  4. String responseData = null;
  5. try
  6. {
  7. responseData = HttpRequestUtil.simpleGet(restUrl);
  8. } catch (IOException e)
  9. {
  10. e.printStackTrace();
  11. }
  12. /**
  13. * 解析 REST 接口的响应结果,解析成 JSON 对象,并且返回
  14. */
  15. RestOut<JSONObject> result = JsonUtil.jsonToPojo(responseData, new TypeReference<RestOut<JSONObject>>()
  16. {
  17. });
  18. return result;
  19. }
  20. //echo简单实现
  21. public RestOut<JSONObject> echo(String word)
  22. {
  23. /**
  24. * 远程调用接口的方法, 完成 demo-provider 的 REST API 远程调用
  25. * REST API 功能: 回显输入的信息
  26. */
  27. String uri = "api/demo/echo/{0}/v1";
  28. /**
  29. * 组装 REST 接口 URL
  30. */
  31. String restUrl = contextPath + MessageFormat.format(uri, word);
  32. log.info("restUrl={}", restUrl);
  33. /**
  34. * 通过 HttpClient 组件调用 REST 接口
  35. */
  36. String responseData = null;
  37. try
  38. {
  39. responseData = HttpRequestUtil.simpleGet(restUrl);
  40. } catch (IOException e)
  41. {
  42. e.printStackTrace();
  43. }
  44. /**
  45. * 解析 REST 接口的响应结果,解析成 JSON 对象,并且返回
  46. */
  47. RestOut<JSONObject> result = JsonUtil.jsonToPojo(responseData, new TypeReference<RestOut<JSONObject>>()
  48. {
  49. });
  50. return result;
  51. }

}

  1. <a name="mGh87"></a>
  2. ## 3.1.3 基础原理:代理模式与RPC客户端实现类
  3. ![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 />代理模式分为静态代理和动态代理
  4. 1. 静态代理: 在代码编写阶段由工程师提供代理类的源码,再编译成代理类。所谓静态,就是在程序运行前就已经存在代理类的字节码文件,代理类和被委托类的关系在运行前就确定了。
  5. 1. 动态代理: 在代码编写阶段不用关心具体的代理实现类,而是在运行阶段直接获取具体的代理对象,代理实现类由 JDK 负责生成。
  6. 通过静态代理模式实现 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)
  7. ```java
  8. @AllArgsConstructor
  9. @Slf4j
  10. class DemoClientStaticProxy implements DemoClient
  11. {
  12. /**
  13. * 被代理的真正实例
  14. */
  15. private MockDemoClient realClient;
  16. @Override
  17. public RestOut<JSONObject> hello()
  18. {
  19. log.info("hello 方法被调用");
  20. return realClient.hello();
  21. }
  22. @Override
  23. public RestOut<JSONObject> echo(String word)
  24. {
  25. log.info("echo 方法被调用");
  26. return realClient.echo(word);
  27. }
  28. }

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

  1. 上面的 RPC 实现类是出于演示目的而做了简化,对委托类并没有做任何扩展。 而实际的远程调用代理类会对委托类进行很多扩展,比如远程调用时的负载均衡、熔断、重试等。
  2. 上面的 RPC 实现类是动态代理实现类的学习铺垫。 Feign 的 RPC 客户端实现类是一个JDK 动态代理类,是在运行过程中动态生成的。

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

    静态代理的缺陷

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

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

动态代理是由JDK通过反射技术在执行阶段动态生成代理类,所以叫动态代理,获取动态代理实例的大致3个步骤:

  1. 明确代理类和被委托类共同的抽象接口,JDK生成的动态代理类会实现该接口
  2. 构造调用处理器对象,该对象要实现InvocationHandler接口,实现其唯一的抽象方法invoke
  3. 通过Proxy.newProxyInstance()方法在运行阶段获取JDK生成的动态代理类的实例,该方法需要三个参数
    1. 类装载器
    2. 抽象接口的class对象
    3. 调用处理器对象

例子:抽象接口MockDemoClient的一个动态代理实例:

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

image.png
创建动态代理实例的核心是创建一个JDK调用处理器InvocationHandler的实现类,调用处理器DemoClientInvocationHandler的代码如下:

  1. /**
  2. * 动态代理的调用处理器
  3. */
  4. @Slf4j
  5. public class DemoClientInocationHandler implements InvocationHandler
  6. {
  7. /**
  8. * 被代理的委托类实例
  9. */
  10. private MockDemoClient realClient;
  11. public DemoClientInocationHandler(MockDemoClient realClient)
  12. {
  13. this.realClient = realClient;
  14. }
  15. @Override
  16. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  17. {
  18. String name = method.getName();
  19. log.info("{} 方法被调用", method.getName());
  20. /**
  21. * 直接调用 hello 方法
  22. */
  23. if (name.equals("hello"))
  24. {
  25. return realClient.hello();
  26. }
  27. /**
  28. * 通过 Java 反射 调用 echo 方法
  29. */
  30. if (name.equals("echo"))
  31. {
  32. return method.invoke(realClient, args);
  33. }
  34. /**
  35. * 通过 Java 反射 调用其他的方法
  36. */
  37. Object result = method.invoke(realClient, args);
  38. return result;
  39. }
  40. }

测试代码

  1. @Test
  2. public void dynamicProxyTest() throws IOException {
  3. /**
  4. * 被代理的真实 PRC 调用类
  5. */
  6. MockDemoClient realObject = new RealRpcDemoClientImpl();
  7. //参数1:类装载器
  8. ClassLoader classLoader = ProxyTester.class.getClassLoader();
  9. //参数2:代理类和委托类共同的抽象接口
  10. Class[] clazz = new Class[]{MockDemoClient.class};
  11. //参数3:动态代理的调用处理器
  12. InvocationHandler invocationHandler = new DemoClientInocationHandler(realObject);
  13. /**
  14. * 使用以上三个参数,创建 JDK 动态代理类
  15. */
  16. MockDemoClient proxy = (MockDemoClient)
  17. Proxy.newProxyInstance(classLoader, clazz, invocationHandler);
  18. RestOut<JSONObject> result1 = proxy.hello();
  19. log.info("result1={}", result1.toString());
  20. RestOut<JSONObject> result2 = proxy.echo("回显内容");
  21. log.info("result2={}", result2.toString());
  22. /**
  23. * 将动态代理类的class字节码,保存在当前的工程目录下
  24. */
  25. byte[] classFile = ProxyGenerator.generateProxyClass(
  26. "Proxy0",
  27. RealRpcDemoClientImpl.class.getInterfaces());
  28. /**
  29. * 输出到文件
  30. */
  31. FileOutputStream fos = new FileOutputStream(new File("Proxy0.class"));
  32. fos.write(classFile);
  33. fos.flush();
  34. fos.close();
  35. }

3.1.5 JDK动态代理机制的原理

动态代理的实质是通过Proxy.newProxyInstance()方法生成一个动态代理的实例

  1. @CallerSensitive
  2. public static Object newProxyInstance(ClassLoader loader,
  3. Class<?>[] interfaces,
  4. InvocationHandler h)
  5. throws IllegalArgumentException
  6. {
  7. ...
  8. }

三个参数的介绍:

  1. ClassLoader类加载器,此处的类加载器和被委托的类加载器相同即可
  2. Class[]类型,代表动态代理类将会实现的抽象接口,也是被委托类所实现的接口
  3. InvocationHandler类型,它的调用处理器实例将作为JDK生成的动态代理对象的内部成员,在对动态代理对象进行方法调用时,invoke方法会被执行

    3.2 模拟Feign RPC动态代理的实现

    image.png

    3.2.1 模拟Feign的方法处理器MethodHandler

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

    1. /**
    2. * RPC 方法处理器
    3. */
    4. interface RpcMethodHandler
    5. {
    6. /**
    7. * 功能:组装 url,完成 REST RPC 远程调用,并且返回 JSON结果
    8. *
    9. * @param argv RPC 方法的参数
    10. * @return REST 接口的响应结果
    11. * @throws Throwable 异常
    12. */
    13. Object invoke(Object[] argv) throws Throwable;
    14. }

    模拟的 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);
  1. /**
  2. * 通过 HttpClient 组件调用 REST 接口
  3. */
  4. String responseData = HttpRequestUtil.simpleGet(restUrl);
  5. /**
  6. * 解析 REST 接口的响应结果,解析成 JSON 对象,并且返回
  7. */
  8. RestOut<JSONObject> result = JsonUtil.jsonToPojo(responseData, new TypeReference<RestOut<JSONObject>>()
  9. {
  10. });
  11. return result;
  12. }

}

  1. <a name="OkFmZ"></a>
  2. ## 3.2.2 模拟Feign的调用处理器InvocationHandler
  3. 调用处理器 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)
  4. ```java
  5. @Slf4j
  6. class MockInvocationHandler implements InvocationHandler
  7. {
  8. /**
  9. * 远程调用的分发映射:根据方法名称,分发方法处理器
  10. * key: 远程调用 Feign 接口的方法名称
  11. * value: 方法处理器 handler
  12. */
  13. private Map<Method, RpcMethodHandler> dispatch;
  14. /**
  15. * 代理对象的创建
  16. *
  17. * @param clazz 被代理的接口类型
  18. * @return 代理对象
  19. */
  20. public static <T> T newInstance(Class<T> clazz)
  21. {
  22. /**
  23. * 从远程调用接口的类级别注解中,获取 REST 地址的 contextPath 部分
  24. */
  25. Annotation controllerAnno = clazz.getAnnotation(RestController.class);
  26. if (controllerAnno == null)
  27. {
  28. return null;
  29. }
  30. String contextPath = ((RestController) controllerAnno).value();
  31. MockInvocationHandler invokeHandler = new MockInvocationHandler();
  32. invokeHandler.dispatch = new LinkedHashMap<>();
  33. /**
  34. * 通过反射,迭代远程调用接口中的每一个方法,组装对应的 MockRpcMethodHandler 模拟方法处理器
  35. */
  36. for (Method method : clazz.getMethods())
  37. {
  38. Annotation methodAnnotation = method.getAnnotation(GetMapping.class);
  39. if (methodAnnotation == null)
  40. {
  41. continue;
  42. }
  43. /**
  44. * 从远程调用接口的方法级别注解中,获取 REST 地址的 uri 部分
  45. */
  46. String uri = ((GetMapping) methodAnnotation).name();
  47. /**
  48. * 组装 MockRpcMethodHandler 模拟方法处理器
  49. * 注入 REST 地址的 contextPath 部分和uri部分
  50. */
  51. MockRpcMethodHandler handler = new MockRpcMethodHandler(contextPath, uri);
  52. /**
  53. *将模拟方法处理器handler 实例缓存到 dispatch 映射中
  54. */
  55. invokeHandler.dispatch.put(method, handler);
  56. }
  57. T proxy = (T) Proxy.newProxyInstance(clazz.getClassLoader(),
  58. new Class<?>[]{clazz},
  59. invokeHandler);
  60. return proxy;
  61. }
  62. /**
  63. * 动态代理的方法调用
  64. *
  65. * @param proxy 动态代理实例
  66. * @param method 待调用的方法
  67. * @param args 方法实参
  68. * @return 返回值
  69. * @throws Throwable 抛出的异常
  70. */
  71. @Override
  72. public Object invoke(Object proxy,
  73. Method method, Object[] args) throws Throwable
  74. {
  75. if ("equals".equals(method.getName()))
  76. {
  77. Object other = args.length > 0 && args[0] != null ? args[0] : null;
  78. return equals(other);
  79. } else if ("hashCode".equals(method.getName()))
  80. {
  81. return hashCode();
  82. } else if ("toString".equals(method.getName()))
  83. {
  84. return toString();
  85. }
  86. log.info("远程方法 {} 被调用", method.getName());
  87. /**
  88. * 从 dispatch 映射中,根据方法名称,获取方法处理器
  89. */
  90. RpcMethodHandler rpcMethodHandler = dispatch.get(method);
  91. /**
  92. * 方法处理器组装 url,完成 REST RPC 远程调用,并且返回 JSON结果
  93. */
  94. return rpcMethodHandler.invoke(args);
  95. }
  96. }

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

image.png

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

  1. @Slf4j
  2. public class FeignProxyMockTester
  3. {
  4. /**
  5. * 测试用例
  6. */
  7. @Test
  8. public void test() throws IOException
  9. {
  10. /**
  11. * 创建远程调用接口的本地JDK Proxy代理实例
  12. */
  13. MockDemoClient proxy =
  14. MockInvocationHandler.newInstance(MockDemoClient.class);
  15. /**
  16. * 通过模拟接口,完成远程调用
  17. */
  18. RestOut<JSONObject> responseData = proxy.hello();
  19. log.info(responseData.toString());
  20. /**
  21. * 通过模拟接口,完成远程调用
  22. */
  23. RestOut<JSONObject> echo = proxy.echo("proxyTest");
  24. log.info(echo.toString());
  25. }
  26. }

3.2.5 Feign弹性RPC客户端实现类

Feign 通过调用处理器、方法处理器完成了对 RPC 被委托类的增强,其调用处理器 InvocationHandler 通过对第三方组件如 Ribbon、Hystrix 的使用, 使 Feign 动态代理 RPC 客户端类具备了客户端负载均衡、失败回退、 熔断器、舱壁隔离等一系列的 RPC 保护能力。
image.png

  1. 失败回退: 当 RPC 远程调用失败时将执行回退代码,尝试通过其他方式来规避处理,而不是产生一个异常。
  2. 熔断器熔断: 当 RPC 远程服务被调用时, 熔断器将监视这个调用。如果调用的时间太长, 那么熔断器将介入并中断调用。如果 RPC 调用失败的次数达到某个阈值, 那么将会采取快速失败策略终止持续的调用失败
  3. 舱壁隔离: 如果所有 RPC 调用都使用同一个线程池,那么很有可能一个缓慢的远程服务将拖垮整个应用程序。弹性客户端应该能够隔离每个远程资源,并分配各自的舱壁线程池,使之相互隔离, 互不影响。
  4. 客户端负载均衡: RPC 客户端可以在服务提供者的多个实例之间实现多种方式的负载均衡,比如轮询、随机、权重等。

    3.3 Feign弹性RPC客户端的重要组件

    在微服务启动时, Feign 会进行包扫描,对加@FeignClient 注解的 RPC 接口创建远程接口的本地 JDK 动态代理实例。之后这些本地 Proxy 动态代理实例会注入 Spring IOC 容器中。当远程接口的方法被调用时,由 Proxy 动态代理实例负责完成真正的远程访问并返回结果。

    3.3.2 Feign的动态代理RPC客户端实例

    ```java @FeignClient(

    1. value = "demo-provider", path = "/demo-provider/api/demo/",
    2. 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”,
      1. method = RequestMethod.GET)
      RestOut echo(
      1. @PathVariable(value = "word") String word);

} ``` image.png

3.3.3 Feign的调用处理器InvocationHandler

Feign 提供了一个默认的调用处理器, 名为 FeignInvocationHandler 类,该类完成基本的调用处理逻辑,处于 feign-core 核心 JAR 包中。
Feign 的调用处理器可以进行替换,如果 Feign是与 Hystrix 结合使用的, 就会被替换成 HystrixInvocationHandler 调用处理器类,而该类处于 feignhystrix 的 JAR 包中。
image.png

3.3.4 Feign的方法处理器MethodHandler
image.png

3.3.5 Feign的客户端组件

不同的 feign.Client 客户端实现类其内部提交 HTTP 请求的技术是不同的。常用的 Feign 客户端实现类如下:

  1. Client.Default 类:默认的实现类,使用 JDK 的 HttpURLConnnection 类提交 HTTP 请求。
  2. ApacheHttpClient 类:该客户端类在内部使用 Apache HttpClient 开源组件提交 HTTP 请求
  3. OkHttpClient 类:该客户端类在内部使用 OkHttp3 开源组件提交 HTTP 请求。
  4. LoadBalancerFeignClient 类:内部使用 Ribbon 负载均衡技术完成 HTTP 请求处理。

image.png