菜鸟的springcloud学习总结(三):服务调用
说明
更新时间:2020/9/28 00:13,更新到了OpenFeign
本文主要对springcloud中的服务调用进行学习与记录,主要偏向于实战,本文会持续更新,不断地扩充
本文仅为记录学习轨迹,如有侵权,联系删除。
一、服务调用
按照上面这张图进行学习,现在进行到了服务调用这一块
二、Ribbon
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
首先是负载均衡,在该系列文章的第二篇服务的注册与发现中,搭建服务集群的时候里面有用了一下Ribbon的负载均衡,下面基于之前的项目进行Ribbon的学习,启动的项目如下
Eureka 服务端 | cloud-eureka-server7001 |
支付服务模块1 | cloud-provider-payment8001 |
支付服务模块2 | cloud-provider-payment8002 |
消费者模块 | cloud-consumer-order80 |
两个支付服务模块组成服务集群,消费者模块通过负载均衡调用这两个服务
首先是Ribbon坐标的引入,这里需要说明一下,因为这里引入了eureka的依赖,这个依赖里面就已经包括了Ribbon的坐标依赖
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
查看Ribbon的坐标依赖,点开Eureka的坐标,发现里面已经包含了Ribbon依赖
(1)服务调用
Eureka的服务调用主要是使用RestTemplate来进行服务的调用,在该系列的第一和第二篇文章中就用的RestTemplate,但没有进行详细的说明,这里详细说明一下它的使用方法,即RestTemplate的服务调用方式
以cloud-consumer-order80为例,配置RestTemplate,新建config包,里面创建配置类ApplicationContextConfig
截图如下
代码如下,注意,这里加了LoadBalanced注解用于做负载均衡
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced//负载均衡,默认采用轮询的方式
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
创建接口,使用RestTemplate进行服务的调用
截图如下
代码如下
package com.zsc.controller;
import com.zsc.bean.PageResult;
import com.zsc.entity.Payment;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.List;
/**
* @ClassName : OrderController
* @Description :
* @Author : CJH
* @Date: 2020-09-27 16:38
*/
@RestController
@RequestMapping("/consumer")
public class OrderController {
// public static final String URL = "http://localhost:8001";//单节点
public static final String URL = "http://CLOUD-PROVIDER-PAYMENT";//集群:表示调用哪一个服务
/**
* (1)restTemplate.xxxForObject:返回对象为响应体中数据转化成的对象
* 例如:PageResult pageResult = restTemplate.getForObject(URL +"/payment/"+ id, PageResult.class);
* pageResult是自定义的统一返回结果类
*
*
* (2)restTemplate.xxxForEntity:返回对象为ResponseEntity对象,包含了一些响应中的重要信息,比如响应头,响应码等
* 例如:ResponseEntity<PageResult> entity = restTemplate.getForEntity(URL + "/payment/" + id, PageResult.class);
*
*/
/**
* 服务发现,对外暴露该服务的相关信息
*/
@Resource
private DiscoveryClient discoveryClient;
@Resource
private RestTemplate restTemplate;
@GetMapping("/discovery")
public Object discovery() {
//获得服务中心的所有服务
List<String> services = discoveryClient.getServices();
services.forEach(s -> {
System.out.println("==========================================================");
System.out.println("服务名称 = " + s);
//根据服务名称获取到服务的实例
List<ServiceInstance> instances = discoveryClient.getInstances(s);
instances.forEach(instance -> {
//服务的主机
System.out.println("服务的主机【instance.getHost()】 = " + instance.getHost());
//注册的服务id
System.out.println("注册的服务id【instance.getServiceId()】 = " + instance.getServiceId());
//服务的端口号
System.out.println("服务的端口号【instance.getPort()】 = " + instance.getPort());
//服务的uri
System.out.println("服务的uri【instance.getUri()】 = " + instance.getUri());
});
});
return this.discoveryClient;
}
@PostMapping("/payment")
public PageResult<Payment> save(@RequestBody Payment payment) {
PageResult pageResult = restTemplate.postForObject(URL+"/payment/", payment, PageResult.class);
return pageResult;
}
@GetMapping("/payment/{id}")
public PageResult<Payment> get(@PathVariable Long id) {
PageResult pageResult = restTemplate.getForObject(URL +"/payment/"+ id, PageResult.class);
return pageResult;
}
@GetMapping("/payment2/{id}")
public PageResult<Payment> get2(@PathVariable Long id) {
ResponseEntity<PageResult> entity = restTemplate.getForEntity(URL + "/payment/" + id, PageResult.class);
//getForEntity方法的返回
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
}else{
return new PageResult<>(444,"操作失败");
}
}
}
注意里面的调用的方式,CLOUD-PROVIDER-PAYMENT是要调用的服务的名称,这个名称在上一篇文章有写过怎么修改服务名称
上面的代码还有一段注释这里提出来讲一下,里面可以简单分为两种调用方式,xxxForEntity和xxxForObject,这两种调用方式的返回值是不一样的,具体说明如下
/**
* (1)restTemplate.xxxForObject:返回对象为响应体中数据转化成的对象
* 例如:PageResult pageResult = restTemplate.getForObject(URL +"/payment/"+ id, PageResult.class);
* pageResult是自定义的统一返回结果类
*
*
* (2)restTemplate.xxxForEntity:返回对象为ResponseEntity对象,包含了一些响应中的重要信息,比如响应头,响应码等
* 例如:ResponseEntity<PageResult> entity = restTemplate.getForEntity(URL + "/payment/" + id, PageResult.class);
*
*/
开始运行,启动的项目如下,以集群的方式运行,而且在上面的配置类中也配置了默认的负载均衡策略,所以刚好可以测试负载均衡的功能
访问cloud-consumer-order80的接口,作为消费者模块,它会去调用另外两个支付服务模块,而且是采用轮询的负载均衡策略进行调用
(2)负载均衡
简单说一下Ribbon自带的负载均衡策略
使用其他负载均衡策略,首先创建一个包ribbonrule,在里面创建一个类,用于定义负载均衡策略
注意:这个包的位置存放是有讲究的
注意:记得把配置类中LoadBalanced注解打开才会生效
然后再主启动类中添加一个注解即可
重启该项目,访问即可
(3)手写负载均衡策略
Ribbon自带的一些负载均衡策略可能不够用,这个时候就得自己手写写负载均衡算法,还是在原来的项目上进行改造,首先将启动类上的随机均衡策略注释掉
然后记得把配置类里面的注解LoadBalanced也注释掉
然后就可以开始手写负载均衡算法,还是模仿写一个轮询算法的均衡策略,具体情况要根据需要来
新建一个ribbonlb包,里面新建一个接口
在实现该接口
代码如下
package com.zsc.ribbonlb.impl;
import com.zsc.ribbonlb.LoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @ClassName : MyLb
* @Description : 自定义手写的负载均衡策略
* @Author : CJH
* @Date: 2020-09-29 16:39
*/
@Component
public class MyLb implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 第几次请求次数
* @return
*/
public final int getAndIncrement(){
int current;
int next;
do{
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0:current+1;
}while (!this.atomicInteger.compareAndSet(current,next));//调用cas进行自旋锁每次next+1
System.out.println("**********next:"+next);
return next;
}
//负载均衡算法:rest接口第几次请求次数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启接口计数从0开始
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
/**
* getAndIncrement():第几次请求次数
* serviceInstances.size():服务的集群数量
* index:返回得到集群服务器位置下标
*/
int index = getAndIncrement() % serviceInstances.size();
System.out.println("index = " + index);
return serviceInstances.get(index);//将服务实例返回
}
}
编写接口进行测试
代码如下
@Resource
private DiscoveryClient discoveryClient;
@Resource
private RestTemplate restTemplate;
/**
* 自定义类
*/
@Resource
private LoadBalancer loadBalancer;
//使用自定义的负载均衡策略算法
@GetMapping("/payment/LB")
public String LB(){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PROVIDER-PAYMENT");
if (instances == null || instances.size()<=0) {
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
System.out.println("uri = " + uri);
String result = restTemplate.getForObject(uri + "/payment/LB", String.class);
System.out.println("result = " + result);
return result;
}
在cloud-provider-payment8001项目中增加一个接口用来测试
同理cloud-provider-payment8002也做同样的接口,然后可以开始测试接口,启动服务
运行结果如下
成功实现了自定义轮询算法。
二、OpenFeign
OpenFeign也是主要用于服务调用,在使用 Ribbon + RestTemplate 时, 利用 RestTemplate 请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,对于服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常会针对每个微服务自行封装一些客户端类来包装这些服务依赖的调用。所以,Feign 在此基础上除了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign 的实现下,我们只需要创建一个可口,并使用注解的方式来配置它(以前是Dao 接口上main标注 Mapper 注解,现在是一个微服务接口上面标注一个Feign 注解即可),即可完成对一个服务提供方的接口绑定,简化了使用 Spring cloud Ribbon 时, 自动封装服务调用客户端的开发量。
下面开始实战
(1)模块创建
创建一个子模块cloud-consumer-openfeigh-order80,引入pom
<dependencies>
<!--引入公共包-->
<dependency>
<groupId>com.zsc</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--springboot starter启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
重点是openFeign和eureka坐标依赖,然后修改yml配置文件,将服务注册到eureka上面
server:
port: 80
spring:
application:
name: cloud-consumer-openfeign-order80
eureka:
client:
#将自己注册进eureka服务中心
register-with-eureka: true
fetch-registry: true
service-url:
#集群的情况下,服务的入住
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7002.com:7002/eureka/
# defaultZone: http://localhost:7001/eureka
然后是主启动类,注意加注解EnableFeignClients
(2)服务调用
它的服务调用跟Ribbon的调用不一样,它的调用更加方便,首先创建一个servic包,里面创建一个服务接口,用于存放需要调用的其他微服务的接口,注意该接口需要加上FeignClient注解,指明要调用哪一个服务
代码如下
@Component
@FeignClient(value = "CLOUD-PROVIDER-PAYMENT")//指定该接口对应的要调用的微服务的名称
public interface PaymentOpenFeignService{
/**
* 下面的这些接口对应要调用的服务的接口定义
* @return
*/
@GetMapping("/payment/discovery")
public Object discovery();
@PostMapping("/payment")
public PageResult<Payment> add(@RequestBody Payment payment);
@GetMapping("/payment/{id}")
public PageResult<Payment> query(@PathVariable("id") Long id);
@GetMapping("/payment/LB")
public String myLB();
@GetMapping("/payment/hello")
public String hello();
}
注意:该接口上的方法定义跟要调用的服务的controller的接口定义一样
controller创建
代码如下
@RestController
@RequestMapping("/consumer")
public class PaymentOpenFeignController {
@Resource
private PaymentOpenFeignService paymentOpenFeignService;
@GetMapping("/payment/hello")
public String hello(){
//直接调用服务接口
String hello = paymentOpenFeignService.hello();
System.out.println("hello = " + hello);
return hello;
}
@PostMapping("/payment")
public PageResult<Payment> add(@RequestBody Payment payment){
return paymentOpenFeignService.add(payment);
}
@GetMapping("/payment/discovery")
public Object discovery() {
//直接调用服务接口
Object discovery = paymentOpenFeignService.discovery();
return discovery;
}
@GetMapping("/payment/{id}")
public PageResult<Payment> query(@PathVariable("id") Long id) {
PageResult<Payment> query = paymentOpenFeignService.query(id);
return query;
}
@GetMapping("/payment/LB")
public String myLB(){
return paymentOpenFeignService.myLB();
}
}
这样通过接口的方式,直接调用其他的服务。下面开始测试,首先启动项目
(3)超时控制
采用这种方式调用服务,会有一个超时控制的问题,就是从消费者掉用支付服务,得到数据,这个过程中,消费者如果调用支付服务超1秒没有响应,消费者会直接报错,但其实支付服务可能只是网络不好导致超时,下面可以在支付服务设置4秒的暂停,之后再返回数据,模拟超时
重启项目,访问刚才定义的模拟超时的接口
就跟上面说的一样,这里报了超时错误,解决这个错误也简单,配置响应时间长一点即可
代码如下
ribbon:
#建立连接后从服务器读取到的可用资源所用的时间
ReadTimeout: 5000
#建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ConnectTimeout: 5000
重启服务后访问上面超时的接口,发现正常响应
(4)日志打印
openfeign还提供了日志打印的增强功能,可以详细的打印出来谁调用哪个服务,响应的结果等详细信息
创建配置类feignLoggerLevel
代码如下
@Configuration
public class OpenFeignLogConfig {
@Bean
Logger.Level feignLoggerLevel(){
/**
* NONE:默认不显示任何日子
* BASIC:仅记录请求方法、url、响应状态码以及执行时间
* HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息
* FULL:除了HEADERS中定义的消息之外,还有请求和响应的正文以及元数据
*/
return Logger.Level.FULL;
}
}
yml配置openfeign日志增加
代码如下
#配置openfeign日志增强
logging:
level:
#将server包日志等级定义为debug
com.zsc.service: debug
重启项目,访问任意接口
控制台信息输出,整个完整的信息都打印了出来