当前位置: 首页 > article >正文

【SpringCloud】OpenFeign

    🔥个人主页: 中草药

🔥专栏:【中间件】企业级中间件剖析


 一、OpenFeign

OpenFeign 是 Spring Cloud 生态中的声明式 HTTP 客户端工具,基于 Netflix Feign 开发。它通过接口和注解定义 HTTP 请求,简化了服务间的 RESTful 调用,并与 Ribbon(负载均衡)、Hystrix(熔断)等组件无缝集成。

快速上手

观察之前RestTemplate所写的代码

public OrderInfo selectOrderById(Integer orderId) {
  OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
  String url = "http://product-service/product/"+ orderInfo.getProductId();
  ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
  orderInfo.setProductInfo(productInfo);
  return orderInfo;
}

虽然RestTemplate对HTTP进行封装之后,已经build直接使用HTTPClient简单方便许多,但是仍然存在一些问题。

1、需要拼接URL,灵活性较高,但是封装臃肿,URL复杂时,容易出错

2、代码的可读性差,风格不统一

此处我们可以用到OpenFeign

添加依赖

<!-- Spring Cloud OpenFeign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

启用 OpenFeign

在 Spring Boot 启动类添加 @EnableFeignClients

@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

定义 Feign 客户端接口

@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
    @RequestMapping("/{productId}")
    ProductInfo getProductInfo(@PathVariable("productId") Integer productId);
}

value: 指定FeignClient的名称,用于服务发现,feign的底层会使用Spring Cloud LoadBalance进行负载均衡,也可以使用url

path:定义当前FeignClient的统一前缀

调用 Feign 客户端

@Slf4j
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private ProductApi productApi;

    public OrderInfo selectOrderById(Integer orderId) {
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
        ProductInfo productInfo = productApi.getProductInfo(orderInfo.getProductId());
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

参数传递

@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
    @RequestMapping("/{productId}")
    ProductInfo getProductInfo(@PathVariable("productId") Integer productId);

    //传递单个参数
    @RequestMapping("/p1")
    public String p1(@RequestParam("id") Integer id);

    //传递多个参数
    @RequestMapping("/p2")
    public String p2(@RequestParam("id") Integer id,@RequestParam("name") String name);

    //传递对象
    @RequestMapping("/p3")
    public String p3(@SpringQueryMap ProductInfo productInfo);

    //传递json
    @RequestMapping("/p4")
    public String p4(@RequestBody ProductInfo productInfo);
}

注意:@RequestParam 不能省略,作为参数绑定 

抽取方式

官方推荐 Feign 的使用方式为继承的方式,但企业开发中,更多是把 Feign 接口抽取为一个独立的模块(做法和继承相似,但理念不同)。
继承

创建一个新的module,并引入依赖,编写相关 interface(内容基本和原服务调用方所用的代码一样)

public interface ProductInterface {
    @RequestMapping("/{productId}")
    ProductInfo getProductInfo(@PathVariable("productId") Integer productId);

    //传递单个参数
    @RequestMapping("/p1")
    public String p1(@RequestParam("id") Integer id);

    //传递多个参数
    @RequestMapping("/p2")
    public String p2(@RequestParam("id") Integer id,@RequestParam("name") String name);

    //传递对象
    @RequestMapping("/p3")
    public String p3(@SpringQueryMap ProductInfo productInfo);

    //传递json
    @RequestMapping("/p4")
    public String p4(@RequestBody ProductInfo productInfo);
}

再通过maven打包

引入相关依赖

<dependency>
    <groupId>org.example</groupId>
    <artifactId>product-api</artifactId>4    
    <version>1.0-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

 修改服务消费方中ProductApi,ProductInfo的路径为 product-api 中的路径

并修改ProductApi的代码,继承引入jar包的ProductInterface

@FeignClient(value = "product-service",path = "/product")
public interface ProductApi extends ProductInterface {

}

 修改服务提供方Controller的代码,实现ProductInterface接口

@RestController
@RequestMapping("/product")
public class ProductController implements ProductInterface {
    @Autowired
    private ProductService productService;

    @RequestMapping("/{productId}")
    public ProductInfo getProductInfo(@PathVariable("productId") Integer productId) {
        return productService.selectProductById(productId);
    }

    //测试传递单个参数
    @RequestMapping("/p1")
    public String p1(Integer id){
        return "product-service 接受到参数 id:"+id;
    }

    //测试传递多个参数
    @RequestMapping("/p2")
    public String p2(Integer id,String name){
        return "product-service 接受到参数 id:"+id+",name:"+name;
    }

    //测试传递对象参数
    @RequestMapping("/p3")
    public String p3(ProductInfo productInfo){
        return "product-service 接受到参数 productInfo:"+productInfo;
    }

    //测试传递Json
    @RequestMapping("/p4")
    public String p4(@RequestBody ProductInfo productInfo){
        return "product-service 接受到参数 productInfo:"+productInfo;
    }
}

完成抽取 

抽取为独立模块
        将 Feign 的 Client 抽取为一个独立的模块,并把涉及到的实体类等都放在这个模块中,打成一个 Jar。服务消费方只需要依赖该 Jar 包即可。这种方式在企业中比较常见,Jar 包通常由服务提供方来实现。

创建一个新的module,并引入依赖,编写相关api

再通过maven打包

引入相关依赖

<dependency>
    <groupId>org.example</groupId>
    <artifactId>product-api</artifactId>4    
    <version>1.0-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

 修改服务消费方中ProductApi,ProductInfo的路径为 product-api 中的路径

启动类添加扫描路径

@EnableFeignClients(clients = {ProductApi.class})
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

完成抽取

服务部署

修改数据库,Nacos 等相关配置

对两个服务进行打包

Maven 打包默认是从远程仓库下载的,product-api 这个包在本地,有以下解决方案:

上传到 Maven 中央仓库 (参考:如何发布 Jar 包到 Maven 中央仓库,比较麻烦)[不推荐]

搭建 Maven 私服,上传 Jar 包到私服 [企业推荐]

从本地读取 Jar 包 [个人学习阶段推荐]

前两种方法比较复杂,咱们使用第三种方式,要先修改服务消费方的pom文件

plugin

 <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
            </plugin>
        </plugins>
</build>

 dependency

<dependency>
            <groupId>org.example</groupId>
            <artifactId>product-api</artifactId>
            <version>1.0-SNAPSHOT</version>
            <scope>system</scope>
            <systemPath>C:/Users/18254/.m2/repository/org/example/product-api/1.0-SNAPSHOT/product-api-1.0-SNAPSHOT.jar</systemPath>
</dependency>

然后完成正常的打包部署运行操作即可

二、Gateway

API网关

        API网关是分布式系统中的一个核心组件,充当客户端与后端服务之间的“中间层”。它统一处理所有外部请求,有点类似于设计模式中的Facade模式(门面模式),负责路由、协议转换、安全控制、监控等功能,类似于系统的“守门人”和“交通指挥中心”。

网关核心功能:

权限控制:作为微服务的入口,对用户进行权限校验,校验失败则拦截。

动态路由:请求先经网关,网关不处理业务,按规则转发到微服务。

负载均衡:当目标服务有多个时,进行负载均衡操作。

限流:流量过高时,按配置放行流量,避免服务压力过大 。


快速上手

创建网关项目

引入相关依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
</dependencies>

 添加Gateway的路由配置

server:
  port: 10030

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: url:8848
    gateway:
      routes:
        - id: order-service #路由规则id,随便起,不重复即可
          uri: lb://order-service/
          predicates:
            - Path=/order/**,/feign/**

        - id: product-service
          uri: lb://product-service/
          predicates:
            - Path=/product/**

即可通过网关进行访问


 Route Predicate Factories

Predicate

  Predicate 是 Java 8 引入的函数式接口,位于 java.util.function 包中。它表示一个条件判断函数,接收一个输入参数,返回布尔值(true/false)。常用于集合过滤、数据校验和条件组合等场景。

@FunctionalInterface
public interface Predicate<T> {
 boolean test(T t);
 //...
}

核心方法

方法功能说明
boolean test(T t)对输入参数 t 进行条件判断,返回布尔值。
Predicate<T> and(Predicate)逻辑  操作,组合当前谓词和另一个谓词。
Predicate<T> or(Predicate)逻辑  操作,组合当前谓词和另一个谓词。
Predicate<T> negate()逻辑  操作,返回当前谓词的反义条件。
static <T> Predicate<T> isEqual(Object)静态方法,生成一个判断对象是否相等的谓词。

代码演示

public class PredicateTest {
    @Test
    public void test() {

        //Predicate<String> predicate = new StringPredicate();

//匿名内部类
//        Predicate<String> predicate = new Predicate<String>() {
//
//            @Override
//            public boolean test(String s) {
//                return s==null || s.isEmpty();
//            }
//        };

        Predicate<String> predicate = s -> s==null || s.isEmpty();
        System.out.println(predicate.test(""));
        System.out.println(predicate.test("a"));
    }

    /**
     * negate 非
     */
    @Test
    public void test2() {
        Predicate<String> predicate = s -> s==null || s.isEmpty();
        System.out.println(predicate.negate().test(""));
        System.out.println(predicate.negate().test("a"));
    }

    /**
     * or
     */
    @Test
    public void test3() {
        Predicate<String> predicate = s -> "a".equals(s);
        Predicate<String> predicate2 = s -> "b".equals(s);
        System.out.println(predicate.or(predicate2).test("a"));
        System.out.println(predicate.or(predicate2).test("b"));
        System.out.println(predicate.or(predicate2).test("c"));
    }

    /**
     * and
     */
    @Test
    public void test4() {
        Predicate<String> predicate = s -> s!=null && !s.isEmpty();
        Predicate<String> predicate2 = s -> s!=null && s.chars().allMatch(Character::isDigit);//判断每一个字符是否都是数字类型
        System.out.println(predicate.and(predicate2).test("aa"));
        System.out.println(predicate.and(predicate2).test("123"));
    }
}

  Route Predicate Factories

Route Predicate Factories :: Spring Cloud Gateway

在 Spring Cloud Gateway 中,Route Predicate Factories 是用于定义路由规则的核心组件。它们通过条件匹配(即“断言”)决定哪些请求应该被路由到特定的下游服务。每个断言工厂生成一个 Predicate<ServerWebExchange>,用于判断当前请求是否符合路由规则。

名称说明示例
After这个工厂需要一个日期时间 (Java 的 ZonedDateTime) 对象,匹配指定日期之后的请求

predicates:

- After=2017-01-20T17:42:47.789-07:00[America/Denver]

Before匹配指定日期之前的请求

predicates:

- Before=2017-01-20T17:42:47.789-07:00[America/Denver]

Between匹配两个指定时间之间的请求,datetime2 的参数必须在 datetime1 之后

predicates:

- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

Cookie请求中包含指定 Cookie,且该 Cookie 值符合指定的正则表达式

predicates:

- Cookie=chocolate, ch.p

Header请求中包含指定 Header,且该 Header 值符合指定的正则表达式

predicates:

- Header=X-Request-Id, \d+

Host请求必须是访问某个 host (根据请求中的 Host 字段进行匹配)

predicates:

- Host=**.somehost.org,**.anotherhost.org

Method匹配指定的请求方式

predicates:

- Method=GET,POST

Path匹配指定规则的路径

predicates:

- Path=/red/{segment},/blue/{segment}

Remote Addr请求者的 IP 必须为指定范围

predicates:

- RemoteAddr=192.168.1.1/24


Gateway Filter Factories

GatewayFilter Factories :: Spring Cloud Gateway

在 Spring Cloud Gateway 中,Gateway Filter Factories 是用于处理请求和响应的核心组件。它们可以对请求进行修改(如添加请求头、重写路径)或对响应进行增强(如添加响应头、记录日志),从而实现流量控制、安全防护、数据转换等功能。过滤器分为两类:

  • Gateway Filter:作用于特定路由的过滤器。

  • Global Filter:全局过滤器,对所有路由生效。

Gateway Filter

 快速上手

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: url:8848
    gateway:
      routes:
        - id: order-service #路由规则id,随便起,不重复即可
          uri: lb://order-service/
          predicates:
            - Path=/order/**,/feign/**
            - After=2025-03-20T17:45:02.565907200+08:00[Asia/Shanghai]
          filters:
            - AddRequestParameter=userName,test

此时发送请求会自动加上参数

@Slf4j
@RestController
@RequestMapping("/feign")
public class FeignController {
    @Autowired
    private ProductApi productApi;

    @RequestMapping("/o1")
    public String o1(Integer id,String userName){
        log.info("接收到过滤器添加的参数,userName:{}",userName);
        return productApi.p1(id);
    }
}

 以下是部分常见过滤器的说明

名称说明示例
AddRequestHeader为当前请求添加 Header- AddRequestHeader=X-Request-red, blue
参数: Header 的名称及值
AddRequestParameter为当前请求添加请求参数- AddRequestParameter=red, blue
参数:参数的名称及值
AddResponseHeader为响应结果添加 Header- AddResponseHeader=X-Response-Red, Blue
参数: Header 的名称及值
RemoveRequestHeader从当前请求删除某个 Header- RemoveRequestHeader=X-Request-Foo
参数: Header 的名称
RemoveResponseHeader从响应结果删除某个 Header- RemoveResponseHeader=X-Response-Foo
参数: Header 的名称
RequestRateLimiter为当前网关的所有请求执行限流过滤,如果被限流,默认响应 HTTP 429-Too ManyRequests。默认提供了 RedisRateLimiter 的限流实现,采用令牌桶算法实现限流功能。此处不做具体介绍

filters:

  - name: RequestRateLimiter

   args:

    redis-rate-limiter.replenishRate: 10

redis-rate-limiter.burstCapacity: 20

redis-rate-limiter.requestedTokens: 1
redis-rate-limiter.replenishRate:令牌桶填充速度,即每秒钟允许多少个请求 (不丢弃任何请求)
redis-rate-limiter.burstCapacity:令牌桶容量,即每秒钟最大能够执行的请求数量 (不丢弃任何请求),将此值设置为零将阻止所有请求
redis-rate-limiter.requestedTokens:每次请求占用几个令牌,默认为 1

Retry针对不同的响应进行重试。当后端服务不可用时,网关会根据配置参数来发起重试请求

filters:

  - name: Retry

   args:

     retries: 3

     statuses: BAD_REQUEST
retries: 重试次数,默认为 3
status:HTTP 请求返回的状态码,针对指定状态码进行重试,对应 org.springframework.http.HttpStatus

RequestSize设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large

filters:

  - name: RequestSize

   args:

     maxSize: 5000000
请求包大小,单位为字节

默认过滤器添加一个 filter 并将其应用于所有路由,这个属性需要一个 filter 的列表,详细参考后续内容-

Default Filters

举例 

server:
  port: 10030

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: url:8848
    gateway:
      routes:
        - id: order-service #路由规则id,随便起,不重复即可
          uri: lb://order-service/
          predicates:
            - Path=/order/**,/feign/**
            - After=2025-03-20T17:45:02.565907200+08:00[Asia/Shanghai]
          filters:
            - AddRequestParameter=userName,test

        - id: product-service
          uri: lb://product-service/
          predicates:
            - Path=/product/**
      default-filters: #对全部生效
        - name: Retry
          args:
            retries: 3
            statuses: BAD_GATEWAY

此处配置后,如若接受到BAD_GATEWAY(502)将会重试3次

@Slf4j
@RestController
@RequestMapping("/feign")
public class FeignController {
    @Autowired
    private ProductApi productApi;

    @RequestMapping("/o1")
    public String o1(Integer id, String userName, HttpServletResponse response) {
        log.info("接收到过滤器添加的参数,userName:{}",userName);
        response.setStatus(502);
        return productApi.p1(id);
    }
}

 Global Filter

         在 Spring Cloud Gateway 中,Global Filter 是一种作用于所有路由的全局过滤器,用于统一处理请求和响应。与路由级别的过滤器(GatewayFilter)不同,全局过滤器无需绑定到特定路由,全局过滤器通常用于实现与安全性,性能监控和日志记录等相关的全局功能。

快速上手

添加依赖

 <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加配置

spring:
 cloud:
   gateway:
     metrics:
       enabled: true
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
    shutdown:
      enabled: true

测试结果

localhost:10030/actuator 显示所有监控的信息链接


  过滤器的执行顺序

        一个项目中,既有GatewayFilter,又有GlobalFilter时,在请求路由后,网关会把当前项目中的GatewayFilter和GlobalFilter合并到一个过滤器链(集合)中,并进行排序,依次执行过滤器。

        每一个过滤器都必须指定一个 int 类型的 order 值,默认值为 0,表示该过滤的优先级. order 值越小,优先级越高,执行顺序越靠前。

  • Filter 通过实现 Order 接口或者添加 @Order 注解来指定 order 值。
  • Spring Cloud Gateway 提供的 Filter 由 Spring 指定。用户也可以自定义 Filter,由用户指定。
  • 当过滤器的 order 值一样时,会按照 defaultFilter > GatewayFilter > GlobalFilter 的顺序执行。 

自定义过滤器

自定义GatewayFilter

        自定义GatewayFilter,需要去实现对应的接口GatewayFilterFactory,Spring Boot默认帮我们实现的抽象类是AbstractGatewayFilterFactory,可以直接使用

代码示例

@Data
public class CustomConfig {
    private String name;
}
@Slf4j
@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomConfig> implements Ordered {
    public CustomGatewayFilterFactory() {
        super(CustomConfig.class);
    }

    /*
    过滤器的核心方法
     */
    @Override
    public GatewayFilter apply(CustomConfig config) {
        return new GatewayFilter() {

            /**
             * exchange ServerWebExchange: HTTP请求-响应交互契约,提供了对HTTP请求和响应的访问
             * chain GatewayFilterChain:过滤器链
             * Mono 是Reactor中的核心类,数据流的发布者,Mono最多只触发一个事件,可以把Mono用在异步完成任务时,发出通知
             * chain.filter(exchange); 执行请求
             * Mono.fromRunnable() 创建一个包括Runnable元素的数据流
             */
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                //Pre类型 执行请求 Post类型

                //pre类型过滤器的代码逻辑
                log.info("Pre Filter , config:{}", config);

                return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                    log.info("Post Filter ... ");//post类型过滤器的代码逻辑
                }));
            }
        };
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

进行配置

这几个类之间的匹配关系是 

 测试结果

自定义GlobalFilter

         GlobalFilter的实现相对比较简单,不需要额外的配置,只需要实现GlobalFilter接口,自动会过滤所有的filter

代码示例

@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("pre Global Filter...");
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            log.info("post Global Filter...");
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

 无需额外配置,即可测试代码


今天应做的事没有做,明天再早也是耽误了。——裴斯泰洛齐

🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸 


http://www.kler.cn/a/596642.html

相关文章:

  • 什么是 POM 设计模式
  • 当今前沿科技:改变世界的最新技术趋势
  • VMware上调整centos终端的背景颜色
  • 机器学习开发完整流程
  • 简介S参数 .snp文件
  • 弱网测试:全链路实战、高阶策略与自动化落地
  • PTA团体程序设计天梯赛-练习集71-75题
  • Python预训练模型实现俄语音频转文字
  • 操作系统WIN11无法出现WLAN图标(解决方案)
  • spring boot 登入权限RBAC模式
  • ROS melodic 安装 python3 cv_bridge
  • Ubuntu22.04通过DKMS包安装Intel WiFi系列适配器(网卡驱动)
  • SOFABoot-06-健康检查
  • Nginx 在 Ubuntu 上的安装与配置指南
  • 蓝桥杯2024B组
  • LeetCode 91 —— 91.解码方法
  • Vue3 基础语法指南:Setup 函数详解
  • 存储过程触发器习题整理1
  • 深度解读 C 语言运算符:编程运算的核心工具
  • 信息系统运行管理员教程3--信息系统设施运维