SpringCloud远程服务调用三种方式及原理
nimo10050 人气:0一个简单的微服务架构图
本文设计的 Spring Cloud 版本以及用到的 Spring Cloud 组件
- Spring Cloud Hoxton.SR5
- eureka
- feign
- ribbon
后面的内容都将围绕上面的图来分析.
调用远程服务的三种方式
在 Spring Cloud 服务架构中, 一个服务可能部署多个实例, 通常情况下, 这个时候请求一个服务接口, 是需要通过 服务名 去调用的, 比如: http://user-service/getUser
.
然后在 外力 的帮助下, 通过服务名拿到多个实例的地址列表, 再借助负载均衡算法, 从地址列表中选择一个具体的地址, 发送 HTTP 请求.
具体的做法分为如下三种:
1、基于 RestTemplate 和 @LoadBalanced 注解
RestTemplate
是 spring-web 包提供的, 用来调用 HTTP 接口的工具类, 它提供了 GET
、POST
等常用的请求方法.使用方式如下:
添加到 spring 容器
@Bean public RestTemplate restTemplate() { return new RestTemplate(); }
使用前注入依赖
@Autowired private RestTemplate restTemplate;
常用 API
// 发送 GET 请求 restTemplate.getForObject(...) // 发送 POST 请求 restTemplate.postForObject(...) // 自定义 restTemplate.execute(...)
按照上面那种简单的写法, 我们只能调用有明确 IP 和 端口 的接口, 要想实现我们的需求, 至少要做两件事情:
- 根据服务名拿到服务实例的信息
- 负载均衡算法
RestTemplate
提供了拦截器的功能 ClientHttpRequestInterceptor
, 开发者可以 手动编码 实现上面两个功能. Spring Cloud 已经帮我们实现了这个功能.使用方式如下:
在原有基础上加上 @LoadBalanced
注解
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
调用接口时,传入服务名称
User user = restTemplate.getForObject("http://user-service/getUser", User.class);
一个注解就帮我们完成了负载均衡.
2、基于DiscoveryClient
org.springframework.cloud.client.discovery.DiscoveryClient
可以帮我们实现服务发现的功能, 只要我们拿到服务对应的实例信息, 后面 负载均衡 可以手动编码实现.
注入依赖
@Autowired private DiscoveryClient discoveryClient;
获取注册中心服务实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
选取一个实例的地址信息, 发送请求
3、基于 Feign 的声明式调用
在启动类上加对应的注解.
@EnableFeignClients
声明接口
@FeignClient("user-service") public interface UserFeignClient { @GetMapping("/getUser") User getUser(); }
原理分析
关于源码分析部分, 本文并不会逐行分析, 只会把 关键方法 注释说明(如果读者自行 debug, 是不会迷路的.), 中间很多无聊的方法跳转的过程都省略了.
RestTemplate 与 @LoadBalanced 注解的带来的 “化学反应”
先看一下大致的实现思路.
1、以 @LoadBalanced 为入口开启源码之旅
源码注释的大概意思是, 在 RestTemplate
上加上这个注解, 就能使用 LoadBalancerClient
接口 做一些事情, 通过查看这个接口的注释, 它能提供的能力跟负载均衡相关.
所以,到这里我们已经清楚的了解到 @LoadBalanced
注解能为我们提供 负载均衡 的能力, 下面就需要弄清楚底层是如何实现负载均衡的.
Annotation to mark a RestTemplate or WebClient bean to be configured to use a LoadBalancerClient
通过查看源代码, 我们在如下两个地方看到了 @LoadBalanced 的使用, 通过调试发现, 断点根本没有走到第二个地方.
public class LoadBalancerAutoConfiguration { @LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList(); }
public class LoadBalancerWebClientBuilderBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof WebClient.Builder) { if (context.findAnnotationOnBean(beanName, LoadBalanced.class) == null) { return bean; } ((WebClient.Builder) bean).filter(exchangeFilterFunction); } return bean; } }
所以我们还是要把目光聚焦到下面的源代码:
public class LoadBalancerAutoConfiguration { @LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList(); }
这里我通过描述这块代码的逻辑, 来引出一个有趣的 Spring 相关的知识点(关于这个知识点原理, 可以先直接跳到文末 Spring @Qualifier 注解的妙用):
首先, 我们应该知道, 通过如下方式, 我们可以把 Spring 容器中的所有 RestTemplate
类型的 Bean
对象添加到下面的集合中.
@Autowired private List<RestTemplate> restTemplates = Collections.emptyList();
而我们在上面的基础上再加上 @LoadBalanced
注解, 那么这个集合收集的元素就加了一层限制条件, 集合中的 Bean
不仅要是 RestTemplate
类型, 而且 Bean
在声明时, 必须加上 @LoadBalanced
注解, 比如下面的声明方式:
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
然后我们接着看 Spring Cloud 如何对 RestTemplate
进行加工的
public class LoadBalancerAutoConfiguration { @LoadBalanced @Autowired(required = false) private List<RestTemplate> restTemplates = Collections.emptyList(); @Autowired(required = false) private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList(); // 第一步: 遍历 restTemplates 集合 @Bean public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated( final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) { return () -> restTemplateCustomizers.ifAvailable(customizers -> { for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) { // RestTemplateCustomizer#customize for (RestTemplateCustomizer customizer : customizers) { customizer.customize(restTemplate); } } }); } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") static class LoadBalancerInterceptorConfig { // 第二步: 进行自定义操作, 也就是把 LoadBalancerInterceptor 这个我们文章开头提到的拦截器设置进去. @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer( final LoadBalancerInterceptor loadBalancerInterceptor) { return restTemplate -> { List<ClientHttpRequestInterceptor> list = new ArrayList<>( restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); }; } }
到此为止, 程序启动前的一些关键步骤已经搞清楚了, 下面继续分析调用流程.
2、请求调用流程
源码入口:
User user = restTemplate.getForObject("http://user-service/getUser", User.class);
顺着 getForObject
进到关键方法
public class RestTemplate { // doExecute protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException { // 创建请求对象 // 这里最终其实通过 InterceptingClientHttpRequestFactory#createRequest 方法 // 创建了 InterceptingClientHttpRequest ClientHttpRequest request = createRequest(url, method); response = request.execute(); } }
紧接着 看 InterceptingClientHttpRequest
的 execute
方法
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest { // 第一步: 执行完父类的 execute 方法后, 会来到这里. @Override protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException { InterceptingRequestExecution requestExecution = new InterceptingRequestExecution(); return requestExecution.execute(this, bufferedOutput); } private class InterceptingRequestExecution implements ClientHttpRequestExecution { private final Iterator<ClientHttpRequestInterceptor> iterator; public InterceptingRequestExecution() { this.iterator = interceptors.iterator(); } // 第二步: 先 执行前面设置的拦截器 LoadBalancerInterceptor 通过 服务名, + 负载均衡 , 拿到其中一个实例的请求地址. // 然后根据真实的地址, 发送 http 请求. @Override public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException { // 先 执行前面设置的拦截器 LoadBalancerInterceptor 通过 服务名, + 负载均衡 , 拿到其中一个实例的请求地址. nextInterceptor.intercept(request, body, this); // 然后根据真实的地址, 发送 http 请求. AbstractClientHttpRequest#execute return delegate.execute(); } } } }
LoadBalancerInterceptor
的负载均衡处理
到这里, 我们就可以回答开头提到的问题: @LoadBalanced
是如何给 RestTemplate
提供负载均衡能力的, 众所周知 Ribbon 的能力就 负载均衡.
源码再往后看就是 Ribbon 的领域了, 我们不再继续深究. 后面可以单独写一篇文章对 Ribbon 的原理和源码进行分析.
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor { // 看到这里, 我们应该回想到一开始说的, @LoadBalanced 注解的相关注释说明. // 加上 @LoadBalanced 注解, 我们就能给 RestTemplate 赋予负载均衡的能力. private LoadBalancerClient loadBalancer; @Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { // 因为我们集成了 Ribbon、 所以这里 loadBalancer 就是 RibbonLoadBalancerClient return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); } }
Spring @Qualifier 注解的妙用
/** * This annotation may be used on a field or parameter as a qualifier for * candidate beans when autowiring. It may also be used to annotate other * custom annotations that can then in turn be used as qualifiers. */ @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Qualifier { String value() default ""; }
不管是根据上面的注释, 还是我们的使用经验来讲, 我们都应该知道 @Qualifier
这个注解:它起到的是限定, “精确匹配”Bean
的作用,比如: 当同一类型的 Bean
有多个不同实例时,可通过此注解来做 筛选或匹配。
然后再来看下这个注解的一段注释:
It may also be used to annotate other custom annotations that can then in turn be used as qualifiers.
简单翻一下就是: @Qualifier
可以注解其他 自定义的注解, 然后这些 自定义注解 就可以反过来为我们注入 Bean 时, 起到限定的作用(上面已经讲过它限定了什么).
于是我们再回过头看下 @LoadBalanced
注解源码:
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Qualifier public @interface LoadBalanced { }
从上可以看出, 这个自定义注解上是包含 @Qualifier
, 所以 @LoadBalanced
注解是可以在我们注入 bean 时, 起到限定作用的.
关于 @Qualifier
详细的源码和原理分析 可以围绕 QualifierAnnotationAutowireCandidateResolver
这个类做检索, 这里不再详细阐述.
加载全部内容