Springcloud之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。

在这里插入图片描述
如果要使用 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.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

这个包引入了以下的依赖:
在这里插入图片描述
这里面有两个非常重要的包:

  • 一个是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工程

在这里插入图片描述

(2)pom文件
    <dependencies>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </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

<?xml version="1.0" encoding="UTF-8" ?>
<!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工程

在这里插入图片描述

(2)pom文件
<dependencies>
    <!--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>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--一般基础通用配置-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </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服务者提供者所提供的服务(我们这里假定只有新增和查询):

在这里插入图片描述

  • 那么我们 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相同,只是端口不同):
在这里插入图片描述

(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

在这里插入图片描述

  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),出现报错页面:

在这里插入图片描述

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

在阈值范围内,没有问题
在这里插入图片描述

六、日志打印演示

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、发请求

在这里插入图片描述

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

在这里插入图片描述

七、OpenFeign的核心原理

请参考以下文章:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值