SpringCloud微服务间通信(同步通信HTTP)

0. 简介

0.1 目录

  • 0. 简介
    • 0.1 目录
    • 0.2 引言
    • 0.3 思考
  • 1. RestTemplate
    • 1.0 简介
    • 1.1 案例
    • 1.2 问题
  • 2. Ribbon
    • 2.1 流程与原理
    • 2.2 依赖
    • 2.3 实现负载均衡
      • 1. DiscoveryClient
      • 2. LoadBalancerClient
      • 3. @LoadBalance
    • 2.4 问题
    • 2.5 组件细节
      • 实现负载均衡原理
      • 负载均衡策略
      • 设置策略
    • 2.6 停止维护
  • 3. OpenFeign
    • 3.0 Ribbon回顾
    • 3.1 OpenFeign简介
      • Feign
      • OpenFeign
    • 3.2 使用
    • 3.3 小结
    • 3.4 参数传递
      • 1. 零散类型(GET)
        • a. QueryString传参
        • b. URL路径传参
      • 2. 对象类型(POST)
        • a. form表单
        • b. Application/JSON(常用/推荐)
      • 3. 数组和集合类型
        • a. 数组
        • b. 集合(了解)
    • 3.5 响应处理
      • 1. 返回对象
      • 2. 返回集合
      • 3. 返回分页
        • Map
        • FastJSON
    • 3.6 超时处理
      • 默认超时时间
      • 修改超时时间
    • 3.7 日志

0.2 引言

针对不同环境和目标的客户和服务可以通过不同的机制进行通信。根据协议,它可以是同步的或异步的。

同步通信-请求响应方法

在同步通信中,需要一个预定义的源服务地址,确切地将请求发送到何处,并且该服务(主叫方和被叫方)目前都应已启动。尽管协议可能是同步的,但I/O操作可以是异步的,其中客户端不必等待响应。这是I/O和协议的区别。 Web API通用的通用请求-响应方法包括REST,GraphQL和gRPC。

异步通讯

在异步通信的情况下,呼叫者不必具有被呼叫者的特定目的地。一次处理多个使用者变得相对容易(因为服务可能会增加使用者)。此外,如果接收服务关闭,则消息将排队,然后在接收服务打开时继续处理。从松散耦合,多服务通信以及应对部分服务器故障的角度来看,这尤其重要。这些是使微服务倾向于异步通信的决定性因素。诸如MQTT,STOMP,AMQP之类的异步协议由Apache Kafka Stream,RabbitMQ之类的平台处理。

理解何时何地使用同步模型与异步模型是设计有效的微服务通信的基础性决策。

作者:Morgan Yong
链接:https://zhuanlan.zhihu.com/p/302599305
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

同步通信与HTTP协议紧密相关,SpringCloud采用HTTP协议进行同步通信。本文介绍了SpringCloud关于同步通信的重要组件和软件。

0.3 思考

  1. 如何解决微服务间的服务通信问题?

    • TTP REST方式:使用HTTP协议进行数据传输,JSON格式(SpringCloud使用HTTP协议进行数据传输

    • RPC方式:远程过程调用,二进制

  2. 为什么大多数服务间通信组件使用HTTP协议而不是RPC?

    OSI:物理层 -> 数据链路层 -> 网络层 -> 传输层(RPC) -> 会话层 -> 表示层 -> 应用层(HTTP)

    RPC性能比HTTP好,但是RPC要求两个服务必须使用同一种编程语言,如Dubbo(高性能RPC远程接口调用框架)就要求调用和被调用服务都是用Java编写,这使得其他语言都不能使用该框架,局限性非常大。

    HTTP: 应用层协议,使用HTTP Rest方式,使用JSON作为传输数据的格式进行通信;高度解耦,效率低

    RPC: 传输层协议,直接使用对象二进制方式传递数据,耦合度高,效率高。代表:Dubbo

  3. 如何在Java代码中发起一个HTTP请求?(来进行微服务间通信)

    • Spring框架提供了一个HttpClient对象RestTemplate(不是SpringCloud的一个组件,就是一个对象而已,相当于Java中的浏览器),发起一个http请求(让它模拟浏览器,可以发起GET, POST…)
  4. 实现服务间通信(案例)

    服务注册中心:Consul

    客户端:用户服务(USERS),订单服务(ORDERS)

  5. 服务间通信需要解决的问题

    1. 使用哪种方式通信? – HTTP(SpringCloud); RPC(Dubbo)
    2. 服务调用的负载均衡? – Ribbon(Netflix)…

1. RestTemplate

1.0 简介

  • RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。

  • RestTemplate是Spring用于同步client端的核心类,简化了与http服务的通信,并满足RestFul原则,程序代码可以给它提供URL,并提取结果。默认情况下,RestTemplate默认依赖jdk的HTTP连接工具。当然你也可以 通过setRequestFactory属性切换到不同的HTTP源,比如Apache HttpComponents、Netty和OkHttp。

  • RestTemplate能大幅简化了提交表单数据的难度,并且附带了自动转换JSON数据的功能

SpringCloud微服务间通信(同步通信HTTP)_第1张图片

关于RestTemplate的详情这里不介绍,只是作为一个工具在这里使用,为了突出该工具在微服务间通信时的不足,进而介绍其他组件。

1.1 案例

实现用户服务和订单服务的服务间通信

这里以用户服务为例:

  1. 创建项目springcloud_04_users

  2. pom.xml引入依赖:

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-consul-discoveryartifactId>
        dependency>
    
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
    dependencies>
    
  3. application.properties配置

    用户服务

    server.port=8888	
    spring.application.name=USERS
    

    订单服务:

    server.port=9999	
    spring.application.name=ORDERS
    
  4. 开发入口类

    @SpringBootApplication
    @EnableDiscoveryClient	// 
    public class UsersApplication {
        public static void main(String[] args) {
            SpringApplication.run(UsersApplication.class, args);
        }
    }
    

    Spring Cloud Edgware开始,@EnableDiscoveryClient@EnableEurekaClient 可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。

  5. 登陆consul UI界面查看服务是否注册成功

    SpringCloud微服务间通信(同步通信HTTP)_第2张图片
  6. 简单为两个服务个字添加一个Controller,实现一个demo:

    此时两个服务想要通信(获取对方数据),必须通过URLhttp://localhost:8888/user或者http://localhost:9999/orderf,发起HTTP请求。要实现这一点,上面已经提到,Spring为我们封装了一个HttpClient对象RestTemplate,这个对象有HTTP的GET, POST…方法,具体用法:

    OrderController

    @RestController
    public class OrderController {
    
        private static final Logger log = LoggerFactory.getLogger(OrderController.class);
    
        @GetMapping("order")
        public String demo() {
            log.info("Oder demo running ...");
            return "order demo OK!";
        }
    
    }
    

    UserController

    @RestController
    public class UserController {
    
        private static final Logger log = LoggerFactory.getLogger(UserController.class);
    
        @GetMapping("user")
        public String invokeDemo() {
            log.info("User demo running ...");
            RestTemplate restTemplate = new RestTemplate();
            String orderResult = restTemplate.getForObject("http://localhost:9999/order", String.class);
            log.info("订单服务调用成功:{}", orderResult);
            return "调用Order服务成功,结果为: " + orderResult;
        }
    
    }
    

1.2 问题

RestTemplate restTemplate = new RestTemplate();
        String orderResult = restTemplate.getForObject("http://localhost:9999/order", String.class);

现有的直接使用RestTemplate的方式实现服务间通信存在什么问题?

  1. 没有把服务注册中心利用起来
  2. 调用服务的主机地址和端口直接写死在url中,无法实现集群的负载均衡
  3. 被调用服务的请求路径写死在代码中,倘若路径发生变化不利于后期维护
  4. 不能自动转换响应结果为对应的对象(可由Feign/OpenFeign解决)

解决RestTemplate的负载均衡问题?

a. 自定义负载均衡的策略(最简单的策略,随机

b. 使用SpringCloud提供组件Ribbon来解决负载均衡调用(推荐)

2. Ribbon

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端(而非服务端)负载均衡的服务调用。

注意:Ribbon是一个用于实现负载均衡的客户端组件,它最终还是得配合RestTemplate来发送HTTP请求,Ribbon只负责负载均衡!

RestTemplate:发送HTTP请求进行服务调用

Ribbon:实现服务调用的负载均衡

2.1 流程与原理

SpringCloud微服务间通信(同步通信HTTP)_第3张图片
  1. “用户服务”需要调用“订单服务”,告诉Ribbon需要调用的服务的服务名,即“ORDERS”

  2. Ribbon拿到服务名后去服务注册中心找对应的服务(主机)列表,拉取,即 ORDERS 9999 9990

  3. 然后这里就体现了Ribbon的负载均衡,默认采用轮询(Round Robin)策略来选择需要访问调用的主机,即第一次调用9999,第二次9990,第三次9999…

  4. 此时主机的ip和端口已由Ribbon确认,只需RestTemplate发送HTTP请求,拿到结果。

面试题:

Ribbon是服务端的负载均衡还是客户端的负载均衡?

客户端。Ribbon从服务注册中心拉取到主机列表后,会在本地进行一个缓存(这也是为什么采用轮询策略的原因)。

如果缓存的主机宕机,怎么办?

  • 如果缓存还没有更新,服务使用已经宕机的机器的ip进行调用,那么本次调用也会失败,属于服务调用失败。但是服务注册中心会定时向服务发送心跳包进行健康状况监测,如果发现服务不可用后会从主机列表删除该服务。然后服务注册中心会向Ribbon发送一个更新缓存的请求,于是Ribbon就会把对应宕机的主机信息从本地缓存剔除(更新缓存)。
  • 这个现象在日常使用中也很常见,比如某个时刻访问某个网站显示不出,过一会儿再刷新一下又好了,说明缓存更新了。

2.2 依赖

引入Ribbon依赖:


<dependency>
  <groupId>org.springframework.cloudgroupId>
  <artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>

注意:在consul client的依赖中已经存在ribbon相关依赖,无需再次引入

SpringCloud微服务间通信(同步通信HTTP)_第4张图片

2.3 实现负载均衡

Ribbon组件实现负载均衡提供了三个:

  1. DiscoveryClient
  2. LoadBalancerClient
  3. @LoadBalance

因为引入了Ribbon的依赖,DiscoveryClient与LoadBalancerClient已经自动在工厂中被创建了对象,所以需要使用可以直接注入。

1. DiscoveryClient

服务发现客户端对象,根据服务id(服务名)去服务注册中心获取对应服务的服务列表到本地(返回所有的可用服务方)。

@Autowired
private DiscoveryClient discoveryClient;

//获取服务列表
List<ServiceInstance> products = discoveryClient.getInstances("服务ID");
for (ServiceInstance product : products) {
  log.info("服务主机:[{}]",product.getHost());
  log.info("服务端口:[{}]",product.getPort());
  log.info("服务地址:[{}]",product.getUri());
  log.info("====================================");

缺点:没有负载均衡,需要自行实现

2. LoadBalancerClient

负载均衡客户端对象,根据服务id去服务注册中心获取对应服务的服务列表,根据默认负载均衡策略(轮询)选择列表中的一台机器进行返回。

例如:LoadBalancerClient从提供的服务中获取某一个实例(默认策略为轮询),比如订单服务需要访问商品服务,商品服务有3个节点,LoadBalancerClient会通过choose(),方法获取到3个节点中的一个服务,拿到服务的信息之后取出服务ip信息,就可以得到完成的想要访问的ip地址和接口,最后通过RestTempate访问商品服务。

@Autowired
private LoadBalancerClient loadBalancerClient;

//根据负载均衡策略选取某一个服务调用
ServiceInstance product = loadBalancerClient.choose("服务ID");
log.info("服务主机:[{}]",product.getHost());
log.info("服务端口:[{}]",product.getPort());
log.info("服务地址:[{}]",product.getUri());

缺点:每次使用时需要根据服务id获取一个负载均衡机器,之后再通过RestTemplate调用服务

3. @LoadBalance

负载均衡客户端注解。

修饰:方法

作用:使当前方法、对象具有Ribbon负载均衡功能

使用:在Sping中所有实例都由Spring工厂来管理,所以对象的创建也最好交由Spring工厂里创建维护。而我们每次new RestTemplate()显然不符合这个理念。而想要实现这一过程就需要写Springboot的配置类(在Spring框架中即xml配置文件,因为springboot摒弃了繁琐的xml配置文件,所以改为配置类),用于工厂创建对象,使用时只需要注入即可。

com/zzw/config/BeansConfig.java

@Configuration
public class BeansConfig {

    // 在工厂中创建RestTemplate对象
    @Bean
    @LoadBalanced	// 让RestTemplate具有负载均衡特性
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

controller

url中http://需要保留,之后就可以只写服务id和请求的url,整个url会被自动解析

//3.调用
String forObject = restTemplate.getForObject("http://服务ID/hello/hello?name=" + name, String.class);

2.4 问题

路径依旧写死在代码中,不利于维护

String forObject = restTemplate.getForObject("http://ORDERS/order", String.class);

2.5 组件细节

实现负载均衡原理

根据调用服务id(如"ORDERS")去服务注册中心获取对应服务列表,并将服务列表拉取至本地进行缓存。然后在本地通过默认的轮询策略在现有的列表中选择一个可用的节点提供服务。

注意:Ribbon实现的是客户端的负载均衡

负载均衡策略

通过源码得知:Ribbon的负载均衡的底层父接口IRule,提供了很多的负载均衡策略

- RoundRobinRule         		`轮训策略	按顺序循环选择 Server(默认)
- RandomRule             		`随机策略	随机选择 Server
- AvailabilityFilteringRule `可用过滤策略
 	`会先过滤由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务,然后对剩余的服务列表按照轮询策略进行访问

- WeightedResponseTimeRule  `响应时间加权策略   
	`根据平均响应的时间计算所有服务的权重,响应时间越快服务权重越大被选中的概率越高,刚启动时如果统计信息不足,则使用		
		RoundRobinRule策略,等统计信息足够会切换到

- RetryRule                 `重试策略          
	`先按照RoundRobinRule的策略获取服务,如果获取失败则在制定时间内进行重试,获取可用的服务。
	
- BestAviableRule           `最低并发策略     
	`会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 

SpringCloud微服务间通信(同步通信HTTP)_第5张图片

SpringCloud微服务间通信(同步通信HTTP)_第6张图片

设置策略

# 在USERS服务中进行配置,即配置对ODERS服务调用时的负载均衡策略,注意需要写全类名
ORDERS.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

2.6 停止维护

Ribbon官方停止维护说明:https://github.com/Netflix/ribbon

SpringCloud微服务间通信(同步通信HTTP)_第7张图片

ribbon-core, ribbon-eureka, ribbon-loadbalancer依然在大规模生产实践中部署,意味着日后需要实现服务间通信的负载均衡依然可以使用ribbon组件

3. OpenFeign

3.0 Ribbon回顾

RestTemplate + Ribbon解决方案的缺点:

  • 路径写死,不灵活,不利于维护
  • 不能自动转换响应结果为对应的对象
  • 必须集成Ribbon实现负载均衡
  • Ribbon停止维护(核心组件依旧在大规模生产实践中

3.1 OpenFeign简介

Feign

Feign旨在使得Java Http客户端变得更容易。

SpringCloud微服务间通信(同步通信HTTP)_第8张图片

Feign集成了Ribbon、RestTemplate实现了负载均衡的执行Http调用,只不过对原有的方式(Ribbon+RestTemplate)进行了封装,开发者不必手动使用RestTemplate调服务,而是定义一个接口,在这个接口中标注一个注解即可完成服务调用,这样更加符合面向接口编程的宗旨,简化了开发。(不支持SpringMVC的注解,有一套自己的注解)

SpringCloud微服务间通信(同步通信HTTP)_第9张图片

springcloud F 及F版本以上 springboot 2.0 以上基本上使用openfeign,openfeign 如果从框架结构上看就是2019年feign停更后出现版本,也可以说大多数新项目都用openfeign ,2018年以前的项目在使用feign。

OpenFeign

前面介绍过停止迭代的Feign,简单点来说:OpenFeign是springcloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping, @ResponseBody, @RequestBody等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性(可以使用springmvc的注解),可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器(JSON格式可以直接转为Java对象,无需手动操作)。Feign默认集成了Ribbon,默认实现了负载均衡的效果并且springcloud为feign添加了springmvc注解的支持;

OpenFeign是对Feign的封装,使用和特性几乎一样。说OpenFeign是伪HTTP客户端是因为,他底层使用的还是RestTemplate来发送请求。

3.2 使用

  1. 构建两个子项目springcloud_06_categoryspringcloud_07_product

  2. 引入依赖

    注意:服务调用方法引入依赖OpenFeign依赖!比如:category服务需要调用product服务,那么category服务引入openfeign的依赖;反之亦然。

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-consul-discoveryartifactId>
        dependency>
    
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
    
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>
    dependencies>
    
  3. 配置文件

    类别服务

    server.port=8081
    spring.application.name=CATEGORY
    
    # consul server 服务注册地址
    spring.cloud.consul.host=localhost
    spring.cloud.consul.port=8500
    # 指定当前注册的服务的服务名,默认引用:spring.application.name
    spring.cloud.consul.discovery.service-name=${spring.application.name}
    

    商品服务

    server.port=8091
    spring.application.name=PRODUCT
    
    # consul server 服务注册地址
    spring.cloud.consul.host=localhost
    spring.cloud.consul.port=8500
    # 指定当前注册的服务的服务名,默认引用:spring.application.name
    spring.cloud.consul.discovery.service-name=${spring.application.name}
    
  4. 入口类

    调用方启用@EnableFeignClients

    @SpringBootApplication
    //@EnableDiscoveryClient  // 可不写
    @EnableFeignClients
    public class CategoryApplication {
        public static void main(String[] args) {
            SpringApplication.run(CategoryApplication.class, args);
        }
    }
    

    服务方无需启用@EnableFeignClients

    @SpringBootApplication
    //@EnableDiscoveryClient  // 可不写
    public class ProductApplication {
        public static void main(String[] args) {
            SpringApplication.run(ProductApplication.class, args);
        }
    }
    
  5. 创建一个客户端调用接口

    在服务调用方创建一个本地包,如/feignclients,在该包下创建调用接口ProductClient

    保证定义的方法的返回值和路径和调用的服务方一致即可,方法名无所谓。如此,日后如果路径变化或返回类型变化只需要在接口中修改

    // 调用商品服务的接口
    @FeignClient("PRODUCT")  // 调用服务的服务ID
    public interface ProductClient {
        
        @GetMapping("/product")
        public String product();
    }
    
  6. 在服务调用处注入我们创建的FeignClient接口(在单体应用中,都是直接将不同service注入到当前controller)

    @RestController
    public class CategoryController {
    
        private static final Logger log = LoggerFactory.getLogger(CategoryController.class);
    
        @Autowired
        private ProductClient productClient;
    
        @GetMapping("/category")
        public String category() {
            log.info("进入类别服务...");
            String product = productClient.product();
            return "类别服务状态:OK!,获取商品服务:" + product;
        }
    }
    
  7. 运行两个服务,查看调用结果

    商品服务

    SpringCloud微服务间通信(同步通信HTTP)_第10张图片

    类别服务

    SpringCloud微服务间通信(同步通信HTTP)_第11张图片
  8. 测试负载均衡

    启动两个商品服务:8091和8092;使用类别服务调用多次,发现负载均衡已实现。

3.3 小结

服务间通信手段:

  • HTTP协议
    • RestTemplate(最原生实现通信,相当于HTTP的Java浏览器) + Ribbon(负载均衡)
    • Feign/OpenFeign(集成了前RestTemplate+Ribbon,使用更加简单,面向接口编程,推荐)
  • RPC…

3.4 参数传递

参数传递方式:

  1. 传递零散类型参数,如(?id=)…
  2. 传递对象类型参数
  3. 数组或集合类型参数

1. 零散类型(GET)

注意:OpenFeign是伪Http客户端,在OpenFeign中传参时,因为有两种“零散类型参数传递”方式,所以通过传统写法定义的接口,在传递参数时,当参数数量多于2个时,OpenFeign不知道要用哪种方式组织参数(queryString传参还是路径传参:一种有“?“;一种有“/”),所以会报错:Caused by: java.lang.IllegalStateException: Method has too many Body parameters;当只有一个参数时,会默认通过queryString方式传参

a. QueryString传参

?name=zhangsan这种方式,需要使用@RequestParam给方法的参数注解

商品服务:ProductController

@GetMapping("/testString")
public String testString(String name, Integer age) {
    log.info("Name:{}, Age{}", name, age);
    return "Product Test() OK! port: " + port;
}

类别服务:ProductClient(调用商品服务的接口)

注意@RequestParam属于SpringMVC的注解,在普通SpringMVC项目中可以不写value="xx",但是OpenFeign非常严格,必须写。所以,服务消费方和服务提供方的参数名一定要保持一致。

@GetMapping("/testString")
String testString(@RequestParam("name") String name, @RequestParam("age") Integer age);

b. URL路径传参

xxxURL/zhangsan/13这种方式,需要使用@PathVariable给方法的参数注解

商品服务:ProductController

@GetMapping("/testPath/{name}/{id}")
public String testPath(@PathVariable("name") String name,@PathVariable("id") Integer id) {
    log.info("Name: {}, ID: {}", name, id);
    return "Product testString() OK! port: " + port;
}

类别服务:ProductClient(调用商品服务的接口)

@GetMapping("/testPath/{name}/{id}")
String testPath(@PathVariable("name") String name, @PathVariable("id") Integer id);

2. 对象类型(POST)

a. form表单

使用这种方式,底层在传递数据的时候使用的是form表单形式…

b. Application/JSON(常用/推荐)

使用这种方式,底层在传递数据的时候使用的是JSON格式,需要给参数加上@RequestBody注解,用于解析JSON到Java类

商品服务:ProductController

@PostMapping("/test2")
public String test2(@RequestBody Product product) {
    log.info("Product: " + product);
    return "Product test2() OK! port: " + port;
}

类别服务:ProductClient(调用商品服务的接口)

@PostMapping("/test2")
String test2(@RequestBody Product product);

3. 数组和集合类型

a. 数组

只能使用QueryString的方式传递数组,前端以这样的形式传递:/url/?ids=21&ids=22&ids=23,这样多个ids就会被存入ids数组。

商品服务:ProductController

@GetMapping("/test3")
public String test3(@RequestParam("ids") String[] ids) {
    for (String id : ids) {
      log.info("id = " + id);
    }
    return "Product test3() OK! port: " + port;
}

类别服务:ProductClient(调用商品服务的接口)

@GetMapping("/test3")
String test3(@RequestParam("ids") String[] ids);

b. 集合(了解)

集合在实际业务场景中使用的不多。

SpringMVC框架不能直接接收集合类型参数,如List、Set或Map;如果想要接收集合类型参数,必须将集合放入对象中,使用对象接收的方式接收才行。

  • VO(Value Object): 前端向后端传递数据的对象,称为值对象
  • DTO(Data Transfer Object): 后端向前端传输数据的对象;后端向页面(前端)传递的数据/对象应该是一个对象而非多个对象,所以应该将这些对象都封装入一个对象中。

VO: CollectionVO

// 用来接收集合类型的参数
public class CollectionVO {
    private List<String> ids;

    public List<String> getIds() {
        return ids;
    }

    public void setIds(List<String> ids) {
        this.ids = ids;
    }
}

商品服务:ProductController

使用这种方式,商品服务就能接收/test4?ids=111&ids=222&ids=333这种格式的GET请求,他会自动把这些ids封装到CollectionVO中的ids中去。

@GetMapping("/test4")
public String test4(CollectionVO ids) {
    for (String id : ids.getIds()) {
      	log.info("id = " + id);
    }
    return "Product test4() OK! port: " + port;
}

类别服务:ProductClient(调用商品服务的接口)

想要将参数组织成上述的方式(把请求路径拼成QueryString的形式),使用@RequestParam即可

@GetMapping("/test4")
String test4(@RequestParam("ids") String[] ids);	// 参数名‘ids’其实对应的是CollectionVO中的‘ids’

3.5 响应处理

1. 返回对象

使用OpenFeign调用服务,并返回对象。例如,类别服务调用商品服务,查询某一个类别的某一种商品,需要商品服务返回一个商品对象。两个服务中都需要有同一个entity,或者共同依赖一个公共项目,其中包含公共类。

商品服务:ProductController

@GetMapping("/product/{id}")
public Product query(@PathVariable("id") Integer id) {
    log.info("查询id = {} 的商品", id);
    Product product = new Product(id, "MacBook", new Date());
    return product;
}

类别服务:ProductClient(调用商品服务的接口)

@GetMapping("/product/{id}")
Product query(@PathVariable("id") Integer id);

类别服务:CategoryController

@GetMapping("/category/{id}")
public Product queryProduct(@PathVariable("id") Integer id) {
    log.info("进入类别服务...");
    log.info("查询商品ID{}...", id);
    Product product = productClient.query(id);
    return product;
}

访问http://localhost:8081/category/123,结果:

{
  "id": 123,
  "name": "MacBook",
  "bir": "2022-02-28T19:30:53.594+0000"
}

商品服务:log

2022-02-28 19:30:53.593  INFO 44370 --- [nio-8091-exec-4] com.zzw.controller.ProductController     : 查询id = 123 的商品

2. 返回集合

商品服务:ProductController

@GetMapping("/products")
public List<Product> queryAll(@RequestParam("categoryID") Integer categoryID) {
    log.info("查询类别id = {} 的商品集合", categoryID);
    List<Product> products = new ArrayList<>();
    products.add(new Product(1, "Iphone", new Date()));
    products.add(new Product(2, "Huawei", new Date()));
    products.add(new Product(3, "Xiaomi", new Date()));
    return products;
}

类别服务:ProductClient(调用商品服务的接口)

@GetMapping("/products")
List<Product> queryAll(@RequestParam("categoryID") Integer categoryID);

类别服务:CategoryController

// /category/product?id=99
@GetMapping("/category/product")
public List<Product> queryAllProduct(@RequestParam("id") Integer id) {
    log.info("进入类别服务...");
    log.info("查询类别id{}的商品...", id);
    List<Product> products = productClient.queryAll(id);
    return products;
}

访问http://localhost:8081/category/123,结果:

[
  {
    "id": 1,
    "name": "Iphone",
    "bir": "2022-02-28T19:51:07.248+0000"
  },
  {
    "id": 2,
    "name": "Huawei",
    "bir": "2022-02-28T19:51:07.248+0000"
  },
  {
    "id": 3,
    "name": "Xiaomi",
    "bir": "2022-02-28T19:51:07.248+0000"
  }
]

3. 返回分页

Map

假设有一个业务场景:需要调用服务进行查询,返回分页结果而非简单的集合类型。以商品服务和类别服务为例:商品服务需要根据当前页码,每页的个数,以及类别ID查询商品。(返回时有两种方式:1. 自定义DTO 2. Map

商品服务:ProductController

@GetMapping("/productList")
public Map<String, Object> queryByCategoryIdAndPage(Integer page, Integer rows, Integer categoryId) {
    log.info("当前页: {}, 每页显示数:{}, 类别ID:{}", page, rows, categoryId);
    // 以下为SQL的伪代码,这里省略,直接在控制层模拟数据
    // 根据类别id,分页查询符合当前页的集合数据:
    // List select * from t_product where categoryId=? limit ?(page-1)*rows, ?(rows)
    // 根据类别id,查询总条数:
    // totalCount   select count(id) from t_product where categoryId=?
    Map<String, Object> map = new HashMap<>();
    List<Product> products = new ArrayList<>();
    products.add(new Product(1, "Iphone", new Date()));
    products.add(new Product(2, "Huawei", new Date()));
    products.add(new Product(3, "Xiaomi", new Date()));
    int total = 10000;
    map.put("products", products);
    map.put("total", total);
    return map;
}

因为需要返回:1. 查询的结果List;2. 查询的总条数,所以自然而然想到使用Map,分别put进去两个结果。

注意:Map好用并且万能,但是真实工作里面传参都是传对象,不会用Map;Map使用的时候会有诸多问题,究其原因还是Map里存放的是,从服务方传回消费方,接收到的是JSON字符串,此时强转为自己想要的类型(如Product, List)会导致很多ClassCastException,因为范型在运行的时候就被擦除了(如Product当成Object,在转为JSON后,其自身的数据就丢失了),使用括号进行强转(List)(map.get("results"))是没有用的。说白了Feign可以做底层的简单的序列化,而遇到复杂的则不行。

关于强转:

  • 只有父类对象本身就是用子类new出来的时候, 才可以在将来被强制转换为子类对象.

但是考虑到使用Map的缺陷,可以考虑:

  1. 自定义DTO
  2. 使用JSON类库(如FastJSON、Jackson

FastJSON

Fastjson是阿里巴巴的开源JSON解析库,基于Java语言,支持JSON格式的字符串与JavaBean之间的相互转换。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到了极致。

由于接口简单易用,已经被广泛使用在缓存序列化,协议交互,Web输出等各种应用场景中。

引入依赖(类别服务)


<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>fastjsonartifactId>
    <version>1.2.76version>
dependency>

在商品服务中,ProductController本身就是一个@RestController,所有的返回都会被解析成String字符串,所以我们在类别服务的ProductClient可以直接写上返回类型是String

// 调用商品服务,根据类别id查询,分页查询商品信息,以及总条数
@GetMapping("/productList")
String queryByCategoryIdAndPage(@RequestParam("page") Integer page,@RequestParam("rows") Integer rows,@RequestParam("categoryId") Integer categoryId);

类别服务:CategoryController

@GetMapping("/categoryPage")
public String queryCategoryByPage() {
    log.info("进入类别服务...");
    String result = productClient.queryByCategoryIdAndPage(1, 5, 1);
    System.out.println(result);

    // JSON反序列化
    JSONObject jsonObject = JSONObject.parseObject(result);
    Object total = jsonObject.get("total");
    Object products = jsonObject.get("products");

    // JSON二次反序列化
    List<Product> productList = JSONObject.parseArray(products.toString(), Product.class);
    productList.forEach(product -> {
      	log.info("product {} ", product);
    });
    return result;
}

测试:访问http://localhost:8081/categoryPage

注意此时返回的是String,而不是JSON

bA3tRH

类别服务的日志/控制台:

IvulpU

3.6 超时处理

默认超时时间

OpenFeign的服务调用的默认超时:

使用OpenFeign组件在进行服务间通信时默认要求被调用的服务在1s内给予响应,一旦超过1s,将直接报错。

简单模拟:

商品服务:ProductController

@GetMapping("/product")
public String product() throws InterruptedException {
    log.info("进入商品服务...");
    Thread.sleep(1000);
    return "商品服务状态:OK!当前提供服务端口:" + port;
}

类别服务:CategoryController

@GetMapping("/category")
public String category() {
    log.info("进入类别服务...");
    String product = productClient.product();
    return "类别服务状态:OK!,获取商品服务:" + product;
}

访问http://localhost:8081/category,会报错

Read timed out executing GET http://PRODUCT/product

修改超时时间

假设类别服务(CATEGORY)需要调用商品服务(PRODUCT),需要在类别服务(CATEGORY)中设置超时时长(connectTimeout/connect-timeout两种写法都可以,SpringCloud官网使用驼峰;IDEA自动补全的是’-'横杠)

# Feign超时时间设置 (PRODUCT服务)
# connect-timeout/connectTimeout: 连接超时时长,连接时超过这个时间即调用失败
feign.client.config.PRODUCT.connectTimeout=5000
# read-timeout/readTimeout: 等待超时时长,等待服务调用时超过这个时间即调用失败
feign.client.config.PRODUCT.readTimeout=5000
#配置所有服务连接超时
feign.client.config.default.connectTimeout=5000  		
#配置所有服务等待超时
feign.client.config.default.readTimeout=5000			

3.7 日志

OpenFeign为了更好更方便地在开发过程中调试数据传递、响应处理,添加了日志功能(包括请求路径、参数、响应)。

# 0.说明
- 往往在服务调用时我们需要详细展示feign的日志,默认feign在调用是并不是最详细日志输出,因此在调试程序时应该开启feign的详细日志展示。feign对日志的处理非常灵活可为每个feign客户端指定日志记录策略,每个客户端都会创建一个logger默认情况下logger的名称是feign的全限定名需要注意的是,feign日志的打印只会DEBUG级别做出响应。
- 我们可以为feign客户端配置各自的logger.lever对象,告诉feign记录那些日志logger.lever有以下的几种值:
	`NONE  不记录任何日志
	`BASIC 仅仅记录请求方法,url,响应状态代码及执行时间
	`HEADERS 记录Basic级别的基础上,记录请求和响应的header
	`FULL 记录请求和响应的header,body和元数据

开启日志:

#开启指定服务日志展示
feign.client.config.PRODUCT.loggerLevel=full  
#全局开启服务日志展示
#feign.client.config.default.loggerLevel=full

#指定feign调用客户端对象所在包,必须是debug级别
logging.level.com.zzw.feignclients=debug

访问:http://localhost:8081/categoryPage

SpringCloud微服务间通信(同步通信HTTP)_第12张图片

你可能感兴趣的:(分布式系统,SpringCloud,计算机网络,微服务,http,microservices,spring,cloud)