SpringCloud远程服务调用三种方式及原理

目录
  • 一个简单的微服务架构图
  • 调用远程服务的三种方式
    • 1、基于 RestTemplate 和 @LoadBalanced 注解
    • 2、基于DiscoveryClient
    • 3、基于 Feign 的声明式调用
  • 原理分析
    • 1、以 @LoadBalanced 为入口开启源码之旅
    • 2、请求调用流程
  • Spring @Qualifier 注解的妙用

一个简单的微服务架构图

本文设计的 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 接口的工具类, 它提供了 GETPOST 等常用的请求方法.使用方式如下:

添加到 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();
	}
}

紧接着 看 InterceptingClientHttpRequestexecute 方法

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 这个类做检索, 这里不再详细阐述.

到此这篇关于SpringCloud远程服务调用三种方式及原理的文章就介绍到这了,更多相关SpringCloud服务调用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringCloud让微服务实现指定程序调用

    我们在做微服务时,有时候需要将微服务做一些限制,比如只能我们自己的服务调用,不能通过浏览器直接调用等. 我们可以使用spring cloud sleuth,在应用调用微服务时通过Tracer产生一个traceId,并通过request设置到header里面, 然后sleuth会将该traceId在整个链路传递,我们在微服务中定义一个拦截器,取到header里面的traceId并和链路中的traceId比较, 如果相等,则表明是我们自己的应用调用,拦截器通过,否则这次请求被拦截 代码详见githu

  • SpringCloud通过Nacos实现注册中心与远程服务调用详解流程

    目录 1. 基于Nacos实现服务注册与发现 1.1 pom依赖 1.2 yaml配置 1.3 添加启动注解 1.4 启动服务查看控制台 2.基于Nacos实现远程服务调用 2.1 客户端创建RestTemplate Bean 2.2 客户端调用代码 2.3 服务端暴露接口 2.4 服务调用测试 本文主要记录基于Nacos实现服务注册中心和远程服务调用 1. 基于Nacos实现服务注册与发现 基于pring-boot-starter-parent 2.6.8,pring-cloud-depend

  • SpringCloud之Feign代理,声明式服务调用方式

    目录 引入相关依赖然后再主入口启用注解 引入相关依赖然后再主入口启用注解:@Enabl Feign配合Ribbon.Hystrix的超时策略配置如下 1.pom 2.主入口 3.配置文件 4.业务代码与实现 5.controller测试 将其他微服务中的服务接口,用feign在本项目中进行调用. Spring Cloud Feign是一套基于Netflix Feign实现的声明式服务调用客户端.它使得编写Web服务客户端变得更加简单.我们只需要通过创建接口并用注解来配置它既可完成对Web服务接口

  • SpringCloud 服务负载均衡和调用 Ribbon、OpenFeign的方法

    1.Ribbon Spring Cloud Ribbon是基于Netflix Ribbon实现的-套客户端―负载均衡的工具. 简单的说,Ribbon是Netlix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用.Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等.简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器.我们很容易使用Ribbon实现自定义的负载均

  • springcloud使用feign调用服务时参数内容过大问题

    目录 feign调用服务时参数内容过大 场景 解决方法 feign消费时,如果传入参数过长 导致feign.FeignException: status 400 reading错误 解决办法 feign调用服务时参数内容过大 场景 前端参数传入到gateway后,gateway使用feign调用服务时,传入的参数内容过大(参数常见于富文本.或者其他附属信息过多)会导致传输不过去,虽然配置可以调节内容大小,但是最大的也有上限,所以特殊处理一道. 例如该类参数: 解决方法 可新增两个redis公共方

  • SpringCloud Feign服务调用请求方式总结

    前言 最近做微服务架构的项目,在用feign来进行服务间的调用.在互调的过程中,难免出现问题,根据错误总结了一下,主要是请求方式的错误和接参数的错误造成的.在此进行一下总结记录.以下通过分为三种情况说明,无参数,单参数,多参数.每种情况再分get和post两种请求方式进行说明.这样的话,6种情况涵盖了feign调用的所有情况. 有个建议就是为了保证不必要的麻烦,在写feign接口的时候,与我们的映射方法保持绝对一致,同时请求方式,请求参数注解也都不偷懒的写上.如果遵循这种规范,可以避开90%的调

  • SpringCloud服务的发现与调用详解

    目录 前言 一.服务提供者 二.服务消费者 总结 相关推荐 上一章:Eureka注册中心 前言 上一章中,我们介绍了Eureka注册中心及集群的搭建,这一节将介绍服务的发现和调用.注意,这个时候我们只有注册中心,并没有引入其他的组件,所以需要使用SpringCloud原生态的服务发现和调用的方式实现,循序渐进的带你走入微服务的世界. 上篇文章我们已经创建好了注册中心,这次我们需要创建一个服务提供者(provider)和一个服务消费者(consumer)两个项目. 一.服务提供者 新建Maven项

  • SpringCloud超详细讲解Feign声明式服务调用

    目录 入门案例 @FeignClient注解详解 Feign Client的配置 Feign请求添加headers 负载均衡 (Ribbon) 容错机制 Hystrix支持 Sentinel支持 Feign开启容错机制支持后的使用方式 请求压缩feign.compression 日志级别 入门案例 在服务消费者导入依赖 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>

  • springcloud feign服务之间调用,date类型转换错误的问题

    目录 feign服务之间调用,date类型转换错误 自定义feign请求头 通过判断是否为feign请求 OpenFeign服务间调用时日期格式异常 异常为 原因 解决方法 feign服务之间调用,date类型转换错误 最近尝试换springcloud开发,原先是springboot,每次的返回值的Date类型都通过@ControllerAdvice格式化yyyy-MM-dd HH:mm:ss然后返回的.这次用feign之后,2个服务之间调用,一直报错查了好久百度都搞不定,后面灵光一闪...不多

  • SpringCloud远程服务调用三种方式及原理

    目录 一个简单的微服务架构图 调用远程服务的三种方式 1.基于 RestTemplate 和 @LoadBalanced 注解 2.基于DiscoveryClient 3.基于 Feign 的声明式调用 原理分析 1.以 @LoadBalanced 为入口开启源码之旅 2.请求调用流程 Spring @Qualifier 注解的妙用 一个简单的微服务架构图 本文设计的 Spring Cloud 版本以及用到的 Spring Cloud 组件 Spring Cloud Hoxton.SR5 eur

  • Go语言net包RPC远程调用三种方式http与json-rpc及tcp

    目录 一.服务端 二.http客户端 三.TCP客户端 四.json客户端 五.运行结果 rpc有多种调用方式,http.json-rpc.tcp 一.服务端 在代码中,启动了三个服务 package main import ( "log" "net" "net/http" "net/rpc" "net/rpc/jsonrpc" "sync" ) //go对RPC的支持,支持三个级别:T

  • 判断python对象是否可调用的三种方式及其区别详解

    查找资料,基本上判断python对象是否为可调用的函数,有三种方法 使用内置的callable函数 callable(func) 用于检查对象是否可调用,返回True也可能调用失败,但是返回False一定不可调用. 官方文档:https://docs.python.org/3/library/functions.html?highlight=callable#callable 判断对象类型是否是FunctionType type(func) is FunctionType # 或者 isinst

  • 详解Shell脚本中调用另一个Shell脚本的三种方式

    主要以下有几种方式: Command Explanation fork 新开一个子 Shell 执行,子 Shell 可以从父 Shell 继承环境变量,但是子 Shell 中的环境变量不会带回给父 Shell. exec 在同一个 Shell 内执行,但是父脚本中 exec 行之后的内容就不会再执行了 source 在同一个 Shell 中执行,在被调用的脚本中声明的变量和环境变量, 都可以在主脚本中进行获取和使用,相当于合并两个脚本在执行. 第一种:fork 特点:会生成子PID而且可重复被

  • 详解SpringBoot 调用外部接口的三种方式

    目录 1.简介 2.方式一:使用原始httpClient请求 3.方式二:使用RestTemplate方法 4.方式三:使用Feign进行消费 1.简介 SpringBoot不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程.在Spring-Boot项目开发中,存在着本模块的代码需要访问外面模块接口,或外部url链接的需求, 比如在apaas开发过程中需要封装接口在接口中调用apaas提供的接口(像发起流程接口submit等等)下面也是

  • Android 打包三种方式实例详解

     Android 打包三种方式实例详解 前言: 现在市场上很多app应用存在于各个不同的渠道,大大小小几百个,当我们想要在发布应用之后统计各个渠道的用户下载量,我们就要进行多渠道打包. 01.应用的打包签名什么是打包? 打包就是根据签名和其他标识生成安装包. 签名是什么? 1.在android应用文件(apk)中保存的一个特别字符串 2.用来标识不同的应用开发者:开发者A,开发者B 3.一个应用开发者开发的多款应用使用同一个签名 就好比是一个人写文章,签名就相当于作者的署名. 如果两个应用都是一

  • 详解JS异步加载的三种方式

    一:同步加载 我们平时使用的最多的一种方式. <script src="http://yourdomain.com/script.js"></script> <script src="http://yourdomain.com/script.js"></script> 同步模式,又称阻塞模式,会阻止浏览器的后续处理,停止后续的解析,只有当当前加载完成,才能进行下一步操作.所以默认同步执行才是安全的.但这样如果js中有输

  • JavaScript模拟实现封装的三种方式及写法区别

    前  言   继承是使用一个子类继承另一个父类,那么子类可以自动拥有父类中的所有属性和方法,这个过程叫做继承!  JS中有很多实现继承的方法,今天我给大家介绍其中的三种吧. 1.在 Object类上增加一个扩展方法 //声明一个父类 function Person(name){ this.name=name; this.age=age; this.say=function(){ alert("我叫"+this.name); } } //声明一个子类 function Student()

  • python 使用elasticsearch 实现翻页的三种方式

    使用ES做搜索引擎拉取数据的时候,如果数据量太大,通过传统的from + size的方式并不能获取所有的数据(默认最大记录数10000),因为随着页数的增加,会消耗大量的内存,导致ES集群不稳定.因此延伸出了scroll,search_after等翻页方式. 一.from + size 浅分页 "浅"分页可以理解为简单意义上的分页.它的原理很简单,就是查询前20条数据,然后截断前10条,只返回10-20的数据.这样其实白白浪费了前10条的查询. GET test/_search { &

随机推荐