详解Feign远程调用

目录

前言

1. RestTemplate的问题

2. Feign代替RestTemplate

2.1 Feign使用步骤

2.2 总结

3. Feign的自定义配置

3.1 配置文件方式

3.2 Java代码方式

3.3 总结

4. Feign使用优化

总结

5. Feign最佳实践分析

5.1 继承方式

5.2 抽取方式

5.3 总结

6. Feign抽取方式的代码实现


前言

作为开发来讲,在实际开发工作中,我们遇到最多的,也是最重要的就是远程调用的关系。特别是在微服务发展的这么快的新时代。服务的粒度越来越小,服务与服务之间也多了很多远程调用的关系。接下来,我会以我自己写的cloud-demo为例,来详细讲讲Feign的远程调用问题。

1. RestTemplate的问题

大家都知道在SpringBoot中一般都是适用RestTemplate,就像我下面一样:

使用RestTemplate访问restful接口,非常的简单粗暴,但是很不优雅。他的主要缺点就是:

  • 代码可读性差,编程体验不统一
  • 参数复杂URL难以维护

2. Feign代替RestTemplate

 如果我们想要更加优雅的发起远程调用的话,我们就需要今天的主角——FeignFeign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign ,其作用就是帮助我们优雅的实现http请求的发送,解决上面的问题。

2.1 Feign使用步骤

Feign的使用步骤如下:

1)引入依赖

我们在order-service服务的pom文件中引入feign的依赖:

<!--feign客户端依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2)添加注解

在order-service的启动类添加@EnableFeignClients注解开启Feign的功能:

3)编写Feign的客户端

在order-service中新建一个接口,内容如下:

package cn.itcast.order.clients;

import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient("userservice")
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如:

  • 服务名称:userservice
  • 请求方式:GET
  • 请求路径:/user/{id}
  • 请求参数:Long id
  • 返回值类型:User

这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了。 

跟我们之前的RestTemplate对比一下:

4)测试

修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate:

这样来对比一下,是不是比使用RestTemplate调用更加优雅? 编程体验是不是变得统一了?上面一个方法,下面一个方法。如果我不说,跟上面的调用方法是不是一样,根本看不见url的调用。

接下来,我们把user-service和order-service都启动,做一个测试,看看能不能正常发起远程调用,把user-server的日志都清空,然后多发起几次请求。

都能正确的返回,接下来,我们看看user-service两个实例的日志:

发现两个实例都被调用了,说明我们不仅实现了远程调用,还实现了负载均衡。这到底是为什么?我们现在打开order-service的pom文件,看看他的集成依赖:

发现feign核心内部带上了Ribbon,所以feign不仅可以远程调用也能负载均衡。

PS:我这里查看依赖用了一个插件,叫做Maven Helper,可以快速查看maven依赖树关系。

2.2 总结

使用Feign的步骤:

  • 引入依赖
  • 添加@EnableFeignClients注解
  • 编写FeignClient接口
  • 使用FeignClient中定义的方法代替RestTemplate

3. Feign的自定义配置

Feign可以支持很多自定义配置,常用的如下表所示:

类型作用说明
feign.Logger.Level修改日志级别包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder响应结果的解析器http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder请求参数编码将请求参数编码,便于通过http请求发送
feign. Contract支持的注解格式默认是SpringMVC的注解
feign. Retryer失败重试机制请求失败的重试机制,默认是没有,因为feign核心依赖包括Ribbon,所以会使用Ribbon的重试

一般情况下,默认值就能满足我们使用,如果要自定义是,只需要创建自定义的@Bean覆盖默认Bean即可。

日志的级别主要分为四种:

  • NONE:不记录任何日志信息,这是默认值。
  • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间。
  • HEADERS:在BASIC的基础上,额外记录了请求和响应的头消息。
  • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。

下面以日志为例来演示如何自定义配置。

3.1 配置文件方式

基于配置文件修改feign的日志级别可以针对单个服务:

feign:  
  client:
    config: 
      userservice: # 针对某个微服务的配置
        loggerLevel: FULL #  日志级别 

order服务启动之后,发起请求可以看到,日志记录包含请求记录和响应的明细:

 配置成功。

3.2 Java代码方式

除了配置文件方式,也可以基于Java代码来修改日志级别,先声明一个类,然后声明一个Logger.Level的对象:

package cn.itcast.order.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;

public class DefaultFeignConfiguration {

    @Bean
    public Logger.Level logLevel() {
        return Logger.Level.BASIC;// 日志级别为BASIC
    }
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)

如下图:

如果要局部生效, 则把它放在对应的@FeignClient这个注解中:

@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)

如下图:

我这里用的是局部生效,清空日志,发起请求之后可以看到:

 BASIC级别比FULL级别的日志要少很多。

3.3 总结

Feign的日志配置:

1.方式一时配置文件:

feign.client.config.xxx.loggerLevel

  • 如果xxx是default则代表全局
  • 如果xxx是服务名称,例如userservice则代表某服务

2. 方式二是java代码配置Logger.Level这个Bean:

  • 如果在@EnableFeignClients注解中声明则代表全局
  • 如果在@FeignClient注解中声明则代表某个服务

4. Feign使用优化

Feign底层发起http请求,依赖于其他的框架。其底层客户端实现包括:

  • Client.Default类:默认的Feign.Bulider.client的客户端实现类,内部以HttpURLConnection来实现HTTP URL请求处理,不支持连接池
  • Apache HttpClient类:内部使用Apache httpClient开源组件完成URL请求的Feign.Bulider.client客户端实现类,支持设置连接池
  • OkHttpClient类:内部使用OkHttp3开源组件完成HHTTP URL请求的Feign.Bulider.client客户端实现类,支持设置连接池

因为默认的实现HttpURLConnection是不支持设置连接池的,所以默认情况下,Feign的性能应该是不太好,我们来做个压测试试。         

聚合报告

平均响应时间吞吐量最小响应时间最大响应时间
1788ms149.7/sec9ms8484ms

 测试工具

测试服务器:Intel(R) Celeron(R) J4105 CPU @ 1.50GHz   1.50 GHz

测试工具:JMeter 5.6.3

线程数:2000

Ramp-Up:10

PS:本文出现的所有压测结果,我至少测试了十遍,最后选取了相对平均的结果。

接下来,我们用Apache的HttpClient来演示,看看性能是否有所提升。

1)引入依赖

在order-service的pom文件中引入Apache的HttpClient依赖:

<!--httpclient依赖-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

PS:我这里的父工程管理了所有的版本依赖。所以如果你父工程没有管理的话,要加上依赖包的版本号。

2)配置连接池

在order-service的application.yml中添加配置:

feign:
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 每个路由的最大连接数

如果你没有办法确定配置是否生效,可以在FeignClientFactoryBean中的loadBalance方法中打断点,Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient:

如果你对源码感兴趣的话,可以看这篇Feig核心源码解析 ,里面说的还是比较详细的。

接下来,压测HttpClient,测试结果如下:

聚合报告

平均响应时间吞吐量最小响应时间最大响应时间
703ms153.7/sec7ms4946ms

在高并发的情况下,HttpClient比默认的HttpURLConnection性能要好一点。剩下的OkHttp,你们感兴趣的话,可以去自己测试一下。

总结

Feign的优化:

  1. 日志级别尽量用basic或none
  2. 使用HttpClient或OkHttp代替HttpURLConnection(本文以HttpClient为例)
  • 引入feign-httpClient依赖
  • 配置文件开启httpClient功能,设置连接池参数

5. Feign最佳实践分析

所谓的最佳实践,就是企业在使用一个东西的过程中,各种踩坑,最后总结出来的一个相对比较好的实现方式。

通过上面的学习,我们知道Feign主要的作用就是基于http的远程调用的客户端。在没有使用Feign之前,我们orderservice远程调用userservice时用的是,restTemplate调用服务提供者的接口(也就是服务提供者的controller),现在我们来对比一下两者: 

UserController的方法和Feign客户端,都是get方法,需要的参数,调用的路径也是一样,都是/user/{id},这么看起来两者除了方法名不一样其他的完全一样。为什么会出现这样的情况,是因为Orderservice里的Feign客户端它是发起请求的一方,而UserController它是接受请求的一方,如果两方的方法和参数不一致,服务的消费者(orderservice)就没办法访问到服务的消费者(userservice)。

但是同样的代码我们却写了两份,那么有没有一种办法能够简化这种重复的代码编写呢?

5.1 继承方式

一样的代码可以通过继承来共享:

1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明

2)Feign客户端和Controller都集成该接口

 就是给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。这样处理了之后的确变得简单了,代码也实现了共享。那它会不会有什么缺点?下面是spring官方给出的说明:

翻译过来就是:一般情况下,我们不推荐在服务和客户端之间共享接口。它引入了紧耦合,而且实际上也不适用于当前形式的SpringMVC(方法参数映射不能继承)。我来简单解释一下:

第一,紧耦合。之前我们为什么会实现微服务,是因为每个服务之间的耦合度低,每个服务都能独立开发和测试。现在服务消费者(FeignClient)和服务提供者(UserController)之间,在API层面都属于统一标准声明,将来这个接口的声明变了之后,消费者和提供者的代码都要跟着改变,对于代码的维护成本太高了。

第二,不适用于当前形式的SpringMVC(方法参数映射不能继承)。我们都知道SpringMVC在声明的时候,除了声明GetMapping之外,还要声明参数。但是官方给出的说明里面提到,方法和参数映射是没有办法继承的,也就是说,提供者(UserController)在继承了接口之后,父接口的@PathVariable还不能省略,自己得重新写一遍,还要自己定义业务逻辑,变成下面这样:

尽管上面的方法存在缺点,但是它具备了面对契约编程的思想,所以在企业中使用的还是比较多的。特别是中小型项目,不太在意耦合度,可以使用。

5.2 抽取方式

将Feign的Client抽取为独立模块,并且把接口相关的POJO、默认的Feign配置都放在这个模块中,提供给所有消费者使用。

这样说可能比较难理解,我来举个例子,现在我有一个userservice里面有一个UserController对外暴露了一个查询用户接口,这时候,我的两个微服务,orderservice和payservice都需要调用这个接口,像之前的做法就是,每个服务中都定义一个UserClient去调用,如下图:

将来我的项目越做越大,服务也越来越多,十几个服务都需要调用userservice,那么重复的UserClient也增加到了十几个,这时候就会出现重复开发和资源浪费的问题,不如就将重复的UserClient以及相关的配置,抽取出来成为一个独立的模块(feign-api),后续有其他服务想要调用的时候,直接引入依赖就行,如下图:

这样处理了之后,服务之间额耦合度就没有那么高了。但是我们做开发的都知道,没有完美的方案,所以这样的方式也是有弊端的。feign-api把所有的远程调用的接口都封装了起来,如果我的某一个服务只需要其中的一两个接口调用的方法,我现在整个模块都引入了,就有可能造成资源浪费。

上面两种方法都不完美,主要看你选择的侧重点在什么地方,如果你的项目比较在意耦合度,希望耦合度低一些,就选择方式二(抽取方式)。如果你的项目比较在契约思想,需要统一标准,那你就选择方式一(继承方式)。

5.3 总结

Feign的最佳实践:

  • 让controller和FeignClient继承统一接口
  • 将FeignClient、POJO、Feign的默认配置都定义到一个项目中,供所有消费者使用

6. Feign抽取方式的代码实现

下面,我会以Feign最佳实践中的抽取方式为例,来演示。步骤如下:

  1. 创建一个module,命名为feign-api,然后引入feign的starter依赖
  2. 将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
  3. 在order-service中引入feign-api的依赖
  4. 修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包
  5. 重启测试

1)创建一个module,命名为feign-api,然后引入feign的starter依赖

首先创建一个module,命名为feign-api:

创建之后可以在项目中看到feign-api的模块:

然后再feign-api的pom文件中引入依赖:

<dependencies>
    <!--引入openFeign依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

2)将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中

找到order-service中编写的UserClient、User、DefaultFeignConfiguration:

复制到feign-api之前,给feign-api创建一个包:

复制过去:

复制好了之后,删除order-service中相关的类:

3)在order-service中引入feign-api的依赖

在order-service的pom文件中引入feign-api的依赖:

<!--feign-api依赖-->
<dependency>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>

4)修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包

在删除order-service中的三个组件之后,就可以看到项目报错:

修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包。

5)重启测试

重启后,发现重启报错了:

我们先找到报错的这个地方,代码如下:

我们发现,这里没有报错。为什么服务启动的时候就报错了呢?熟悉Spring的开发都知道,编译的时候没有报错,说明UserClient这个类的确是存在的。无法注入成功,证明这个类没有创建对象,在Spring的容器里面找不到他。 在我们创建UserClient的时候,有一个注解是@FeignClient,只有扫描到这个注解的时候,才会把FeignClient注入到spring的容器当中,现在我们把Feign的相关信息都移到了feign-api里面,在我们启动OrderApplication的时候,有一个注解@EnableFeignClients它没有加扫描范围,所以只会扫描OrderApplication所在的包(cn.itcast.order)下面的所有文件,而UserClient所在的包是cn.itcast.feign.clients。所以没有扫描到@FeignClient注解,自然也就没办法把UserClient注入到spring容器当中。那怎么解决呢?

当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种解决方式:

方式一:指定FeignClient所在的包:

@EnableFeignClients(basePackages = "cn.itcast.feign.clients")

方式二:指定需要加载的Client接口:

@EnableFeignClients(clients = {UserClient.class})

 第一种方式是把包下面的所有FeignClient都拿过来,第二种方式是指定某一个特定的一个或几个FeignClient,这里的clients是一个数组,如果你有多个的话就加上{xxx.class,xxx.class},只有一个的话,可以直接clients=xxx.class。个人更推荐第二个。

添加完之后,重启看看。 

这里已经重启成功了,测试一下看看:

能正常调用了,没有问题了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值