# 五、http 客户端 Feign🎄

  • Feign 替代 RestTemplate
  • 自定义配置
  • Feign 使用优化
  • 最佳实践

# 5.1、RestTemplate 方式调用存在的问题🌳

先来看我们以前利用 RestTemplate 发起远程调用的代码:

// 2. 利用 RestTemplate 发起 http 请求,查询用户
// 2.1 url 路径
String url = "http://userservice/user/" + order.getUserId();
// 2.2 发送请求,实现远程调用
// 默认返回 json 数据类型,我们可以指定返回的类型为 user 对象类型
User user = restTemplate.getForObject(url, User.class);

上面存在下面的问题:

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

# 5.2、Feign 的介绍🌳

Feign 是一个声明式的 http 客户端,官方地址:https://github.com/OpenFeign/feign

其作用就是帮助我们优雅的实现 http 请求的发送,解决上面提到的问题。

image-20231006155457781

# 5.3、定义和使用 Feign 客户端🌳

使用 Feign 的步骤如下:

1、引入依赖:

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

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

@EnableFeignClients
// 如果我们创建的远程调用类不在启动类的扫描范围则可以使用下面的方式扫描远程调用类
// @EnableFeignClients(basePackages = "com.atguigu.gulimail.member.feign")
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
/*
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate()
    {
        return new RestTemplate();
    }
*/
  /*  @Bean
    public IRule randomRule()
    {
        return new RandomRule();
    }*/
}

image-20231006155927233

3、编写 Feign 客户端

声明一个远程调用,定义了一个接口叫 UserClient 这个接口将来封装的就是所有对 userservice 发起的远程调用。

在接口上添加了注解 @FeignClient (“服务名称”) 并且指定了服务名称

@FeignClient("userservice")
public interface UserClient {
    @GetMapping("user/{id}")
    User findById(@PathVariable Long id);
}

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

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

4、修改 order-service 中的 queryOrderById 的方法内容:

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserClient userClient;
    public Order queryOrderById(Long orderId) {
        // 1. 查询订单
        Order order = orderMapper.findById(orderId);
        // 2. 用 Feign 远程调用
        User user = userClient.findById(order.getUserId());
        // 3. 封装 User 对象到 Order
        order.setUser(user);
        // 4. 返回
        return order;
    }
}

这里需要注意的是 order-service 与 user-service 是否在同一个 namespace 中,否则访问就会报错 500 状态码。

报错具体信息为:

Load balancer does not have available server for client: userservice
//翻译
负载均衡器没有客户端可用的服务器:userservice

罪魁祸首

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 集群名称
        # 这个 namespace 就是罪魁祸首
        # namespace: ed32fa6f-e9e3-4483-b860-7bcd9731d0d9 # 命名空间,填 ID dev 环境
        ephemeral: true # 设置是否为临时实例 设置为非临时实例

上面的报错中可以先去 nacos 控制台看一下注册中心的情况这是个好习惯不然就是找错半天。

启动 order-service 服务和 user-service 服务访问 url:http://localhost:8080/order/101

结果:

image-20231006164032976

再将两个 user-service 启动然后刷新页面访问 10 次 url:http://localhost:8080/order/101 查看 idea 控制台的打印

8081

image-20231006165128092

8082

image-20231006165138249

8083

image-20231006165147393

都访问到了,说明我们不仅仅实现了远程调用而且还实现了负载均衡。Feign 非常的强大底层还集成了负载均衡的功能

总结

Feign 的使用步骤

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

# 5.4、自定义 Feign 配置🌳

Feign 运行自定义配置来覆盖默认配置,可以修改的配置如下:

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

一般我们需要配置的就是日志级别。

feign.Logger.Level:

NONE:没有任何日志

BASIC:发起一次 http 请求时,记录请求是什么时候发的,什么时候结束的,耗时等基本信息

HEADERS:除了请求信息还有请求头和响应体信息

FULL:完整的记录日志

# 5.4.1、配置 Feign 日志有两种方式:🌲

# 方式一🌴

方式一:配置文件方式

1、全局生效

feign:
  client:
    config:
      default: # 这里 default 就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL # 完整日志

2、局部生效

feign:
  client:
    config:
      userservice: # 这里 default 就是全局配置,如果是写服务名称,则是针对某个微服务的配置
        loggerLevel: FULL # 完整日志

启动 order-service 服务访问 utl:http://127.0.0.1:8080/order/101

image-20231007091133634

# 方式二🌴

配置 Feign 日志的方式二:java 代码方式,需要先声明一个 Bean:

public class FeignClientConfiguration {
    @Bean
    public Logger.Level feignLogLevel()
    {
        return Logger.Level.BASIC; # 日志级别
    }
}

1、而后如果是全局配置,则把它放到 @EnableFeignClients 这个注解中:

@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)

2、 如果是局部配置,则把它放到 @FeignClient 这个注解中:

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

重新启动 order-service 服务访问 utl:http://127.0.0.1:8080/order/101

image-20231007091445317

注意:如果配置文件方式与代码方式同时开启的话,它会使用配置文件的方式。

总结

Feign 的日志配置:

  1. 方式一是配置文件,feign.client.config.xxx.loggerLevel

    1. 如果 xxx 是 default 则代表全局
    2. 如果 xxx 是服务名称,例如 userservice 则代表局限于某服务
  2. 方式二是 java 代码配置 Logger.Level 这个 Bean

    1. 如果在 @EnableFeignClients 注解声明则代表全局
    2. 如果在 @FeignClient 注解中声明则代表局限于某服务
    • 注意:这两个注解一个是启动类的,一个是远程调用接口的。

# 5.5、Feign 的性能优化🌳

Feign 底层的客户端实现:

  • URLConnection:默认实现,不支持连接池
  • Apache HttpClient:支持连接池
  • OKHttp:支持连接池

因此优化 Feign 的性能主要包括:

  1. 使用连接池代替默认的 URLConnection
  2. 日志级别,最好用 basic 或 none,因为开日志也会拉低性能

# 5.5.1、Feign 的性能优化 - 连接池配置🌲

Feign 添加 HttpClient 的支持:

引入依赖:

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

配置连接池:

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

总结

Feign 的优化:

  1. 日志级别尽量用 basic
  2. 使用 HttpClient 或 OKHttp 代替 URLConnection
    1. 引入 feign-httpClient 依赖
    2. 配置文件开启 httpClient 功能,设置连接池参数

# 5.6、Feign 的最佳实践🌳

# 5.6.1、方式一🌲

方式一 (继承):给消费者的 FeignClient 和提供者的 controller 定义统一的父接口作为标准

在 UserClient 接口中使用 GetMapping 声明远程调用所需要的信息,比如请求方式,参数,路径,返回值等。

这个接口最终的目的是什么:是让消费者基于这些声明信息发送一次 http 的请求,而这个请求就会达到 userservice 服务对应的一个实例上 UserController 的 queryById。我们拿这两个做一个对比:请求方式都是 GET,请求路径 findById 是 “user/{id}”,而 queryById 是 “/{id}” 原因是 类上已经有 “/user” 了,方法名不一样不用管。参数一样。

所以两个方法对比结果是:除了方法名外,其它都一样。而这两个方法的声明是必须要一样的否则报错

order-service 基于 UserClient 访问 UserService 而 UserClient 中声明了请求方式,请求参数,请求路径等信息。那么 order-service 就基于这些信息发送 http 请求,而 UserService 恰好在接收这个请求。如果 UserClient 中的 findById 与 UserController 中的 queryById 声明的不一样,比如发送的是 post 请求而接收的是 get 请求它们就匹配不到了。

image-20231007095439949

问题:既然它们两个一模一样那么就意味着可以做一下抽取

假如定义了一个接口叫 UserAPI,现在我想定义一个 Feign 客户端接口类就不用再写了直接继承 UserAPI 就好了

还有 UserController ,直接实现 UserAPI 这个接口。就是给 Feign 客户端和 Controller 定义统一标准,它俩就可以不用再去写了。

image-20231007101108979

但是这种方式也有一个问题如下是 Spring 官网给出的一段说明:

简单理解下面的说明就是:一般情况下我们不推荐去共享接口在服务端和客户端之间,它会造成紧耦合

什么是紧耦合呢:两个微服务 userservice 和 orderservice 都已经实现相同接口了从 api 层面都已经耦合了为紧耦合,将来 UserAPI 中声明变了 UserClient 与 UserController 都要跟着变

image-20231007101211588

而且这种方案对 SpringMVC 不起作用

# 5.6.2、方式二🌲

方式二 (抽取):将 FeignClient 抽取为独立模块,并且把接口有关的 POJO,默认的 Feign 配置都放到这个模块中,提供给所有消费者使用

解释:

UserController 对外暴漏查询用户的接口,有两个微服务 order-service 和 pay-service 假如它俩都需要去查询用户,之前的方式是各写各自的 UserClient 然后都去调 UserController 查询用户接口,如果说将来微服务越来越多十几个都来调 UserController 那 UserClient 就等于写了十多遍了这样就是重复开发了,很麻烦。

image-20231007102445971

所以准备一个 feign-api (项目 - 独立模块) ,它为消费者把 Client 定义好,接口定义过程中的实体类,Feign 的配置它都管了

将来消费者要使用就引依赖就可以了,引入了后直接调用 UserController 查询用户

image-20231007102717991

总结

Feign 的最佳实践:

  1. 让 controller 和 FeignClient 继承同一接口
  2. 将 FeignClient,POJO,Feign 的默认配置都定义到一个项目中 (独立模块) 统一抽取出来打 jar 包,供所有消费者使用

# 5.6.3、演示方式二🌲

# 5.6.3.1、抽取 FeignClient🌴

实现最佳实践方式二的步骤如下

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

image-20231007105607975

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>cn.itcast.demo</groupId>
        <artifactId>cloud-demo</artifactId>
        <version>1.0</version>
    </parent>
    <artifactId>feign-api1</artifactId>
    <packaging>jar</packaging>
    <name>feign-api1</name>
    <url>http://maven.apache.org</url>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

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

image-20231007110150030

3、将 order-service 中的 config,clients,User 都删除,在 order-service 中引入 feign-api 的依赖

image-20231007110347974

<!-- 引入 feign-api-->
<dependency>
   <groupId>cn.itcast.demo</groupId>
   <artifactId>feign-api1</artifactId>
   <version>1.0</version>
</dependency>

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

image-20231007110622216

image-20231007110644248

5、重启测试

但是呢这里细心的可能已经注意到了有地方不对劲启动后也会报错!

报错为:找不到 UserClient 的 Bean

image-20231007111359869

这是为什么呢?

原因:

项目启动 SpringBoot 启动类扫描包的范围是 cn.itcast.order。而 UserClient 导入的包路径却是 cn.itcast.feign.clients.UserClient

SpringBoot 从 order 包开始扫描而导入依赖的路径从 itcast 就不一样了,有一个解决办法就是将 SpringBoot 扫描的范围扩大,但是!!!这是不合理的。不能这样

# 5.6.3.1.1、如下两种解决方案:🎋

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

方式一:指定 FeignClient 所在包 (全拿来)

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

方式二:指定 FeignClient 字节码 (精准打击,推荐使用:用哪个就指定那个)

@EnableFeignClients(clients = {UserClient.class})

使用第二种方式解决代码如下:

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.FeignClientConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = FeignClientConfiguration.class)
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

再次启动项目

image-20231007112615337

总结

不同包的 FeignClient 的导入有两种方式:

  1. 在 @EnableFeignClients 注解中添加 basePackages,指定 FeignClient 所在的包
  2. 在 @EnableFeignClients 注解中添加 clients,指定具体 FeignClient 的字节码