Springcloud之OpenFeign服务调用

文章目录

    • 一、引言
    • 二、什么是Feign
    • 三、Feign 和 Openfeign 的区别
    • 四、如何使用OpenFeign
      • 1、 新建微服务服务提供者cloud-provider-payment8001 和 8002
        • (1)创建maven工程
        • (2)pom文件
        • (3)application.yml
        • (4)主启动类
        • (5)dao层
        • (6)业务接口
        • (7)控制类
      • 2、新建微服务服务消费者cloud-consumer-feign-order80
        • (1)创建Maven工程
        • (2)pom文件
        • (3)application.yml配置文件
        • (4)主启动类
        • (5)业务接口
        • (6)控制类
        • (7)测试
      • 3、OpenFeign 与 Ribbon 调用区别
    • 五、设置OpenFeign的超时控制
      • 1、为什么要设置
      • 2、 修改 Order80 的 application.yml 文件
      • 3、在服务提供者 8001 的控制类中添加超时方法
      • 4、在服务消费者 80 的 service 接口中加入该超时方法
      • 5、在服务消费者 80 的 controller 类中添加该方法暴露给浏览器
      • 6、测试 http://localhost/consumerFeign/payment/timeout
      • 7、更改超时方法的停止时间为 3 秒
      • 8、再次测试 http://localhost/consumerFeign/payment/timeout
    • 六、日志打印演示
      • 1、在 Order80 下新建 config 包并新建 FeignConfig 类
      • 2、在 Order80 的 application.yml 文件中开启 Feign 日志支持
      • 3、发请求
      • 4、后台查看日志打印结果
    • 七、OpenFeign的核心原理

一、引言

当今是微服务横行的时代,各个微服务之间相互调用是一件再平常不过的时候。在采用HTTP协议进行通信的微服务中,我们自己可能去封装一个HttpClient工具类去进行服务间的调用,封装一个HttpClient工具,我们就需要考虑一下这些事情:

  • 我们在发送一个HTTP请求时,我们需要选择请求方式GET、POST、DELETE等,我们需要构建请求参数、构建请求头信息等,那么作为一个工具类我们是不是也要提供各种参数的灵活配置
  • 因为采用restful API 风格的HTTP请求参数和返回数据都是字符串的格式,那我们是否需要考虑序列化和反序列化问题
  • 当同一个服务部署到多台服务器的时候,我们是不是应该采用轮询或者随机的方式去选择服务器,这也就是我们常说的负载均衡。从另一方面来说我们的核心是解决服务间的调用,但是我们在设计一个通用HttpClient工具的时候是否也应该支持负载均衡,以及如何和负载均衡高度解耦

为此,大名鼎鼎的Feign应时而生,我们在学习Feign的实现的时候,我们应该带着这些问题去学习Feign的实现原理。

二、什么是Feign

官网路径:https://github.com/OpenFeign/feign
OpenFeign官网:https://github.com/spring-cloud/spring-cloud-openfeign

Feign 是声明式 Web 服务客户端,它使编写 Web 服务客户端更加容易 Feign 不做任何请求处理,通过处理注解相关信息生成 Request,并对调用返回的数据进行解码,从而实现简化HTTP API 的开发。

当然你也可以直接使用 Apache HttpClient 来实现Web服务器客户端,但是 Feign 的目的是尽量的减少资源和代码来实现和 HTTP API 的连接。通过自定义的编码解码器以及错误处理,你可以编写任何基于文本的 HTTP API。

Springcloud之OpenFeign服务调用_第1张图片
如果要使用 Feign,需要创建一个接口并对其添加 Feign 相关注解,另外 Feign 还支持可插拔编码器和解码器,致力于打造一个轻量级 HTTP 客户端。

下面就是Feign针对一个HTTP API的接口定义:

interface GitHub {
  // RequestLine注解声明请求方法和请求地址,可以允许有查询参数
  @RequestLine("GET /user/list")
  List<User> list();
}

目前由于Spring Cloud微服务的广泛使用,广大开发者更倾向于使用spring-cloud-starter-openfeign,Spring Cloud 添加了对 Spring MVC 注解的支持,在微服务中我们的接口定义有所变化:

@FeignClient(name="服务名",contextId="唯一标识")
interface GitHub {
  @GetMapping("/user/list")
  List<User> list();
}

三、Feign 和 Openfeign 的区别

Feign 最早是由 Netflix 公司进行维护的,后来 Netflix 不再对其进行维护,最终 Feign 由社区进行维护,更名为 Openfeign。

Openfeign的pom依赖:

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

这个包引入了以下的依赖:
Springcloud之OpenFeign服务调用_第2张图片
这里面有两个非常重要的包:

  • 一个是spring-cloud-openfeign-core,这个包是SpringCloud支持Feign的核心包,Spring Cloud 添加了对 Spring MVC 注解的支持(通过SpringMvcContract实现),并支持使用 Spring Web 中默认使用的相同 HttpMessageConverters。另外,Spring Cloud同时集成了Ribbon和Eureka以及Spring Cloud LoadBalancer,以在使用 Feign 时提供负载均衡的 HTTP客户端。针对于注册中心的支持,包含但不限于 Eureka,比如 Consul、Naocs 等注册中心均支持。
  • 另一个包是feign-core,也就是feign的原生包,具体使用细节可以参考Feign配置使用。通俗点说,spring-cloud-openfeign-core就是通过一系列的配置创建Feign.builder()实例的过程。

在我们 SpringCloud 项目开发过程中,使用的大多都是这个 Starter Feign。本文也主要针对于openFeign进行讲解。

四、如何使用OpenFeign

1、 新建微服务服务提供者cloud-provider-payment8001 和 8002

注意:这里8001和8002的代码相同,唯一不同的是服务端口,用于负载均衡,因此只整理8001的源码

(1)创建maven工程

Springcloud之OpenFeign服务调用_第3张图片

(2)pom文件

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <dependency>
            <groupId>com.atguigu.springcloudgroupId>
            <artifactId>cloud-api-commonsartifactId>
            <version>${project.version}version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
            <version>1.1.10version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

(3)application.yml

server:
  port: 8001

spring:
  application:
    name: cloud-payment-service
  zipkin:
      base-url: http://localhost:9411
  sleuth:
    sampler:
    #采样率值介于 0 到 1 之间,1 则表示全部采集
    probability: 1
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver              # mysql驱动包
    url: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456


eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #单机版
      defaultZone: http://localhost:7001/eureka
      # 集群版
      #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
      instance-id: payment8001
      #访问路径可以显示IP地址
      prefer-ip-address: true
      #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
      #lease-renewal-interval-in-seconds: 1
      #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
      #lease-expiration-duration-in-seconds: 2


mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.springcloud.entities    # 所有Entity别名类所在包

(4)主启动类

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

(5)dao层

PaymentDao.java

@Mapper
public interface PaymentDao
{
    public int create(Payment payment);

    public Payment getPaymentById(@Param("id") Long id);
}

PaymentMapper.xml


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >

<mapper namespace="com.atguigu.springcloud.dao.PaymentDao">

    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial)  values(#{serial});
    insert>


    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <id column="serial" property="serial" jdbcType="VARCHAR"/>
    resultMap>
    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
        select * from payment where id=#{id};
    select>

mapper>

(6)业务接口

PaymentService.java

public interface PaymentService
{
    public int create(Payment payment);

    public Payment getPaymentById(@Param("id") Long id);
}

PaymentServiceImpl.java

@Service
public class PaymentServiceImpl implements PaymentService
{
    @Resource
    private PaymentDao paymentDao;

    public int create(Payment payment)
    {
        return paymentDao.create(payment);
    }

    public Payment getPaymentById(Long id)
    {
        return paymentDao.getPaymentById(id);
    }
}

(7)控制类

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

    @Resource
    private DiscoveryClient discoveryClient;

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) {
        int result = paymentService.create(payment);
        log.info("*****插入结果:" + result);

        if (result > 0) {
            return new CommonResult(200, "插入数据库成功,serverPort: " + serverPort, result);
        } else {
            return new CommonResult(444, "插入数据库失败", null);
        }
    }

    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        Payment payment = paymentService.getPaymentById(id);

        if (payment != null) {
            return new CommonResult(200, "查询成功,serverPort:  " + serverPort, payment);
        } else {
            return new CommonResult(444, "没有对应记录,查询ID: " + id, null);
        }
    }

    @GetMapping(value = "/payment/discovery")
    public Object discovery() {
        List<String> services = discoveryClient.getServices();
        for (String element : services) {
            log.info("*****element: " + element);
        }

        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        for (ServiceInstance instance : instances) {
            log.info(instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
        }

        return this.discoveryClient;
    }

    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeout() {
        // 业务逻辑处理正确,但是需要耗费3秒钟
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return serverPort;
    }
}

2、新建微服务服务消费者cloud-consumer-feign-order80

(1)创建Maven工程

Springcloud之OpenFeign服务调用_第4张图片

(2)pom文件

<dependencies>
    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-openfeignartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
    dependency>
    
    <dependency>
        <groupId>com.atguigu.springcloudgroupId>
        <artifactId>cloud-api-commonsartifactId>
        <version>${project.version}version>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-webartifactId>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-actuatorartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-devtoolsartifactId>
        <scope>runtimescope>
        <optional>trueoptional>
    dependency>
    <dependency>
        <groupId>org.projectlombokgroupId>
        <artifactId>lombokartifactId>
        <optional>trueoptional>
    dependency>
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-testartifactId>
        <scope>testscope>
    dependency>
dependencies>

(3)application.yml配置文件

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

(4)主启动类

这里注意使用@EnableFeignClients注解来开启Feign的使用

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

(5)业务接口

cloud-consumer-feign-order80 是服务消费者,这里我们调用的服务的来自服务提供者8001 和 8003,因此需要查看8001提供的服务有哪些。

  • 查看8001服务者提供者所提供的服务(我们这里假定只有新增和查询):

Springcloud之OpenFeign服务调用_第5张图片

  • 那么我们 order80 的PaymentFeignService接口就可以这么写:
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService
{
	@GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody  Payment payment);

}

我们可以注意到接口上添加了 @FeignClient 这个注解,其中的value需要指明要使用的服务提供者的名称。

这里我们要调用服务提供者 8001 提供的服务,因此需要去8001 的application.yml文件中查看(8002的服务名称和8001相同,只是端口不同):
Springcloud之OpenFeign服务调用_第6张图片

(6)控制类

@RestController
@Slf4j
public class OrderFeignController
{
    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping(value = "/consumerFeign/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
        return paymentFeignService.getPaymentById(id);   //调用服务
    }


    @GetMapping(value ="/consumerFeign/payment/create")
    public CommonResult<Payment> create(Payment payment){
        return paymentFeignService.create(payment);     //调用服务
    }
}

(7)测试

  1. 启动Eureka7001,7002服务注册中心
  2. 启动服务提供者8001、8002
  3. 启动服务消费者80
  4. 测试 create 服务:http://localhost:80/consumerFeign/payment/create

Springcloud之OpenFeign服务调用_第7张图片

  1. 测试 getPaymentById 服务:http://localhost:80/consumerFeign/payment/get/31

在这里插入图片描述

3、OpenFeign 与 Ribbon 调用区别

Ribbon调用需要配合RestTemplate进行远程调用,写法略微麻烦一些,但是openfeign 使用 @FeignClient 即可完成RestTemplate的远程调用及Ribbon的负载均衡的功能,对于我们开发人员来说可以更加专注于业务,同时避免代码的冗余。

五、设置OpenFeign的超时控制

1、为什么要设置

假设我们仍然使用上述模块,此时可能会出现一种情况:当使用order80调用服务提供者8001和8002时,8001的业务逻辑处理需要三秒钟,但是消费者order80这边认为只要超过一秒没有给我响应数据就认为这个请求是失败的。这种情况下如果我们不进行设置,那么order80调用8001永远会失败,没有请求成功的可能。

2、 修改 Order80 的 application.yml 文件

这里我们设置超过五秒没有响应则认为该请求失败:

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

3、在服务提供者 8001 的控制类中添加超时方法

@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout() {
    // 业务逻辑处理正确,但是需要耗费10秒钟
    try {
        TimeUnit.SECONDS.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return serverPort;
}

4、在服务消费者 80 的 service 接口中加入该超时方法

@GetMapping("/payment/feign/timeout")
public String paymentFeignTimeout();

5、在服务消费者 80 的 controller 类中添加该方法暴露给浏览器

@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout()
{
    // OpenFeign客户端一般默认等待1秒钟,我们设置了5秒,但是服务提供者需要10秒处理业务逻辑
    return paymentFeignService.paymentFeignTimeout();
}

6、测试 http://localhost/consumerFeign/payment/timeout

响应时间(10s)超出我们设置的时间(5s),出现报错页面:

Springcloud之OpenFeign服务调用_第8张图片

7、更改超时方法的停止时间为 3 秒

@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout() {
    // 业务逻辑处理正确,但是需要耗费3秒钟
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return serverPort;
}

8、再次测试 http://localhost/consumerFeign/payment/timeout

在阈值范围内,没有问题
Springcloud之OpenFeign服务调用_第9张图片

六、日志打印演示

1、在 Order80 下新建 config 包并新建 FeignConfig 类

@Configuration
public class FeignConfig
{
    @Bean
    Logger.Level feignLoggerLevel()
    {
        return Logger.Level.FULL;
    }
}

2、在 Order80 的 application.yml 文件中开启 Feign 日志支持

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.atguigu.springcloud.service.PaymentFeignService: debug

3、发请求

Springcloud之OpenFeign服务调用_第10张图片

4、后台查看日志打印结果

七、OpenFeign的核心原理

请参考以下文章:

  • Spring Cloud – OpenFeign 核心原理
  • OpenFeign原理解析

你可能感兴趣的:(spring,cloud,spring,cloud,java,服务器)