spring-cloud(六)微服务调用篇(二)OpenFeign

spring-cloud-Hoxton.SR6 (六)微服务调用篇(二)OpenFeign

本文spring-cloud 版本为:hoxton.sr6

本文spring-boot版本为:2.2.x-2.3.x

前言…

思考: 使用RestTemplate+ribbon已经可以完成微服务间服务间的调用,且能实现负载均衡效果,为什么还要使用feign?

RestTemplate+ribbon存在的问题:

1.ribbon已停止更新

2.每次调用服务都需要写这些代码,存在大量的代码冗余

image-20200830113133618

3.服务地址如果修改,维护成本增高

4.使用时不够灵活

基于以上情况,微服务调用之 OpenFeign组件便出来了!下边一起来进入学习模式吧!

各位客官,里边请儿!


一、什么是OpenFeign?

​ 其实一开始呢,是没有OpenFeign 呢,最早的是Feign,Feign是Spring Cloud组件中的一个轻量级RESTfulHTTP服务客户端,其内置了ribbon,默认实现了负载均衡的效果,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。…反正优点多多!

​ 但是!Feign 不支持SpringMVC注解 ,它有一套自己的注解!用过springMVC注解的都知道(废话,都微服务了还没用过mvc注解?@RequestMapping、@RequestBody 、@ResponseBody、@PathVariable、@RequestParam 等等),如果这些没法用的话,那么对开发人员来说,天大噩耗,又得码多少代码才能实现原有功能哦!所以呢,这玩意肯定会被废弃淘汰的呀,所以呀 OpenFeign闪亮登场!

OpenFeign 组件

- 官网地址: https://cloud.spring.io/spring-cloud-openfeign/reference/html
- OpenFeign 在Feign的基础上添加了springmvc注解的支持。使得微服务间负载均衡以及调用如同吃饭喝水般简单快捷又方便!用过的,都说好!

简单地聊了聊feign 、OpenFeign,那么咱们来系统梳理下 Ribbon、feign、OpenFeign 的区别吧!


二、Ribbon、feign、OpenFeign 三者间区别

Ribbon

Ribbon 是 Netflix开源的基于HTTP和TCP等协议负载均衡组件

Ribbon 可以用来做客户端负载均衡,调用注册中心的服务

Ribbon的使用需要代码里手动调用目标服务,存在大量的代码冗余,操作死板不灵活

Feign

Feign是Spring Cloud组件中的一个轻量级RESTfulHTTP服务客户端

Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。

Feign本身不支持Spring MVC的注解 戳我进官方文档,它有一套自己的注解

OpenFeign

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,
并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。


这三者呢,我仅用过Ribbon+RestTemplate 、OpenFeign ,至于 feign的话,其仅仅只能算是过渡产物吧,实际一个注册中心下,公司里基本都会使用OpenFeign了!

下边呢,我们开始进入系统的学习吧!

三、OpenFeign 组件的简单使用

OpenFeign的使用非常简单哈,可以快速上手,完成微服务间调用!且在最开始的注册中心篇其实已经初略的使用过了!

总结来讲的话,就四个步骤吧!

  • 导入依赖
  • 注解开启功能
  • 编写调用接口

首先我们来回顾一下,服务于服务间相对而言来讲的概念!

服务提供者:

服务提供者实际也是一个独立的微服务,提供者即被调用者,一个服务的接口 依赖于另一个服务接口提供的返回值,那么被依赖的服务即为服务提供者!

服务消费者:

服务消费者实际也是一个独立的微服务,消费者即调用者,请求调用的发起者服务!

例如:现在有用户中心服务模块、登录注册服务模块

我在登录时候是不是用户需要请求登录注册服务模块接口,登录接口中呢,是不是需要去用户中心服务判断用户正确性,然后响应给用户是否登录成功?

此时啊,登录注册服务便是依赖于用户中心模块,此时,登录注册为服务消费者,用户中心模块为提供者!

至于某些服务是否为提供者还是消费者,那得看业务链路了!没有一定死的!

正如同某同志追女神备胎了n年,发现原来女神又是另一位男同学的舔狗! 站的角度不同罢了!


正式开搞,搭建两个商品服务一个订单服务

image-20200830182804858

导入依赖

因为前边演示Ribbon+RestTemplate 做了一些微服务,所以我们copy 改一改即可,暂时将demo-order作为服务消费者去消费demo-product吧!

<!--Open Feign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

image-20200830182044635

注解开启功能

导入依赖后,OpenFeign默认是关闭的,必须手动开启(注解开启)

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SpringcloudOpenfeignHystrixOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringcloudOpenfeignHystrixOrderApplication.class, args);
    }

}

image-20200830182220697

编写调用接口

首先,我们来看一下,服务提供者 商品服务demo-product向我们提供了什么接口

image-20200830185657685

ok,订单服务 demo-order中编写调用商品服务demo-product的接口

重点:

  • 必须为接口 且打上@FeignClient 注解 value 中指明调用服务提供者名字 (注册中心找,找到了copy过来即可)image-20200830184009889
@FeignClient(value = "demo-product")
public interface ProductFeign {

    @GetMapping("/product/demo/{id}")
    Result findProduct(@PathVariable("id") Integer id);
}

order 向外部暴露的接口

@RestController
@RequestMapping("/order")
public class OrderController {
    /**
     * openfeign客户端对象 调用商品服务
     */
    private final ProductFeign productFeign;
    @Autowired
    public OrderController(ProductFeign productFeign) {
        this.productFeign = productFeign;
    }

    @GetMapping("/find/product/{id}")
    public Result findProduct(@PathVariable("id") Integer id) {
        return productFeign.findProduct(id);
    }
}

这就编写完了,我们order 服务可以直接调用 Product 服务了,且前边也说了OpenFeign是基于Ribbon 的,所以呢,其也开启了负载功能!

那么,我们访问9002 /order/find/product/{id}接口 测试一下

image-20200830185748357

image-20200830185755810

嗯!商品服务调用成功!并且哈ProductFeign客户端是可以复用的哈,即当前订单服务哪里需要他,只要类是交由spring管理的 都可以将ProductFeign客户端进行注入并使用

Feign编写简单总结

1.编写接口 接口上使用 @FeignClient 属性value指明被调用服务名

2.按照服务提供者接口 编写入参 返回值 url即可

四、OpenFeign 进阶

OpenFeign 组件呢,其功能就是微服务间调用以及负载,那么我们首先来理一理微服务间调用!

实际就是一个服务的接口 依赖与其他服务接口响应,凑在一起成为一次完整的请求链路

那么在实际项目中哈,传参可能多种多样!使用OpenFeign 有没有什么注意的点呢?当然是有哈,下边我们就来演示与讲解!

上边的例子是使用的@PathVariable 来完成请求,可能有的老项目呢,不会遵从RestFul风格,仅仅就是普通的传参 例如/order/find/product?name=zs&age=12


GET方式调用服务传递参数
  • PathVariable形式

使用@PathVariable @GetMapping 指明Url

image-20200830193835040

  • 直接参数形式

服务提供者提供接口如下

image-20200830215546028

    @RequestMapping("/demo")
    public Result getProductByParam(Integer id,String name) {
        log.info("调用成功" + port);
        return Result.success("商品获取成功:当前商品Id为:" + id +"商品名为:"+name+ "当前调用商品服务IP 端口" + ip + ":" + port);
    }

那么消费者使用OpenFeign如何调用呢?OpenFeign这样写吗?

image-20200830194253460

我们order创建接口外部调用测试一下

image-20200830194313893

image-20200830194605288

我们在两个商品服务接口中打上断点以及在Order 调用那里打上断点看一下!

image-20200830194859415

成功进入order 且调用了ProductFeign 客户端,但是! 我们的product服务却没有接收到请求!

order 服务在调用product那块 报了空指针! 没有获取到参数

image-20200830195141708

解决措施参数打上@RequestParam注解

Feign客户端、服务提供者接口、参数打上@RequestParam注解

再次请求,发现服务提供者断点来了,且拿到了参数!

image-20200830200901080

但是啊!一波平了一波又起了啊!

断点太久,发现我们的order服务(服务消费者)居然直接抛出了异常!看异常意思,接口读取超时.但是呢,事实上请求的确是来到了我们的服务提供者product 啊,仅仅是因为断点了一点时间嘛!这也太扯了是吧!

image-20200830201311451

在实际生产环境或者开发环境中,或许因为网络分区原因,或许因为接口本身逻辑复杂耗时较长,如果这时候,OpenFeign在调用服务的时候直接返回读取超时肯定是不合理的!所以,我们要改OpenFeign超时时间!

Openfeign超时时间

OpenFeign超时说明

  • 默认情况下,openFiegn在进行服务调用时,要求服务提供方处理业务逻辑时间必须在1S内返回,如果超过1S没有返回则OpenFeign会直接报错,不会等待服务执行,但是往往在处理复杂业务逻辑是可能会超过1S,因此需要修改OpenFeign的默认服务调用超时时间。

  • 调用超时会出现如下错误:

    image-20200830202124960

OpenFeign超时时间设置

  • yml设置 在服务消费者端设置OpenFeign 超时时间

    feign:
      client:
        config:
          default:
            #连接超时时间 单位为毫秒
            connectTimeout: 5000
            #读取超时时间 单位为毫秒
            readTimeout: 5000
    
  • 测试模拟 将我们的两个服务提供者 product 接口 一个睡眠6秒 一个睡眠4秒

    image-20200830203746849

    image-20200830203838316

    测试:

    image-20200830203904185

    image-20200830203938859

端口为9000的product服务接口调用成功!说明我们的OpenFeign超时时间设置是有效的了!


但是呢,每个微服务处理的业务不同,那么消费者端的OpenFeign应该也随着服务提供者业务来设置超时时间呀!这个呢,Openfeign已经实现了

我们上方那种设置呢,就是针对的全局设置OpenFeign超时时间

OpenFeign全局超时时间 此设置只要是当前消费者调用到Feign客户端均以XX秒(我这里设置5000则 5秒)超时

feign:
  client:
    config:
      default:
        #连接超时时间 单位为毫秒
        connectTimeout: 5000
        #读取超时时间 单位为毫秒
        readTimeout: 5000

OpenFeign针对不同服务设置不同超时时间

其实呢,很简单将上边的 default 改为服务提供者具体名字即可例如:demo-product

feign:
  client:
    config:
      #服务提供者名字  设置了此后,那么当前项目调用 demo-product 服务超时时间为xx(我这里为5秒)
      demo-product:
        #连接超时时间
        connectTimeout: 5000
        #读取超时时间
        readTimeout: 5000

可能有小伙子比较俏皮,相出指明一个或多个具体服务的超时时间,其余统一再设置超时时间可不可以呢

例如:yml配置改成这样!

feign:
  client:
    config:
      #demo-product 服务超时时间设置
      demo-product:
        #连接超时时间
        connectTimeout: 5000
        #读取超时时间
        readTimeout: 5000
      #全部服务提供者超时时间设置
      default:
        #连接超时时间
        connectTimeout: 3000
        #读取超时时间
        readTimeout: 3000

那么,此时我们的Feign客户端访问 Product时候是以全局的3秒为准还是以自己的5秒为准呢?测试一下!

睡6秒的依然超时

image-20200830205316099

睡4秒的,依然能正确获取接口响应

image-20200830205402963

那么说明,指定了具体服务超时时间后,OpenFeign 则以服务名设置的超时时间为准,否则则使用全局 default


OK 碰到问题就先解决嘛,前边是GET 方式调用服务传递参数来着!接下来,看下POST

OpenFeign调用详细日志展示

往往在服务调用时我们需要详细展示feign的日志,默认feign在调用是并不是最详细日志输出,因此在调试程序时应该开启feign的详细日志展示。

feign对日志的处理非常灵活可为每个feign客户端指定日志记录策略,并且呢feign日志的打印只会DEBUG级别做出响应。

我们可以为feign客户端配置各自的logger.level对象,并且设置不同的日志内容打印级别

- NONE  #不记录任何日志
- BASIC #仅仅记录请求方法,url,响应状态代码及执行时间
- HEADERS #记录Basic级别的基础上,记录请求和响应的header
- FULL #记录请求和响应的header,body和元数据

怎么设置呢?yml配置文件设置即可

feign.client.config.demo-product.loggerLevel=full  #开启指定服务(我这里则为demo-product)日志展示
#feign.client.config.default.loggerLevel=full  #全局开启服务日志展示(所有的配置)
logging.level.com.leilei.demo=debug    #日志扫包(feign客户端需在此包下),日志级别必须是debug级别

指定具体服务设置feign日志级别

feign:
  client:
    config:
      #demo-product 服务设置
      demo-product:
        #显示调用日志
        loggerLevel: full
        #连接超时时间
        connectTimeout: 5000
        #读取超时时间
        readTimeout: 5000

全局服务设置feign日志级别

feign:
  client:
    config:
      #全部服务提供者设置
      default:
        #显示调用日志
        loggerLevel: full
        #连接超时时间
        connectTimeout: 3000
        #读取超时时间
        readTimeout: 3000

日志扫包

logging:
  level:
  	#指定包路径 feign客户端必须在此包路径之下
    com.leilei.demo: debug

与设置超时时间一样,如果指定服务设置日志级别后,再设置全局,那么指定服务生效的依然是其针对具体服务的配置

例如: demo-product 设置了BASIC 全局设置了 FUll ,那么order服务调用 demo-product呢,feign只会是输出其BASIC级别的日志信息

测试看下效果:

未设置前:

请求访问http://localhost:9002/order/find/product/1 啥也没有

image-20200831215348185

设置后

image-20200831215603030

可以看到,我们能够详细的看到feign的调用信息了!

POST方式调用服务传递参数

首先,还是得在服务提供者编写对应接口啊!

    @PostMapping
    public Result addProduct(Product product) {
        log.info("添加商品接口调用成功" + port);
        product.setId(new Random().nextInt(20));
        return Result.success("商品添加成功:当前商品服务Ip 端口为"+ ip + ":" + port,product);
    }

消费者端调用

这里服务虽然为order、 product 但是大家不要被这名字迷惑了啊,我演示的内容仅仅是OpenFeign 微服务间调用而已啊!逻辑别当真啊!

你只需要当它们两为不同业务的微服务接口即可!

你只需要当它们两为不同业务的微服务接口即可!

你只需要当它们两为不同业务的微服务接口即可!

order外暴接口

image-20200830211301258

ProductFeign客户端调用 product服务

image-20200830211806127

测试! 熟悉的错误

image-20200830211420156

image-20200830211545048

之前,是个普通参数,我们用@RequestParam注解 ,现在是个对象,我们又该怎么做呢?

答:使用大家耳熟能详的注解@RequestBody

由此 接口 改造!

image-20200830212046059

image-20200830212055468

再次测试!

image-20200830212116834

OK 接受到了参数!

到了这里,GET、POST 都演示了这种错误!可能有小伙伴纳闷了?为什么要讲这些呢?

那是因为:全是血和泪啊!!当初公司没有遵循RestFul规范,而且我入职的时候,前端后端已经是老相识了,传值很随意!最开始上手微服务时候,传参就是这么传的啊啊啊啊啊!那真的是一步一个脚印一步一个脚印啊!这些报错,都是走过的坑啊!


PUT方式调用服务传递参数

put 一般用作为表单修改,上述的错误呢,已经是演示腻了,所以呢,我们直接来个规范传参即可!

服务提供者提供的接口为:

    @PutMapping
    public Result editProduct(@RequestBody Product product) {
        log.info("修改商品接口调用成功" + port);
        return Result.success("商品修改成功:当前商品服务Ip 端口为"+ ip + ":" + port,product);
    }

消费者入口

    @PutMapping("/place/order")
    public Result  editProduct(@RequestBody Product product) {
        return productFeign.editProduct(product);
    }

消费者端 feign客户端调用提供者

    @PutMapping("/product")
    Result editProduct(@RequestBody Product product);

测试:

image-20200830213519954

DELETE方式调用服务传递参数

提供者端接口

    @DeleteMapping("/{id}")
    public Result deleteProduct(@PathVariable("id") Integer id) {
        return Result.success("商品删除成功:当前商品服务Ip 端口为" + ip + ":" + port, "当前删除商品ID为:"+id);
    }

消费者端Feign 调用提供者接口

    @DeleteMapping("/product/{id}")
    Result editProduct(@PathVariable("id") Integer id);

测试:

image-20200830214204661

那么 CRUD方式都走完了,可能小伙伴还有还有一个疑惑!

我服务提供者端设置了一个公共Url 前缀 /product

image-20200830220240266

那么我在Feign 调用接口是否可以这样写一个统一的呢?

例如这样?

image-20200830220452982

改为下方样式!

image-20200830220436314

测试下:

image-20200830221613646

?????????居然可行?我记得以前使用的时候是不可以这样的啊,会报错的呀??难道改了??这一点我还得下来查查了!但是个人建议还是不要@RequestMapping写在Feign接口类上边了!

OK,OpenFeign 组件的基本使用差不多就这些了!

我们来说下使用注意事项以及重点!加深一下记忆!

OpenFeign 巩固:使用步骤以及注意事项

使用步骤

1.引入依赖

2.直接开启功能

3.feign客户端接口编写 调用对应服务提供者

注意事项以及重点!!!敲黑板了!!!

image-20200811234502825

1.消费者端注意设置OpenFeign超时时间

2.消费者端参数传递注意事项

  • 尽力少用或不用 @RequestParam

    url请求不雅观

    image-20200830215005865

  • 为了规范,推荐使用 @PathVariable 无论是get、delete 还是put、post 请求时额外需要url参数时都不建议使用 @RequestParam

    image-20200830215032024

  • 服务提供方和调用方对象参数一定要使用 @RequestBody

OpenFeign负载均衡策略设置

我们前边讲过 Openfeign 其底层是基于Ribbon的 ,并且 使用OpenFeign 默认的是轮询的负载均衡策略,涉及到项目或者服务器配置,可能会对负载策略做一些更改

我们可以在消费者服务配置Yml中更改负载策略!

#对demo-product 服务设置负载均衡策略为随机
demo-product:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #配置规则 随机
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #配置规则 随机
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #配置规则 轮询
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule #配置规则 重试
# NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule #配置规则 响应时间权重
 # NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略
  # ConnectTimeout: 500 #请求连接超时时间
  # ReadTimeout: 1000 #请求处理的超时时间
  # OkToRetryOnAllOperations: true #对所有请求都进行重试
  # MaxAutoRetriesNextServer: 2 #切换实例的重试次数
  # MaxAutoRetries: 1 #对当前实例的重试次数
OpenFeign 请求头传递

在某一些业务场景,我们可能需要微服务间传递请求头信息 例如(token) 方便我们快速拿到当前操作对象进行业务处理!

默认情况下呢,OpenFeign 是不会帮我们传递任何请求头信息的

详细请看!

我们首先来改造服务提供者接口,尝试打印请求头信息!

image-20200830225754158

    @RequestMapping("/demo/{id}")
    public Result getProduct(HttpServletRequest request, @PathVariable("id") Integer id) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        while (enumeration.hasMoreElements()) {
            String key = enumeration.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return Result.success("商品获取成功:当前商品Id为:" + id + "当前调用商品服务IP 端口" + ip + ":" + port, "请求头信息:" + map);
    }

直接发送请求看看!

image-20200830230001870

那么这个情况怎么办呢?

我们可以在服务消费者复写Feign配置拿到我们的请求头,将请求头包裹在其请求对象中,传递给服务提供者!

直接上代码

package com.leilei.demo;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author lei
 * @version 1.0
 * @date 2020/8/30 22:28
 * @desc feign 请求头传递
 */
@Configuration
public class FeignInterceptor implements RequestInterceptor {
   /**
   * 复写feign请求对象
   * @param requestTemplate
   */
  @Override
  public void apply(RequestTemplate requestTemplate) {
    //获取请求头
    Map<String,String> headers = getHeaders(getHttpServletRequest());
    for(String headerName : headers.keySet()){
      requestTemplate.header(headerName, getHeaders(getHttpServletRequest()).get(headerName));
    }
  }
  //获取请求对象
  private HttpServletRequest getHttpServletRequest() {
    try {
      return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
  }
  //拿到请求头信息
  private Map<String, String> getHeaders(HttpServletRequest request) {
    Map<String, String> map = new LinkedHashMap<>();
    Enumeration<String> enumeration = request.getHeaderNames();
    while (enumeration.hasMoreElements()) {
      String key = enumeration.nextElement();
      String value = request.getHeader(key);
      map.put(key, value);
    }
    return map;
  }
}

ok,配置也搞好了,咱们重启服务测试一波!

image-20200830230651763

大功告成,打完收工!OpenFeign 的一些基础使用就是这了!

附上项目源码:OpenFeign使用

五、问题思索

前边,我们所有的演示均是服务提供者正常为我们提供服务的情况,那么万一服务提供者down机了呢?一段段时间无法提供服务,或者服务提供者业务逻辑执行报错了呢?我们消费者 try catch 返回错误信息吗?我们的消费者还使用OpenFeign 去调用提供者是不是有些不合适呢?

例如:我们这里吧所有服务提供者全部关闭

image-20200830231108405

这个时候,我们仍然访问消费者order 由消费者去调用product(提供者看看什么情况!)

废话,按理说,服务提供者我都关完了,能不报错能访问成功还有个鬼了!

image-20200830231212063image-20200830231340821

这个时候,无论我访问order多少次他都会直接请求我们的product (虽然Product)已经不在了,并且报错信息不友好(虽然可以全局异常,但用户体验仍是不好的)这个时候呢,我们想,要是有一个组件能知道某服务挂掉了,多次尝试不行后一段时间直接返回给拖底数据给用户,然后呢,服务提供者恢复了以后,OpenFeign又正常请求服务提供者,这既提升了用户体验,又避免了没必要的请求保证了消费者服务的稳定(不然一直打印错误日志,不利于以后排查)

上边说的这个组件是有的,叫什么呢?叫**Hystrix**

下一篇!微服务守护者 -Hystrix

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值