概述
由来
在实际开发中,被调用方一般是将需要提供给外部的代码再次进行封装,而调用方则是根据提供的URL来进行调用开发。
在前面的组件介绍中,我们远程调用使用的是RestTemplate类中的方法。但是,使用此类来实现远程调用,我们需要在调用方进行拼接URL、封装请求等内容,并且每个程序猿的代码风格不同,这样不仅导致容易出错,还导致后期维护成本难,容易造成屎山代码。
综上所述,一种新型的组件横空出世。在调用方,它简单到像controller层调用service层一样;在被调用方,它只需一个注解就可以将被调用的服务和封装的接口绑定。这样,无论是服务提供方还是服务调用方,都大大减少了代码的开发。这个组件就是OpenFeign。
功能
- 支持SpringCloudLoadBalancer的负载均衡
 - 支持Sentinel和它的Fallback
 
代码案例
搭建商品服务
建模块

写pom文件
采用的是Nacos + OpenFeign的组件。
<?xml version="1.0" encoding="UTF-8"?>
<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>com.wbz</groupId>
        <artifactId>spring-cloud-test</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>cloud-provider-product-open-feign-8401</artifactId>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--SpringBoot通用模块-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--Druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <!--MySQL驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <!--MP-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <!--注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--配置中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--BootStrap.yml-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>
</project> 
写yml文件
使用的是bootstrap.yml文件
server:
  port: 8401
spring:
  application:
    name: cloud-provider-product-open-feign-8401
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        service: ${spring.application.name}
      config:
        prefix: ${spring.application.name}
        file-extension: yml
  profiles:
    active: dev
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/cloud_product?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/**Mapper.xml
  type-aliases-package: com.wbz.domain 
写主启动类
@MapperScan("com.wbz.mapper")
@SpringBootApplication
public class OpenFeignProductServerApplication8401 {
    public static void main(String[] args) {
        SpringApplication.run(OpenFeignProductServerApplication8401.class, args);
    }
} 
写业务类
// JavaBean
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("product_detail")
public class Product {
    @TableId
    private Long id;
    @TableField
    private String productName;
    @TableField
    private Long productPrice;
    @TableField
    private Integer state;
    @TableField
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    @TableField
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
}
// mapper接口
public interface ProduceMapper extends BaseMapper<Product> {
}
// service接口
public interface ProductService extends IService<Product> {
    Product getProductById(Long productId);
}
// service实现类
@Service
public class ProductServiceImpl extends ServiceImpl<ProduceMapper, Product> implements ProductService {
    @Override
    public Product getProductById(Long productId) {
        return this.getById(productId);
    }
}
// controller类
@RestController
@RequestMapping("/product")
public class ProductController {
    @Resource
    private ProductService productService;
    @GetMapping("/query/{productId}")
    public Product getProductById(@PathVariable Long productId) {
        return this.productService.getProductById(productId);
    }
} 
项目启动之后,在Nacos的管理界面出现改服务就表示启动成功。
搭建commons服务
建模块

写pom文件
<?xml version="1.0" encoding="UTF-8"?>
<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>com.wbz</groupId>
        <artifactId>spring-cloud-test</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>cloud-commons</artifactId>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--OpenFeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--MP-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
        </dependency>
    </dependencies>
</project> 
写业务类
// 商品类
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("product_detail")
public class Product {
    @TableId
    private Long id;
    @TableField
    private String productName;
    @TableField
    private Long productPrice;
    @TableField
    private Integer state;
    @TableField
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    @TableField
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
} 
// 订单表
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("order_detail")
public class Order {
    @TableId
    private Long id;
    @TableField
    private Long userId;
    @TableField
    private Long productId;
    @TableField
    private Integer num;
    @TableField
    private Long price;
    @TableField
    private Integer deleteFlag;
    @TableField
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;
    @TableField
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime updateTime;
    @TableField(exist = false)
    private Product product;
} 
// 远程调用的接口,服务调用方直接引入该模块的依赖,然后调用即可
@FeignClient(value = "cloud-provider-product-open-feign-8401", path = "/product")
public interface ProductFeignApi {
    @GetMapping("/query/{productId}")
    Product getProductById(@PathVariable("productId") Long productId);
} 
在cloud-commons模块搭建完成之后,在商品服务中删除实体类,然后引入该模块的依赖,观察是否够启动并调用成功。

搭建订单服务
建模块

写pom文件
<?xml version="1.0" encoding="UTF-8"?>
<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>com.wbz</groupId>
        <artifactId>spring-cloud-test</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <artifactId>cloud-consumer-order-open-feign-84</artifactId>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!--注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--配置中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--bootstrap-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <!--OpenFeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--负载均衡-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--MySQL驱动-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
        <!--MP-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
        </dependency>
        <!--cloud-commons-->
        <dependency>
            <groupId>com.wbz</groupId>
            <artifactId>cloud-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project> 
新增的pom文件有OpenFeign的依赖和cloud-commons的依赖,并且因为OpenFeign也实现了负载均衡,所以要把父子均衡的依赖也加上去。
        <!--OpenFeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--cloud-commons-->
        <dependency>
            <groupId>com.wbz</groupId>
            <artifactId>cloud-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency> 
写yml文件
server:
  port: 84
spring:
  application:
    name: cloud-consumer-order-open-feign-84
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        service: ${spring.application.name}
      config:
        prefix: ${spring.application.name}
        file-extension: yml
  profiles:
    active: dev
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/cloud_order?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&allowMultiQueries=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  mapper-locations: classpath:mapper/**Mapper.xml
  type-aliases-package: com.wbz.domain 
改主启动类
@MapperScan("com.wbz.mapper")
@SpringBootApplication
@EnableFeignClients // 服务调用
public class OpenFeignOrderConsumerApplication84 {
    public static void main(String[] args) {
        SpringApplication.run(OpenFeignOrderConsumerApplication84.class, args);
    }
} 
新增一个@EnableFeignClients注解用来开启服务调用。
写业务类
// mapper接口
public interface OrderMapper extends BaseMapper<Order> {
}
// service接口
public interface OrderService extends IService<Order> {
    Order getOrderById(Integer id);
}
// service实现类
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    @Resource
    private ProductFeignApi productFeignApi;
    @Override
    public Order getOrderById(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 远程调用
        Product product = this.productFeignApi.getProductById(order.getProductId());
        order.setProduct(product);
        // 返回结果
        return order;
    }
}
// controller实现类
@RestController
@RequestMapping("/order")
public class OrderController {
    @Resource
    private OrderService orderService;
    @GetMapping("/query/{id}")
    public Order getOrderById(@PathVariable Integer id) {
        return this.orderService.getOrderById(id);
    }
} 
在上述的service实现类中可以看到,远程调用真的就像层与层之间的调用一样简单。
启动服务之后,输入127.0.0.1:84/product/query/1,出现下述结果就算成功:

 总结
 
到这里,使用OpenFeign进行远程调用的服务就搭建完成了。很容易能够看出,OpenFeign使用一行代码就实现了远程调用,所以相较于RestTemplate来说,更见简单方便。
在使用OpenFeign组件时,只需要引入对应依赖,然后在调用方使用@EnableFeignClients的注解,以及在客户端使用@FeignClient就能轻松解决问题。
OpenFeign参数传递
在OpenFeign的客户端接口中,进行参数绑定时不能省略,省略之后就会报错。
商品服务
@RestController
@RequestMapping("/product")
public class ProductController {
    @Resource
    private ProductService productService;
    @GetMapping("/query/{productId}")
    public Product getProductById(@PathVariable Long productId) {
        return this.productService.getProductById(productId);
    }
    @GetMapping("/test1")
    public String test1(Long productId) {
        return "商品服务 - 测试传递单个参数成功";
    }
    @GetMapping("/test2")
    public String test2(Long productId, Long id) {
        return "商品服务 - 测试传递多个参数成功";
    }
    @GetMapping("/test3")
    public String test3(Order order) {
        return "商品服务 - 测试传递对象成功";
    }
    @PostMapping("/test4")
    public String test4(@RequestBody Order order) {
        return "商品服务 - 测试传递json成功";
    }
} 
cloud-commons
// 在OpenFeign中进行参数绑定时,不能省略。
@FeignClient(value = "cloud-provider-product-open-feign-8401", path = "/product")
public interface ProductFeignApi {
    // 传递URL上的参数
    @GetMapping("/query/{productId}")
    Product getProductById(@PathVariable("productId") Long productId);
    // 传递单个参数
    @GetMapping("/test1")
    String test1(@RequestParam("productId") Long productId);
    // 传递多个参数
    @GetMapping("/test2")
    String test2(@RequestParam("productId") Long productId,
                 @RequestParam("id") Long id);
    // 传递对象
    @GetMapping("/test3")
    String test3(@SpringQueryMap Order order);
    // 传递json
    @PostMapping("/test4")
    String test4(@RequestBody Order order);
} 
订单服务
// controller类
@RestController
@RequestMapping("/order")
public class OrderController {
    @Resource
    private OrderService orderService;
    @GetMapping("/query/{id}")
    public Order getOrderById(@PathVariable Integer id) {
        return this.orderService.getOrderById(id);
    }
    @GetMapping("/test1/{id}")
    public String test1(@PathVariable Integer id) {
        return this.orderService.test1(id);
    }
    @GetMapping("/test2/{id}")
    public String test2(@PathVariable Integer id) {
        return this.orderService.test2(id);
    }
    @GetMapping("/test3/{id}")
    public String test3(@PathVariable Integer id) {
        return this.orderService.test3(id);
    }
    @GetMapping("/test4/{id}")
    public String test4(@PathVariable Integer id) {
        return this.orderService.test4(id);
    }
}
// serive接口
public interface OrderService extends IService<Order> {
    Order getOrderById(Integer id);
    String test1(@PathVariable Integer id);
    String test2(@PathVariable Integer id);
    String test3(@PathVariable Integer id);
    String test4(@PathVariable Integer id);
}
// service实现类
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
    @Resource
    private ProductFeignApi productFeignApi;
    @Override
    public Order getOrderById(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 远程调用
        Product product = this.productFeignApi.getProductById(order.getProductId());
        order.setProduct(product);
        // 返回结果
        return order;
    }
    @Override
    public String test1(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 远程调用
        return this.productFeignApi.test1(order.getProductId());
    }
    @Override
    public String test2(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 远程调用
        return this.productFeignApi.test2(order.getProductId(), order.getId());
    }
    @Override
    public String test3(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 远程调用
        return this.productFeignApi.test3(order);
    }
    @Override
    public String test4(Integer id) {
        // 获取订单
        Order order = this.getById(id);
        // 远程调用
        return this.productFeignApi.test4(order);
    }
} 
高级特性
大致原理
在OpenFeign组件中,主要使用的两个注解就是在启动类上的@EnableFeignClients注解以及在客户端接口上的@FeignClient注解。
首先,主启动类上的@EnableFeignClients注解开启对Feign接口代理对象的构建以及装配。在这个注解中,导入了一个FeignClientsRegistrar的类,这个类会扫描添加了FeignClient注解的接口,并且创建远程调用的对象,然后将该对象注入到Spring容器中。这样,在我们进行远程调用的代码就可以直接进行注入,并且对于注入的内容来说,会生产一个RequestTemplate的请求模板实例,在其中存储了请求路径、请求参数等内容。在请求调用时,会生成一个Request请求实例,然后根据Feign.Client的负载均衡实例,选择合适的服务进行调用。
这只是大概的过程,并且我也没有翻阅源码,在网上找的一段教程来看从而给出的方法。
@EnableFeignClients

basePackages表示扫描指定的包;
basePackageClasses表示扫描指定的类或接口对应的包;
defaultConfiguration表示自定义的FeignClient配置;
clients表示扫描指定的接口,配置之后,不会根据类路径进行扫描。
这几个定义扫描的都是用来查找FeignClient接口所在的位置,原因是因为如果cloud-commons的包结构和订单服务的包结构不相同的话,那么就无法进行扫描,所以就得自己配置扫描路径。
@FeignClient

value是用来定义服务名称,当多实例部署时,需要名称去拉取服务列表,然后再进行负载均衡,从而找到合适的服务。
url是用来给出服务地址,当只有一个服务时,直接给出URL,然后就可以进行调用。
fallback和fallbackFactory都是用来进行服务降级等功能的,后续在Sentinel中会进行介绍。
path是用来表示统一前缀的,例如都是商品服务中一个类下都是以product为前缀的,就可以直接放在path中,减少代码。
超时处理
在SpringCloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会出现问题的,但是如果业务比较复杂,服务间要进行比较繁杂的业务计算,那后台很有可能会出现ReadException这个依次,因此就要定制化配置超时时间。
测试:首先在商品服务让其睡眠10秒,然后在订单服务中配置超时时间为3秒:

spring:
  application:
    name: cloud-consumer-order-open-feign-84
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        service: ${spring.application.name}
      config:
        prefix: ${spring.application.name}
        file-extension: yml
    openfeign:
      client:
        config:
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000 
项目启动之后,输入127.0.0.1:84/query/product/1,等待一段时间发现如下报错:

官方默认的等待时间为60秒钟,服务端超过规定时间就会导致Feign客户端返回报错。为了避免这样的情况,我们就需要设置Feign客户端的超时控制。
我们不仅可以控制所有的超时时间,还可以针对不同的服务来设置不同的超时时间:
spring:
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
      discovery:
        service: ${spring.application.name}
      config:
        prefix: ${spring.application.name}
        file-extension: yml
    openfeign:
      client:
        config:
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
          # 配置商品服务的超时时间,会覆盖全局的默认时间
          cloud-provider-product-open-feign-8401:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000 
重试机制
OpenFeign默认没有开重试机制,可以通过配置类开启:
@Configuration
public class FeignConfig {
    @Bean
    public Retryer retryer() {
        // 初识间隔时间为100毫秒
        // 重试间最大间隔时间为1秒
        // 最大请求次数为3次
        return new Retryer.Default(100, 1, 3);
    }
} 
为了测试,将订单服务中的代码进行如下修改:

项目启动之后,产生的日志为:

通过日志可以判断得出,我们添加的重试机制生效了。
请求/响应压缩
OpenFeign支持对请求/响应进行GZIP压缩,以减少通信过程中的性能损耗。
通过如下配置就可以实现相对应的压缩功能,并且还对请求压缩做了一些更细致的设置,比如下面的指定压缩数据类型以及指定触发压缩的大小。
spring:
    openfeign:
      client:
        config:
          default:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
          # 配置商品服务的超时时间,会覆盖全局的默认时间
          cloud-provider-product-open-feign-8401:
            #连接超时时间
            connectTimeout: 3000
            #读取超时时间
            readTimeout: 3000
      compression: # 压缩
        request:
          enabled: true
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true 
日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。说白了就是对Feign接口的调用情况进行监控和输出。
Feign的日志级别有四种:
- NONE:默认的,不显示任何日志;
 - BASIC:仅记录请求方法、URL、响应状态码及执行时间;
 - HEADERS:包含上述信息以及请求和响应的头信息;
 - FULL:包含上述信息以及请求和响应的正文及元数据。
 
想要开启日志打印功能,首先要进行配置类的配置,然后再写配置文件:
@Configuration
public class FeignConfig {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
} 
logging:
  level:
    com:
      wbz:
        api:
          ProductFeignApi: debug









