服务注册中心之Eureka、Consul

微服务

  • 微服务
定义:基于单个应用围绕业务进行拆分,拆分出来的每一个服务独立项目,独立开发、独立部署,独立运行自己计算机进程里面,基于分布式服务管理
  • spring cloud
定义:用来帮助开发人员快递的构建一套分布式应用,微服务工具集,提供一系列微服务开发组件【服务注册与发现、负载均衡、路由,同一配置管理】

服务注册中心组件

定义:

服务注册中心在整个微服务架构单独抽取一个服务,这个服务不完成项目中的任务业务功能, 仅仅用来在微服务中记录微服务集熤对整个系统微服务进行健康状态检查,以及服务元数据信息存储

常用注册中心组件:

yiruika eureka(netflix)  
rukeipo zookeeper(java)  
kangsou consul(go)  
laikous nacos(java阿里巴巴)  

(一) Eureka 服务注册中心组件开发

1、eureka server 开发

I、两个组件

eureka server  服务注册中心  
eureka client

II、开发服务注册中心

  • 创建springboot项目
  • 引入eureka server依赖
<!--引入eureka server-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
  • 编写配置文件,指定端口号和服务地址
# eureka server 默认的端口是 8761
server:
  port: 8761
  
# eureka server 服务注册中心地址,暴露服务地址告诉客户端
eureka:
  client:
    service-url: 
      defaultZone: http://localhost:8761/eureka
  • 在入口类加入注解
@EnableEurekaServer 开启当前应用是一个服务注册中心

III、注意:

  • 项目启动成功之后默认在eureka server管理界面出现 UNKNOWN 一个未知应用,这个代表当前应用的服务名称

  • 微服务架构中的服务名称代表服务的唯一标识,至关重要,服务名称必须唯一,使用时必须通过以下配置服务名称

# 指定服务的名称
spring:
  application:
    name: EUEREKASERVE
  • eureka server 启动时报错
com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server

原因:
eureka 含有两个组件: eureka server和eureka client,当项目中引入 eureka server 组件时,这个组件同时将 eureka client 引入到项目中,因此启动时会将自己作为一个服务中心启动,同时也会将自己作为服务客户端进行注册,默认启动时立即注册,注册时服务还没有准备完成,所以出现当前错误
  • 如何关闭 eureka server 自己注册自己

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
    fetch-registry: false # 关闭立即注册
    register-with-eureka: false # 让当前的应用仅仅是服务注册中心

2、eureka client 开发

I、 eureka client 相当于业务拆分出来的一个个微服务

II、开发 eureka client 客户端

  • 创建springboot项目

  • 引入pom依赖

<!--引入 eureka client 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • 编写配置文件
server:
  port: 8989

spring:
  application:
    name: EUREKACLIENT

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka   # 注册中心的地址
  • 在入口类上加上注解
// 当前微服务作为一个eureka server的客户端,进行服务注册
@EnableEurekaClient

3、eureka 自我保护机制 (self preservation mode)

I、什么是自我保护机制

  • 定义:自我保护机制是一种针对网络异常波动的安全保护措施,使用自我保护机制能使eureka集群更加的健壮、稳定的运行
  • 注意:自我保护机制是默认开启的
  • 现象:在自我保护机制模式下,eureka 服务将停止逐出所有示例
  • 作用:当 eureka client 服务不可用的时候,eureka server 会开启自我保护机制,是为了确保灾难性的网络事件不会清除eureka注册表数据,并将其传播到下游的所有客户端
  • 触发:心跳的次数高于预期阈值,默认情况下,eureka server 在一定时间内(默认90s)没有接收到某个微服务实例的心跳,就会移除该实例,但是当网络分区故障发生时,微服务和eureka server之前无法通讯,而微服务本省是运行的,此时不应该移除这个微服务,使用引入了自我保护机制。eureka server在运行期间回去统计心跳失败比列在15分钟之内是否低于85%,如果低于85%,eureka server会将这些实例保护起来,让这些实例不会过期,这种设计的哲学原理就是"宁可信其有,不可信其无"。

II、关闭自我保护机制

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
    fetch-registry: false   # 关闭立即注册
    register-with-eureka: false   # 让当前的应用仅仅是服务注册中心
  server:
    enable-self-preservation: false   # 关闭自我保护
    eviction-interval-timer-in-ms: 3000   # 超时3s自动清除
  instance:
    lease-expiration-duration-in-seconds: 10    # 用来修改eureka server 默认接收心跳的最大时间,默认是90s
    lease-renewal-interval-in-seconds: 5    # 指定客户端多久向eureka server发送一次心跳 默认是30s

4、eureka 集群搭建

I、eureka server 集群

  • 创建3个springboot项目
  • 引入eureka server的依赖
  • 编写配置文件
node1:  server.port=8761
        http://localhost:8762/eureka,http://localhost:8763/eureka
node2:  server.port=8762
        http://localhost:8761/eureka,http://localhost:8763/eureka
node3:  server.port=8763
        http://localhost:8761/eureka,http://localhost:8762/eureka
  • 在3个项目的入口类中加入注解
    @EnableEurekaServer

II、eureka client 集群搭建

  • 创建多个springboot项目
  • 引入eureka client的依赖
  • 编写配置文件
同一台服务器上,端口号需要不同
node1:  server.port=8989
        server.application.name=EUREKACLIENT
        http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka
node2:  server.port=9090
        server.application.name=EUREKACLIENT
        http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka

5、总结(spring-cloud-netflix-eureka)

  • 作用:
    微服务架构充当 服务注册中心
  • 两个角色:
    eureka server 服务注册中心、eureka client 微服务
  • 缺点:
    1、eureka组件1.0 比较稳定和活跃、eureka2.0停止更新
    2、每次需要手动通过代码形式开发服务注册中心,新建一个项目作为服务注册中心,并且代码固定

(二)consul 服务注册中心

1、consul 简介

consul 基于Google的go语言进行开发服务注册中心,轻量级
管理微服务中所有服务注册与发现,管理服务元数据信息存储(服务名、地址列表),心跳健康检查

2、安装

  • 官网地址:https://www.consul.io/downloads 根据服务器的系统下载对应的consul
  • 在指定的目录进行压缩、安装目录不建议有中文
  • 在对应的目录执行命令
consul agent -dev     # 开发,单节点
consul agent -server  # 服务,集群,多节点
  • 访问consul的管理化界面
port:8500
http:localhost:8500
  • 管理界面基本介绍
dc1 :数据中心名称(data center)
修改名称需要在启动时输入命令: consul agent -dev -datacenter xxx  

service :微服务的注册列表,默认consul启动注册自己,会出现一个consul服务  

nodes:用来查看consul集群
  • 配置环境变量
Path=/Users/xxx/consul(自己安装consul的路径)  
这样就可以在任意位置输入命令运行consul注册中心

搭建consul客户端

  • 导入maven依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
  • 编写application配置文件
spring:
  application:
    name: CONSULCLIENT
  cloud:
    consul:
      # 服务注册地址
      host: localhost 
      port: 8500
      discovery:
        # 执行注册当前服务的服务名称
        service-name: ${spring.application.name} 
  • 在入口类中加入注解
    @EnableDiscoveryClient // 通用服务注册客户端注解 代表 consul client、nacos client、zk client

  • 直接启动consul client会出现服务不可用

consul server 会检测所有客户端的心跳,但是发送心跳时,client必须给与响应才能正常使用
在现有客户端中我们并没有引入健康检查的依赖,所以导致健康检查始终不通过,导致服务不能使用

解决方案:
1、配置文件中关闭自动健康检查 在生产情况下不建议开启
spring.cloud.consul.discovery.register-health-check: false
2、引入健康检查的依赖
<!--引入健康检查的依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • consul没有自我保护机制,服务不可用时,会自动断开

微服务之间的通讯

I、通讯方式

  • HTTP Rest 方式:使用http协议进行传输,客户端向服务端发起请求的方式 效率低,处于第七层,springboot使用http协议
  • RPC 方式:远程过程调用,基于语言API定义

II、七层osi
效率(高到低):物理层 数据链路层 网络层 传输层(RPC) 会话层 表示层 应用层(HTTP)

III、java代码发起http请求其他微服务的接口

  • spring框架提供一个httpClient对象:RestTemplate,可以发起一个请求,将restTemplate想成一个客户端

IV、实现服务之间的通讯

  • 开发连个springboot服务 : users 用户服务 orders 订单服务

  • 两个服务引入 consul client和将康检查的依赖

<dependencys>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencys>
  • 配置文件中配置consul
server.port=9999

spring.application.name=ORDERS

# consul server
spring.cloud.consul.host=localhost
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.service-name=${spring.application.name}
  • 在入口类中加入注解,开启服务注册
@SpringBootApplication
@EnableDiscoveryClient
public class UsersApplication {
    public static void main(String[] args) {
        SpringApplication.run(UsersApplication.class,args);
    }
}
  • 在两个服务中开发一个接口
// User 服务接口
package com.cai.controller;

import com.oracle.tools.packager.Log;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("user")
public class UserController {

    @GetMapping
    public String invokeDemo(){

        Log.info("user demo ...");

        // 调用 order 的demo服务 :  url=http:localhost:9999/order  get方式  返回值string类型
        RestTemplate restTemplate = new RestTemplate(); // spring 提供
        String orderResult= restTemplate.getForObject("http://localhost:9999/order", String.class);

        Log.info("调用订单服务成功:"+orderResult);

        return "调用订单服务成功,返回结果:"+orderResult;
    }
}
// order 服务接口
package com.cai.controller;

        import com.oracle.tools.packager.Log;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("order")
public class OrderController {

  @GetMapping
  public String demo(){
    Log.info("order demo ...");

    return "order demo ok";
  }
}
  • 使用 RestTemplate 进行服务之间的通讯存在什么问题
1、之后为了服务的高可用,会集群部署,当订单服务不在是一个节点而是多个节点
那请求的链接就限定死了(调用的主机和端口写死),其他的节点就是用不了了
故:当服务是集群部署时,无法满足  
2、调用服务的路径已经写死,当调用的服务路径发生改变时,不利用维护
  • 解决RestTemplate负载均衡问题 自定义负载均衡策略
// 随机访问,自定义负载均衡策略
public String randomHost(){
    ArrayList<String> hosts = new ArrayList<>();
    hosts.add("localhost:9999");
    hosts.add("localhost:9998");
    hosts.add("localhost:9997");
    int i = new Random().nextInt(hosts.size());
    return hosts.get(i);
}
String orderResult= restTemplate.getForObject("http://"+randomHost()+"/order", String.class);
缺点:
1、无法实现健康检查 
2、负载均衡策略过于单一(随机)

V 使用RestTemplate 提供组件 ribbon 解决负载均衡调用(推荐)

  • ribbon (spring cloud netflix ribbon):负载均衡客户端组件、实现请求调用时负载均衡
ribbon 在客户端调用,会将服务注册中心的服务地址拉取到本地进行缓存,默认使用轮循的方式。
当服务注册中心出现不可用的服务或者出现新的可用服务时,会向ribbon通知,ribbon会重新拉取服务地址
  • ribbon + RestTemplate 的使用
在导入服务注册与发现的maven依赖时,依赖中还包含了 discovery 和 loadbalancer 
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
  • DiscoveryClient
定义:
服务发现客户端对象,根据服务ID去服务注册中心获取对应服务列表到本地中
缺点:
没有负载均衡,需要自己实现负载均衡
package com.cai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("user")
public class UserController {

  private static final Logger log = LoggerFactory.getLogger(UserController.class);

  @Autowired
  private DiscoveryClient discoveryClient;

  @GetMapping
  public String invokeDemo() {

    log.info("user demo ...");
    
    List<ServiceInstance> instances = discoveryClient.getInstances("ORDERS");
    log.info("服务主机{}.服务端口{},服务地址{}",instances.get(0).getHost(),instances.get(0).getPort(),instances.get(0).getUri());
    String result = new RestTemplate().getForObject(instances.get(0).getUri() + "/order", String.class);
    return "返回结果:"+result;
  }
}
  • LoadBalancerClient
定义:
负载均衡客户端对象,根据服务ID去服务中心获取对应服务列表,根据默认负载均衡策略选择列表中一台机器进行返回
缺点:
使用时需要每次先根据服务ID获取一个负载均衡机器之后再通过RestTemplate调用服务
package com.cai.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("user")
public class UserController {

  private static final Logger log = LoggerFactory.getLogger(UserController.class);

  @Autowired
  private LoadBalancerClient loadBalancerClient;
  
  @GetMapping
  public String invokeDemo() {

    log.info("user demo ...");

    ServiceInstance orders = loadBalancerClient.choose("ORDERS");
    log.info("服务主机{}.服务端口{},服务地址{}",orders.getHost(),orders.getPort(),orders.getUri());
    String result = new RestTemplate().getForObject(orders.getUri()+"/order", String.class);
    return "返回结果:"+result;
  }
}
  • @LoadBalancer
定义:
修饰范围在方法上,作用是让当前方法当前对象具有负载均衡

将RestTemplate交给工厂

package com.cai.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration  // 代表这是一个springboot配置类
public class BeansConfig {

  // 在工厂中创建
  @Bean
  public RestTemplate restTemplate(){
    return new RestTemplate();
  }
}
package com.cai.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

@RestController
@RequestMapping("user")
public class UserController {

    private static final Logger log = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping
    public String invokeDemo() {

        log.info("user demo ...");
        // 4、使用 @LoadBalancer 注解
        String result = restTemplate.getForObject("https://ORDERS/order", String.class);
        return "返回结果:"+result;
    }

}
  • 使用 RestTemplate + Ribbon 总结
原理:
根据服务ID去服务注册中心中获取对应服务ID的服务列表,并将服务列表拉取到本地进行缓存,然后在本地通过负载均衡的策略(默认是轮循)在现有的服务列表中选择一个可用节点提供服务
ribbon是客户端的负载均衡

缺点:
路径写死在代码中,不利于维护  (restTemplate.getForObject("https://ORDERS/order", String.class);)

支持的负载均衡策略:
1、根据 LoadBalancerClient 的 choose 方法中(loadBalancerClient.choose("ORDERS"))
发现用到的是一个接口类 ServiceInstanceChooser,并没有调转到LoadBalancerClient类中,
说明LoadBalancerClient中没有choose方法做实现,调用的是接口中choose的方法,
我们需要看接口中 choose 方法的实现类默认是 BlockingLoadBalancerClient ,
在 BlockingLoadBalancerClient 类中的 choose 方法,Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
进入 loadBalancer.choose(request) 的方法,Publisher<Response<T>> choose(Request request);
这里的choose 方法有两个实现方法:RandomLoadBalancer(随机) 和 RoundRobinLoadBalancer(轮循)

a:进入 RandomLoadBalancer(随机)的实现方法
int index = ThreadLocalRandom.current().nextInt(instances.size());
ServiceInstance instance = instances.get(index);
原理和我们写的随机负载均衡一样,都是通过在服务列表个数内随机取一个数来获取索引的服务

b:进入 RoundRobinLoadBalancer(轮循)的实现方法
int pos = Math.abs(this.position.incrementAndGet());
ServiceInstance instance = instances.get(pos % instances.size());
原理是取一个正整数,每次加一,然后再除以服务的数量取模,拿到的模数当索引拿到服务
如:
服务的个数为2
第一次pos的值为1, 1/2 ... 1
第二次pos的值为2, 2/2 ... 0
第三次pos的值为3, 3/2 ... 1
就这样依次下去,每次都是不一样的

  • 如何设置 ribbon 的负载均衡策略
# 修改ribbon负载均衡的策略 (server id + ribbon.NFLoadBalancerRuleClassName =负载均衡策略的全限类名)
ORDERS.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

VI OpenFeign

  • 什么是 OpenFeign
简介:
实现服务间通讯的组件,和 RestTemplate 作用一致,也是一个 Rest Client,默认集成了 ribbon ,实现了负载均衡
区别:
RestTemplate:由spring封装的 HttpClient 对象
OpenFeign   :伪 Http Client,相当于代理,可以使服务间的通讯的代码更加简单:(一个接口+注解)、(自动完成数据传递过程中对象转换)
  • 为什么要使用 OpenFeign
使用 RestTemplate 的问题:
1、路径写死
2、不能自动转换响应结果为对应对象
3、必须集成ribbon实现负载均衡

OpenFeign 组件:
解决了 RestTemplate 实现服务之间通讯的所有问题
  • 引入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 使用:无参调用
    服务端
package com.cai.controller;

import com.cai.entity.Product;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);

    @Value("${server.port}")
    private int port;

    @GetMapping("/product")
    public String product() {
        log.info("进入product方法。。。");
        return "product-" + port;
    }
}

客户端

package com.cai.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(value = "PRODUCT") // value=服务ID
public interface ProductClient {

  @GetMapping("/product")
  String product();
}

使用

package com.cai.controller;

import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CategoryController {

    private static  final Logger log= LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;

    @GetMapping("/category")
    public String category(){
        log.info("进入 category 方法。。。");
        String product = productClient.product();
        log.info("product()——》"+product);
        return "返回结果:"+product;
    }
}
  • 使用:有参调用(QueryString 问号传参)
    服务端
package com.cai.controller;

import com.cai.entity.Product;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);

    @Value("${server.port}")
    private int port;

    @GetMapping("/getProduct")
    public String getProduct(String name, Integer age) {
        log.info("进入getProduct(name,age)方法。。。");
        log.info("参数:name:" + name + ",age:" + age);
        return "getProduct-" + "name:" + name + ",age:" + age;
    }
}

客户端

package com.cai.feignclient;

import com.cai.entiy.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(value = "PRODUCT") // value=服务ID
public interface ProductClient {
    
    // QueryString
    @GetMapping("/getProduct")
    String getProduct(@RequestParam("name") String name,@RequestParam("age") Integer age);
}

使用

package com.cai.controller;

import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CategoryController {

    private static  final Logger log= LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;

    @GetMapping("/category")
    public String category(){

        log.info("进入 category 方法。。。");

        String getString = productClient.product();
        String queryString=productClient.getProduct("张三",18);

        log.info("product()——》"+getString);
        log.info("getProduct(name,age)——》"+queryString);
        log.info("getProduct(product)——》"+jsonString);

        return "返回结果:"+getString+";"+queryString;

    }
}
  • 使用:有参调用(对象传参 json格式)
    服务端
package com.cai.controller;

import com.cai.entity.Product;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);

    @Value("${server.port}")
    private int port;
    
    @PostMapping("/getProduct")
    public String getProduct(@RequestBody Product product){
        log.info("进入getProduct(product)方法。。。");
        log.info("参数:"+product.toString());
        return "getProduct-"+"product:"+product.toString();
    }
}

客户端

package com.cai.feignclient;

import com.cai.entiy.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(value = "PRODUCT") // value=服务ID
public interface ProductClient {
    
    // json格式
    @PostMapping("/getProduct")
    String getProduct(@RequestBody Product product);
}

使用

package com.cai.controller;

import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CategoryController {

    private static  final Logger log= LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;

    @GetMapping("/category")
    public String category(){

        log.info("进入 category 方法。。。");

        String getString = productClient.product();
        String queryString=productClient.getProduct("张三",18);

        Product product=new Product("李四",16);
        String jsonString=productClient.getProduct(product);

        log.info("product()——》"+getString);
        log.info("getProduct(name,age)——》"+queryString);
        log.info("getProduct(product)——》"+jsonString);

        return "返回结果:"+getString+";"+queryString+";"+jsonString;
    }
}

  • 使用注意
注意点:
方法中的形参和实参以及请求路径需要和对应的服务的方法完全一样,方法名不限
  • 使用:有参调用(数组 QueryString格式传参)
    服务端
package com.cai.controller;

import com.cai.entity.Product;
import com.cai.entity.vo.CollectionVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import sun.security.util.ArrayUtil;

import java.util.Arrays;

@RestController
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);

    @Value("${server.port}")
    private int port;
    
    @GetMapping("/getProductArray")
    public String getProductArray(String[] ids){
        log.info("进入getProductArray方法。。。");
        log.info("参数:"+ Arrays.toString(ids));
        return Arrays.toString(ids);
    }
}

客户端

package com.cai.feignclient;

import com.cai.entiy.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(value = "PRODUCT") // value=服务ID
public interface ProductClient {
    
    // springMvc 中会将参数名相同的值 ?ids=1&ids=2 组在一起成为数组,所以接收的参数类型是 @RequestParma
    @GetMapping("/getProductArray")
    String getProductArray(@RequestParam("ids") String[] ids);
}

使用

package com.cai.controller;

import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CategoryController {

    private static  final Logger log= LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;

    @GetMapping("/category")
    public String category(){

        log.info("进入 category 方法。。。");

        String getString = productClient.product();
        String queryString=productClient.getProduct("张三",18);

        Product product=new Product("李四",16);
        String jsonString=productClient.getProduct(product);

        String [] ids=new String[]{"1","3","1","5"};
        String getProductArray=productClient.getProductArray(ids);

        log.info("product()——》"+getString);
        log.info("getProduct(name,age)——》"+queryString);
        log.info("getProduct(product)——》"+jsonString);
        log.info("getProductArray(String [] ids)——》"+getProductArray);

        return "返回结果:\t\n"+getString+";\t\n"+queryString+";\t\n"+
                jsonString+";\t\n"+getProductArray;
    }
}
  • 使用:有参调用(集合 QueryString 模式)
    服务端
package com.cai.controller;

import com.cai.entity.Product;
import com.cai.entity.vo.CollectionVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import sun.security.util.ArrayUtil;

import java.util.Arrays;

@RestController
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);

    @Value("${server.port}")
    private int port;

    // 定义一个接口接收集合类型参数
    // springmvc 不能直接接收集合类型参数,如果要接收集合类型的参数必须将集合放入对象中,使用对象的方式来接收才行
    // oo:oriented object 面向对象
    // vo:value object 值对象,用来传输数据的对象(接收,服务端)
    // dto:data transfer object 数据传输对象(传递、客户端)
    @GetMapping("/getProductList")
    public  String getProductList(CollectionVo ids){
        log.info("进入getProduct方法。。。");
        log.info("参数:"+ids.toString());
        return ids.toString();
    }
}

vo

package com.cai.entity.vo;

import java.util.List;

public class CollectionVo {

    private List<String> ids;

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

    @Override
    public String toString() {
        return "CollectionVo{" +
                "ids=" + ids +
                '}';
    }

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

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

客户端

package com.cai.feignclient;

import com.cai.entiy.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

@FeignClient(value = "PRODUCT") // value=服务ID
public interface ProductClient {

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

使用

package com.cai.controller;

import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CategoryController {

    private static  final Logger log= LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;

    @GetMapping("/category")
    public String category(){

        log.info("进入 category 方法。。。");

        String getString = productClient.product();
        String queryString=productClient.getProduct("张三",18);

        Product product=new Product("李四",16);
        String jsonString=productClient.getProduct(product);

        String [] ids=new String[]{"1","3","1","5"};
        String getProductArray=productClient.getProductArray(ids);

        String getProductList=productClient.getProductList(ids);

        log.info("product()——》"+getString);
        log.info("getProduct(name,age)——》"+queryString);
        log.info("getProduct(product)——》"+jsonString);
        log.info("getProductArray(String [] ids)——》"+getProductArray);
        log.info("getProductList(String [] ids)——》"+getProductList);

        return "返回结果:\t\n"+getString+";\t\n"+queryString+";\t\n"+
                jsonString+";\t\n"+getProductArray+";\t\n"+getProductList;
    }
}
  • 使用:返回参数为对象
    服务端
package com.cai.controller;

import com.cai.entity.Product;
import com.cai.entity.vo.CollectionVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import sun.security.util.ArrayUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@RestController
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);

    @Value("${server.port}")
    private int port;

    @GetMapping("/getProduct/{id}")
    public Product getProduct(@PathVariable("id") String id){
        log.info("进入getProduct(id)方法。。。");

        return new Product(1,"王五",15);
    }
}

客户端

package com.cai.feignclient;

import com.cai.entiy.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@FeignClient(value = "PRODUCT") // value=服务ID
public interface ProductClient {

    @GetMapping("/getProduct/{id}")
    Product getProduct(@PathVariable("id") String id);
}

使用

package com.cai.controller;

import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.java2d.pipe.PixelToParallelogramConverter;

import java.util.List;
import java.util.Map;

@RestController
public class CategoryController {

    private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;

    @GetMapping("/object")
    public Product  categoryForObject() {

        log.info("进入categoryForObject 方法。。。");

        Product product = productClient.getProduct("1");

        return productList;
    }
}
  • 使用:分页查询返回集合、总数
    服务端
package com.cai.controller;

import com.cai.entity.Product;
import com.cai.entity.vo.CollectionVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import sun.security.util.ArrayUtil;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@RestController
public class ProductController {

    private static final Logger log = LoggerFactory.getLogger(ProductController.class);

    @Value("${server.port}")
    private int port;
    
    // 分页查询
    @GetMapping("/productList")
    public Map<String,Object> getProductList(int page,int size,String id){

        HashMap<String, Object> map = new HashMap<>();
        map.put("total_count",100);

        ArrayList<Product> products = new ArrayList<>();
        products.add(new Product(1,"张三",3));
        products.add(new Product(1,"李四",4));
        products.add(new Product(1,"王五",5));
        products.add(new Product(1,"赵六",6));

        map.put("data",products);

        return map;
    }
}

客户端

package com.cai.feignclient;

import com.cai.entiy.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@FeignClient(value = "PRODUCT") // value=服务ID
public interface ProductClient {

    @GetMapping("/productList")
    Map<String, Object> getProductList(@RequestParam("page") int page, @RequestParam("size") int size,@RequestParam("id") String id);

}

使用:

package com.cai.controller;

import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.java2d.pipe.PixelToParallelogramConverter;

import java.util.List;
import java.util.Map;

@RestController
public class CategoryController {

    private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;
    
    @GetMapping("/object")
    public Map<String, Object>  categoryForObject() {

        log.info("进入categoryForObject 方法。。。");

        Map<String, Object> productList = productClient.getProductList(1, 3, "1");

        return productList;
    }
}

当我们从 Map 中的集合取 product对象时,会出现错误


package com.cai.controller;

import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.java2d.pipe.PixelToParallelogramConverter;

import java.util.List;
import java.util.Map;

@RestController
public class CategoryController {

  private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

  @Autowired
  private ProductClient productClient;
  
  @GetMapping("/object")
  public Map<String, Object>  categoryForObject() {

    log.info("进入categoryForObject 方法。。。");

    Map<String, Object> productList = productClient.getProductList(1, 3, "1");

    List<Product> data = (List<Product>) productList.get("data");
    log.info("data:"+data);
    data.forEach(product -> System.out.println(product));
    
    return productList;
  }
}

异常为:

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.cai.entiy.Product

解决方案:

将客户端接口的返回值设置为字符串,自定义序列化方式,使用 JsonObject 反序列化,需要用到 alibaba 的 fastjson 依赖

修改之后的客户端

package com.cai.controller;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.cai.entiy.Product;
import com.cai.feignclient.ProductClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.java2d.pipe.PixelToParallelogramConverter;

import java.util.List;
import java.util.Map;

@RestController
public class CategoryController {

    private static final Logger log = LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    private ProductClient productClient;

    @GetMapping("/category")
    public String category() {

        log.info("进入 category 方法。。。");

        String getString = productClient.product();
        String queryString = productClient.getProduct("张三", 18);

        Product product = new Product(2, "李四", 16);
        String jsonString = productClient.getProduct(product);

        String[] ids = new String[]{"1", "3", "1", "5"};
        String getProductArray = productClient.getProductArray(ids);

        String getProductList = productClient.getProductList(ids);

        log.info("product()——》" + getString);
        log.info("getProduct(name,age)——》" + queryString);
        log.info("getProduct(product)——》" + jsonString);
        log.info("getProductArray(String [] ids)——》" + getProductArray);
        log.info("getProductList(String [] ids)——》" + getProductList);

        return "返回结果:\t\n" + getString + ";\t\n" + queryString + ";\t\n" +
                jsonString + ";\t\n" + getProductArray + ";\t\n" + getProductList;

    }
    
    @GetMapping("/object")
    public String  categoryForObject() {

        log.info("进入categoryForObject 方法。。。");

        String result = productClient.getProductList(1, 3, "1");

        // JSONObject 实现了 Map 接口 ,相当于 Map对象
        JSONObject jsonObject = JSONObject.parseObject(result);
        // Map 对象用 Get 的方法获取值
        Object totalCount = jsonObject.get("total_count");
        log.info("totalCount:"+totalCount);
        Object data = jsonObject.get("data");
        log.info("data:"+data);

        List<Product> products = JSONObject.parseArray(data.toString(), Product.class);
        products.forEach(product -> log.info("product:"+product));

        return result;
    }
}
  • OpenFeign 默认超时时间

默认时间

OpenFeign 的服务连接默认超时时间 和 服务等待响应超时时间一样,都是 1s
  • 设置默认的超时时间
feign:
  client:
    # 设置默认的连接和读取时间
    default-config:
      connectTimeout: 5000
      readTimeout: 5000
  • 设置对单个服务的超时时间,优先级大于默认配置
feign:
  client:
    config:
      PRODUCT:
        # 配置连接时间
        connectTimeout: 2000
        # 配置读取时间
        readTimeout: 5000
  • OpenFeign 日志

简介

Openfeign 是伪httpclient客户端对象,用来帮助我们完成服务间通信,底层用的还是http协议完成服务间的调用
所以为了更好的方便在开发过程中调试Openfeign数据传输和响应处理,Openfeign在设计时添加了日志功能,
默认Openfeign日志功能需要手动开启的

日志使用

1、展示Openfeign的日志
在配置文件中加入以下配置:
logging.level.包名=日志等级

例如:
logging.level.com.cai.controller= debug

2、feign每一个对象提供一个日志对象
在配置文件中加入以下配置:
feign.client.config.服务ID.loggerLevel= FULL

例如:
feign.client.config.ORDERS.loggerLevel= FULL

3、feign日志的级别
    NONE    不记录日志
    BASIC   仅仅记录请求方法、url、响应状态码以及执行时间
    HEADERS 记录BASIC级别的基础上,记录请求和响应的header
    FULL    记录请求和响应的header、body和元数据,展示全部http协议状态
posted @ 2021-08-21 17:10  幸运小菜  阅读(558)  评论(0编辑  收藏  举报