springboot openfeign 自定义feign 接口重试机制

文章目录

  • 前言
  • 一、配置文件:
  • [二、openfeign 的重试处理](#二、openfeign 的重试处理)
    • [2.1 增加自定义注解:](#2.1 增加自定义注解:)
    • [2. 2 自定义重试策略](#2. 2 自定义重试策略)
    • [2. 3 自定义连接池配置类](#2. 3 自定义连接池配置类)
    • [2. 4 feign接口设置](#2. 4 feign接口设置)
    • [2. 5 配置熔断降级](#2. 5 配置熔断降级)
  • 总结

前言

openfeign 自带的重试机制默认不进行重试,即使我们打开重试机制,也是对所有的接口都起作用,对于支持幂等性的接口在重试时没有问题,但是对于 非幂等性的接口如 插入,更新,删除,此时开启了重试,就很有可能相同的业务被重复执行,基于此本文对openfeign的重试机制进行修改。

本文环境: idea + springboot(2.3.12.RELEASE)+springcloud(Hoxton.SR9) + jdk11 + maven(3.9.12)


一、配置文件:

增加openfeign的jar 支持

c 复制代码
   <!-- 版本控制 -->
   <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.12.RELEASE</version>
        <relativePath/>
    </parent>
    
    <groupId>com.example</groupId>
    <artifactId>springboot-feign-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-feign-demo</name>
    <description>Spring Boot OpenFeign Demo with Hystrix</description>
    <packaging>jar</packaging>
    
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
    </properties>
 <!-- jar 依赖 -->   
 <dependencies>
 <!-- Spring Cloud OpenFeign (版本由 dependencyManagement 管理) -->
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-openfeign</artifactId>
     </dependency>
     
     <!-- Spring Cloud Netflix Hystrix -->
     <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
     </dependency>
     
     <!-- OkHttp - 高性能 Feign 连接池 -->
     <dependency>
         <groupId>io.github.openfeign</groupId>
         <artifactId>feign-okhttp</artifactId>
     </dependency>
     <dependency>
         <groupId>com.squareup.okhttp3</groupId>
         <artifactId>okhttp</artifactId>
         <version>4.9.3</version>
     </dependency>
  </dependencies>

配置Hystrix 可以对feign接口熔断时 降级调用

c 复制代码
# Hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
      circuitBreaker:
        requestVolumeThreshold: 10
        errorThresholdPercentage: 50

由于openfeign 本身内部并没有配置线程池,所以我们加入 线程池配置,这样就不用每次调用接口都生成一个新的http 连接,节省资源

c 复制代码
# OkHttp 连接池配置
okhttp:
  enabled: true                   # 是否启用连接池
  connection-pool:
    max-idle-connections: 50      # 最大空闲连接数
    keep-alive-duration: 300      # 连接保持存活时间 (秒)
  read-timeout: 5000              # 读取超时 (ms)
  write-timeout: 5000             # 写入超时 (ms)
  connect-timeout: 5000           # 连接超时 (ms)

配置feign 的时间以及打开熔断器和重试机制

c 复制代码
# Feign 配置
feign:
  # 启用 OkHttp 作为 HTTP 客户端(连接池)
  httpclient:
    enabled: true
  # 熔断器配置
  hystrix:
    enabled: true
  # 重试配置
  retryer:
    enabled: true           # 启用重试(需在配置类中定义 Bean)
  client:
    config:
      default:
        connectTimeout: 5000  # 连接超时时间 (ms)
        readTimeout: 5000     # 读取超时时间 (ms)
  compression:
    request:
      enabled: true           # 启用请求压缩
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048  # 最小压缩大小
    response:
      enabled: true           # 启用响应压缩

配置log 的级别,方便观察效果

c 复制代码
# 日志配置
logging:
  level:
    com.example.demo.feign: DEBUG
    com.example.demo.fallback: DEBUG
    # Feign 客户端日志
    feign: DEBUG
    feign.Logger: DEBUG
    # Spring Cloud OpenFeign 日志
    org.springframework.cloud.openfeign: DEBUG
    # Ribbon 日志(Feign 底层使用)
    com.netflix.client: DEBUG
    # Hystrix 日志
    com.netflix.hystrix: DEBUG

二、openfeign 的重试处理

基于openfeign 不能单独为某个接口设置重试,所以进行改造,考虑以增加注解的方式来处理,即openfeign中增加了该自定义注解,那么进行重试,如果没有加入该注解则不进行重试。

实现思路为: 增加自定义注解-》项目启动加载bean-》扫描所有被标记了该注解的方法-》解析请求路径和访问的方式-》 放入到map-》重写feign 接口的重试逻辑-》 当发生重试时解析方法的请求路径和访问的方式作为key -》从map 中获取是否又被自定义注解标注-》有标准在进行重试

2.1 增加自定义注解:

Retryable 定义一个方法级别的注解

c 复制代码
package com.example.demo.config;

import java.lang.annotation.*;

/**
 * Feign 重试注解
 * 
 * 标注在 Feign Client 接口方法上,表示该方法在失败时启用重试机制
 * 未标注此注解的方法失败时直接进入 Fallback,不重试
 * 
 * 使用示例:
 * ```java
 * @FeignClient(name = "api", fallback = ApiFallback.class)
 * public interface ApiClient {
 *     
 *     // GET 请求,启用重试
 *     @Retryable
 *     @GetMapping("/data/{id}")
 *     String getData(@PathVariable("id") String id);
 *     
 *     // POST 请求,不重试
 *     @PostMapping("/data")
 *     String submitData(@RequestBody String data);
 *     
 *     // PUT 请求,但需要重试
 *     @Retryable(maxAttempts = 5)
 *     @PutMapping("/data/{id}")
 *     String updateData(@PathVariable("id") String id, @RequestBody String data);
 * }
 * ```
 * 
 * @author Mao Dan
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    
    /**
     * 最大尝试次数(包含首次请求)
     * 默认值:3 次
     * 
     * @return 最大尝试次数
     */
    int maxAttempts() default 3;
    
    /**
     * 初始重试间隔(毫秒)
     * 默认值:100ms
     * 
     * @return 初始间隔时间
     */
    int period() default 100;
    
    /**
     * 最大重试间隔(毫秒)
     * 每次重试间隔会递增,但不超过此值
     * 默认值:1000ms
     * 
     * @return 最大间隔时间
     */
    int maxPeriod() default 1000;
}

2. 2 自定义重试策略

其中continueOrPropagate 方法由于进行重试

c 复制代码
package com.example.demo.config;

import feign.Request;
import feign.RetryableException;
import feign.Retryer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ConcurrentHashMap;

/**
 * 基于注解的 Feign 重试器
 * 
 * 重试策略:
 * - 方法上有 @Retryable 注解:启用重试
 * - 方法上没有 @Retryable 注解:不重试,直接触发 Fallback
 * 
 * 工作原理:
 * 1. Feign 在解析接口时,CustomRetryableContract 会检查每个方法的@Retryable 注解
 * 2. 将方法签名和重试配置缓存到 retryableMethods 中
 * 3. 请求失败时,Retryer 通过 request 信息查找缓存,判断是否重试
 * 
 * @author Mao Dan
 */
public class AnnotationBasedRetryer implements Retryer {

    private static final Logger logger = LoggerFactory.getLogger(AnnotationBasedRetryer.class);

    /**
     * 重试方法缓存
     * Key: methodKey (格式:className#methodName(args)), Value: Retryable 注解
     */
    private static final ConcurrentHashMap<String, Retryable> retryableMethods = new ConcurrentHashMap<>();

    // 当前重试状态
    private int attempt;
    private long sleep;

    /**
     * 默认构造函数
     */
    public AnnotationBasedRetryer() {
        this.attempt = 1;
        this.sleep = 100;
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        Request request = e.request();
        
        // 从 request 中获取方法标识
        String requestKey = extractMethodKey(request);
        
        // 查找是否有@Retryable 注解(支持路径变量匹配)
        Retryable retryable = findRetryable(requestKey);
        boolean hasRetryable = retryable != null;
        
        // 记录匹配的 key(用于调试)
        String matchedKey = hasRetryable ? findMatchedKey(requestKey) : requestKey;

        logger.info("===========================================");
        logger.info("⚠️  第 {} 次请求失败", attempt);
        logger.info("请求:{} {}", request.httpMethod(), request.url());
        logger.info("请求标识:{}", requestKey);
        logger.info("匹配标识:{}", matchedKey);
        logger.info("@Retryable 注解:{}", hasRetryable ? "有 ✅" : "无 ❌");
        
        if (hasRetryable) {
            logger.info("重试配置:maxAttempts={}, period={}ms, maxPeriod={}ms",
                retryable.maxAttempts(), retryable.period(), retryable.maxPeriod());
        }
        
        logger.info("失败原因:{}", e.getMessage());

        // 没有 @Retryable 注解,直接抛出异常,不重试
        if (!hasRetryable) {
            logger.warn("⚠️  方法未标注 @Retryable 注解,跳过重试,直接触发 Fallback");
            logger.warn("===========================================");
            throw e;
        }

        // 检查是否达到最大重试次数
        if (attempt++ >= retryable.maxAttempts()) {
            logger.error("❌ 已达到最大重试次数 {} 次,将触发 Fallback 降级", retryable.maxAttempts());
            logger.error("===========================================");
            throw e;
        }

        // 执行重试
        logger.info("⏳ 等待 {}ms 后进行第 {} 次重试...", sleep, attempt);
        logger.info("===========================================");

        try {
            Thread.sleep(sleep);
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            logger.warn("重试被中断");
        }

        // 递增重试间隔
        sleep = Math.min(retryable.maxPeriod(), sleep + retryable.period());
    }

    /**
     * 提取方法标识
     * Feign 的 request 中没有直接的方法名,需要通过 URL 和 method 推断
     * 
     * @param request Feign 请求
     * @return 方法标识(格式:HTTP 方法:URL 路径)
     */
    public static String extractMethodKey(Request request) {
        String httpMethod = request.httpMethod().name();
        String url = request.url();
        
        // 从 URL 提取路径(去掉域名和端口)
        String path = extractPath(url);
        
        String methodKey = httpMethod + ":" + path;
        logger.debug("提取方法标识:{} {} -> {}", httpMethod, path, methodKey);
        
        return methodKey;
    }

    /**
     * 从完整 URL 中提取路径
     */
    public static  String extractPath(String url) {
        try {
            // 去掉协议和域名
            if (url.startsWith("http://")) {
                url = url.substring(7);
            } else if (url.startsWith("https://")) {
                url = url.substring(8);
            }
            
            // 去掉端口
            int portIndex = url.indexOf(':');
            if (portIndex != -1) {
                url = url.substring(url.indexOf('/', portIndex));
            }
            
            // 去掉查询参数
            int queryIndex = url.indexOf('?');
            if (queryIndex != -1) {
                url = url.substring(0, queryIndex);
            }
            
            return url;
        } catch (Exception e) {
            return url;
        }
    }

    /**
     * 注册可重试的方法
     * 由 FeignConfig.CustomRetryableContract 调用
     */
    public static void registerRetryableMethod(String methodKey, Retryable retryable) {
        retryableMethods.put(methodKey, retryable);
        logger.info("✅ 注册重试方法:{} -> @Retryable(maxAttempts={}, period={}ms, maxPeriod={}ms)", 
            methodKey, retryable.maxAttempts(), retryable.period(), retryable.maxPeriod());
    }

    /**
     * 查找匹配的 Retryable 注解(支持路径变量)
     * 
     * @param requestKey 请求的 key(如:GET:/api/data/123)
     * @return 匹配的 Retryable 注解,未找到返回 null
     */
    private Retryable findRetryable(String requestKey) {
        // 1. 先尝试精确匹配
        Retryable retryable = retryableMethods.get(requestKey);
        if (retryable != null) {
            logger.debug("精确匹配:{}", requestKey);
            return retryable;
        }
        
        // 2. 尝试路径变量匹配(如:GET:/api/data/{id} 匹配 GET:/api/data/123)
        for (String registeredKey : retryableMethods.keySet()) {
            if (keyMatches(registeredKey, requestKey)) {
                logger.debug("路径变量匹配:{} -> {}", requestKey, registeredKey);
                return retryableMethods.get(registeredKey);
            }
        }
        
        logger.debug("未找到匹配的@Retryable 注解:{}", requestKey);
        return null;
    }

    /**
     * 获取匹配的 key(用于日志)
     */
    private String findMatchedKey(String requestKey) {
        for (String registeredKey : retryableMethods.keySet()) {
            if (pathMatches(registeredKey, requestKey)) {
                return registeredKey;
            }
        }
        return requestKey;
    }

    /**
     * 判断两个 key 是否匹配(支持路径变量)
     * Key 格式:HTTP 方法:URL 路径(如:GET:/api/data/{id})
     * 
     * @param registeredKey 注册的 key
     * @param requestKey 请求的 key
     * @return true=匹配,false=不匹配
     */
    private boolean keyMatches(String registeredKey, String requestKey) {
        // 分割为数组
        String[] registeredParts = registeredKey.split(":");
        String[] requestParts = requestKey.split(":");
        
        // HTTP 方法必须相同
        if (registeredParts.length < 2 || requestParts.length < 2) {
            return false;
        }
        
        if (!registeredParts[0].equals(requestParts[0])) {
            return false;
        }
        
        // 比较路径
        String registeredPath = registeredParts[1];
        String requestPath = requestParts[1];
        
        return pathMatches(registeredPath, requestPath);
    }

    /**
     * 判断两个路径是否匹配(支持路径变量)
     * 
     * @param registeredPath 注册的路径(如:/api/data/{id})
     * @param requestPath 请求的路径(如:/api/data/123)
     * @return true=匹配,false=不匹配
     */
    private boolean pathMatches(String registeredPath, String requestPath) {
        String[] registeredSegments = registeredPath.split("/");
        String[] requestSegments = requestPath.split("/");
        
        // 段数必须相同
        if (registeredSegments.length != requestSegments.length) {
            return false;
        }
        
        // 逐段比较
        for (int i = 0; i < registeredSegments.length; i++) {
            String registered = registeredSegments[i];
            String request = requestSegments[i];
            
            // 空段跳过
            if (registered.isEmpty() && request.isEmpty()) {
                continue;
            }
            
            // 如果注册的是路径变量(以{开头,}结尾),则匹配
            if (registered.startsWith("{") && registered.endsWith("}")) {
                continue;
            }
            
            // 否则必须精确匹配
            if (!registered.equals(request)) {
                return false;
            }
        }
        
        return true;
    }

    /**
     * 打印所有已注册的重试方法(调试用)
     */
    public static void printRegisteredMethods() {
        logger.info("╔════════════════════════════════════════════════════════╗");
        logger.info("║  已注册的@Retryable 方法 (Key 格式:HTTP 方法:URL 路径)       ║");
        logger.info("╠════════════════════════════════════════════════════════╣");
        if (retryableMethods.isEmpty()) {
            logger.info("║  (暂无)                                              ║");
        } else {
            retryableMethods.forEach((key, retryable) -> 
                logger.info("║  {} -> maxAttempts={}, period={}ms          ", 
                    key, retryable.maxAttempts(), retryable.period())
            );
        }
        logger.info("╚════════════════════════════════════════════════════════╝");
    }

    @Override
    public Retryer clone() {
        AnnotationBasedRetryer clone = new AnnotationBasedRetryer();
        clone.attempt = this.attempt;
        clone.sleep = this.sleep;
        return clone;
    }
}

2. 3 自定义连接池配置类

c 复制代码
package com.example.demo.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * OkHttp 连接池配置属性
 * 从 application.yml 中读取 okhttp.* 配置
 * 
 * @author Mao Dan
 */
@Component
@ConfigurationProperties(prefix = "okhttp")
public class OkHttpProperties {

    /**
     * 连接池配置
     */
    private ConnectionPoolConfig connectionPool = new ConnectionPoolConfig();

    /**
     * 读取超时 (毫秒)
     */
    private int readTimeout = 5000;

    /**
     * 写入超时 (毫秒)
     */
    private int writeTimeout = 5000;

    /**
     * 连接超时 (毫秒)
     */
    private int connectTimeout = 5000;

    /**
     * 是否启用连接池
     */
    private boolean enabled = true;

    // Getters and Setters

    public ConnectionPoolConfig getConnectionPool() {
        return connectionPool;
    }

    public void setConnectionPool(ConnectionPoolConfig connectionPool) {
        this.connectionPool = connectionPool;
    }

    public int getReadTimeout() {
        return readTimeout;
    }

    public void setReadTimeout(int readTimeout) {
        this.readTimeout = readTimeout;
    }

    public int getWriteTimeout() {
        return writeTimeout;
    }

    public void setWriteTimeout(int writeTimeout) {
        this.writeTimeout = writeTimeout;
    }

    public int getConnectTimeout() {
        return connectTimeout;
    }

    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * 连接池配置
     */
    public static class ConnectionPoolConfig {
        /**
         * 最大空闲连接数
         */
        private int maxIdleConnections = 50;

        /**
         * 连接保持存活时间 (秒)
         */
        private long keepAliveDuration = 300;

        // Getters and Setters

        public int getMaxIdleConnections() {
            return maxIdleConnections;
        }

        public void setMaxIdleConnections(int maxIdleConnections) {
            this.maxIdleConnections = maxIdleConnections;
        }

        public long getKeepAliveDuration() {
            return keepAliveDuration;
        }

        public void setKeepAliveDuration(long keepAliveDuration) {
            this.keepAliveDuration = keepAliveDuration;
        }
    }
}

2. 4 feign接口设置

这里配置我们的自定义重试策略,配置okhttp 的连接池,以及扫描哪些加载哪些配置了Retryable 注解的方法

c 复制代码
package com.example.demo.config;

import feign.Client;
import feign.MethodMetadata;
import feign.RequestInterceptor;
import feign.Retryer;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

/**
 * Feign 配置类 - 基于注解的重试机制
 * 
 * 使用方式:
 * 在 Feign Client 接口方法上添加 @Retryable 注解,该方法失败时会重试
 * 未添加注解的方法失败时直接进入 Fallback,不重试
 * 
 * 示例:
 * ```java
 * @FeignClient(name = "api", fallback = ApiFallback.class)
 * public interface ApiClient {
 *     
 *     @Retryable  // 这个方法会重试
 *     @GetMapping("/data/{id}")
 *     String getData(@PathVariable("id") String id);
 *     
 *     @PostMapping("/data")  // 这个方法不重试
 *     String submitData(@RequestBody String data);
 *     
 *     @Retryable(maxAttempts = 5, period = 200)  // 自定义重试参数
 *     @PutMapping("/data/{id}")
 *     String updateData(@PathVariable("id") String id, @RequestBody String data);
 * }
 * ```
 */
@Configuration
public class FeignConfig {

    private static final Logger logger = LoggerFactory.getLogger(FeignConfig.class);

    private final OkHttpProperties okHttpProperties;

    public FeignConfig(OkHttpProperties okHttpProperties) {
        this.okHttpProperties = okHttpProperties;
    }

    /**
     * 自定义 Feign Contract - 解析@Retryable 注解
     * 使用 SpringMvcContract 支持(@GetMapping, @PostMapping 等)
     */
    @Bean
    public SpringMvcContract feignContract() {
        logger.info("配置 Feign Contract 以支持@Retryable 注解...");
        // 使用 SpringMvcContract,支持 Spring 注解
        return new CustomRetryableContract();
    }

    /**
     * 配置 Feign 请求拦截器
     */
    @Bean
    public RequestInterceptor requestInterceptor() {
        return template -> {
            // 添加通用请求头
            template.header("Content-Type", "application/json");
            template.header("User-Agent", "SpringBoot-Feign-Client");
        };
    }

    /**
     * 配置 Feign 日志级别
     */
    @Bean
    public feign.Logger.Level feignLoggerLevel() {
        return feign.Logger.Level.FULL;
    }

    /**
     * 配置 Feign 重试器 - 基于 @Retryable 注解
     */
    @Bean
    public Retryer retryer() {
        logger.info("╔════════════════════════════════════════════════════════╗");
        logger.info("║  正在配置 Feign 注解重试器...                              ║");
        logger.info("║  重试策略:仅 @Retryable 注解的方法重试                    ║");
        logger.info("║  使用方法:在 Feign Client 接口方法上添加 @Retryable 注解   ║");
        logger.info("║  Key 格式:HTTP 方法:URL 路径 (如:GET:/api/data/{id})       ║");
        logger.info("╚════════════════════════════════════════════════════════╝");
        return new AnnotationBasedRetryer();
    }

    /**
     * 配置 OkHttp 高性能连接池
     * 替代默认的 URLConnectionClient,提供连接复用、HTTP/2 支持等
     */
    @Bean
    public Client feignClient(OkHttpClient okHttpClient) {
        logger.info("╔════════════════════════════════════════════════════════╗");
        logger.info("║  正在配置 OkHttp 高性能连接池...                           ║");
        logger.info("║  特性:连接复用、HTTP/2 支持、智能路由                     ║");
        logger.info("╚════════════════════════════════════════════════════════╝");
        return new feign.okhttp.OkHttpClient(okHttpClient);
    }

    /**
     * 配置 OkHttpClient 连接池参数(从配置文件读取)
     */
    @Bean
    public OkHttpClient okHttpClient() {
        OkHttpProperties.ConnectionPoolConfig poolConfig = okHttpProperties.getConnectionPool();
        
        logger.info("╔════════════════════════════════════════════════════════╗");
        logger.info("║  正在配置 OkHttp 高性能连接池...                           ║");
        logger.info("║  最大空闲连接数:{}", poolConfig.getMaxIdleConnections());
        logger.info("║  连接保持存活时间:{} 秒", poolConfig.getKeepAliveDuration());
        logger.info("║  连接超时:{} ms", okHttpProperties.getConnectTimeout());
        logger.info("║  读取超时:{} ms", okHttpProperties.getReadTimeout());
        logger.info("║  写入超时:{} ms", okHttpProperties.getWriteTimeout());
        logger.info("║  特性:连接复用、HTTP/2 支持、智能路由                     ║");
        logger.info("╚════════════════════════════════════════════════════════╝");

        // 连接池配置(从配置文件读取)
        ConnectionPool connectionPool = new ConnectionPool(
            poolConfig.getMaxIdleConnections(),   // 最大空闲连接数
            poolConfig.getKeepAliveDuration(),    // 连接保持存活时间 (秒)
            TimeUnit.SECONDS
        );

        return new OkHttpClient.Builder()
            .connectionPool(connectionPool)
            .connectTimeout(okHttpProperties.getConnectTimeout(), TimeUnit.MILLISECONDS)  // 连接超时
            .readTimeout(okHttpProperties.getReadTimeout(), TimeUnit.MILLISECONDS)        // 读取超时
            .writeTimeout(okHttpProperties.getWriteTimeout(), TimeUnit.MILLISECONDS)      // 写入超时
            .retryOnConnectionFailure(true)               // 连接失败自动重试
            .followRedirects(true)                        // 跟随重定向
            .followSslRedirects(true)                     // 跟随 SSL 重定向
            .build();
    }

    /**
     * 打印已注册的重试方法(调试用)
     */
    public void logRegisteredRetryableMethods() {
        logger.info("╔════════════════════════════════════════════════════════╗");
        logger.info("║  已注册的@Retryable 方法列表:                              ║");
        // 由于 retryableMethods 在 AnnotationBasedRetryer 中,需要添加方法暴露
        logger.info("║  (启动后会在上方显示具体注册信息)                        ║");
        logger.info("╚════════════════════════════════════════════════════════╝");
    }

    /**
     * 自定义 Contract - 解析@Retryable 注解并注册到 Retryer
     * 继承 SpringMvcContract 以支持 @GetMapping, @PostMapping 等注解
     */
    private static class CustomRetryableContract extends SpringMvcContract {

        @Override
        public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
            MethodMetadata metadata = super.parseAndValidateMetadata(targetType, method);

            // 检查方法是否有@Retryable 注解
            Retryable retryable = method.getAnnotation(Retryable.class);
            
            if (retryable != null) {
                // 构建方法标识(与 AnnotationBasedRetryer 中的 extractMethodKey 对应)
                // 使用 Spring MVC 注解中的路径信息
                String methodKey = buildMethodKey(method);
                
                // 注册到 Retryer
                AnnotationBasedRetryer.registerRetryableMethod(methodKey, retryable);
                
                logger.info("✅ 方法 {}.{}() 标注了@Retryable 注解,启用重试",
                    targetType.getSimpleName(), method.getName());
            } else {
                logger.debug("❌ 方法 {}.{}() 未标注@Retryable 注解,不重试",
                    targetType.getSimpleName(), method.getName());
            }

            return metadata;
        }

        /**
         * 构建方法标识(与 AnnotationBasedRetryer.extractMethodKey 保持一致)
         * 格式:HTTP 方法:URL 路径
         */
        private String buildMethodKey(Method method) {
            // 从 Spring MVC 注解中提取 HTTP 方法和路径
            String httpMethod = extractHttpMethod(method);
            String path = extractPath(method);
            
            String methodKey = httpMethod + ":" + path;
            logger.info("构建方法标识:{}.{}() -> {}", 
                method.getDeclaringClass().getSimpleName(), 
                method.getName(), 
                methodKey);
            
            return methodKey;
        }

        /**
         * 从方法注解中提取 HTTP 方法
         */
        private String extractHttpMethod(Method method) {
            // 检查各种 HTTP 方法注解
            if (method.isAnnotationPresent(org.springframework.web.bind.annotation.GetMapping.class)) {
                return "GET";
            } else if (method.isAnnotationPresent(org.springframework.web.bind.annotation.PostMapping.class)) {
                return "POST";
            } else if (method.isAnnotationPresent(org.springframework.web.bind.annotation.PutMapping.class)) {
                return "PUT";
            } else if (method.isAnnotationPresent(org.springframework.web.bind.annotation.DeleteMapping.class)) {
                return "DELETE";
            } else if (method.isAnnotationPresent(org.springframework.web.bind.annotation.PatchMapping.class)) {
                return "PATCH";
            } else if (method.isAnnotationPresent(org.springframework.web.bind.annotation.RequestMapping.class)) {
                // 从@RequestMapping 中提取 method 属性
                org.springframework.web.bind.annotation.RequestMapping requestMapping = 
                    method.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
                if (requestMapping.method().length > 0) {
                    return requestMapping.method()[0].name();
                }
            }
            return "UNKNOWN";
        }

        /**
         * 从方法注解中提取 URL 路径
         */
        private String extractPath(Method method) {
            // 优先从具体注解获取路径
            org.springframework.web.bind.annotation.GetMapping getMapping = 
                method.getAnnotation(org.springframework.web.bind.annotation.GetMapping.class);
            if (getMapping != null && getMapping.value().length > 0) {
                return getMapping.value()[0];
            }

            org.springframework.web.bind.annotation.PostMapping postMapping = 
                method.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class);
            if (postMapping != null && postMapping.value().length > 0) {
                return postMapping.value()[0];
            }

            org.springframework.web.bind.annotation.PutMapping putMapping = 
                method.getAnnotation(org.springframework.web.bind.annotation.PutMapping.class);
            if (putMapping != null && putMapping.value().length > 0) {
                return putMapping.value()[0];
            }

            org.springframework.web.bind.annotation.DeleteMapping deleteMapping = 
                method.getAnnotation(org.springframework.web.bind.annotation.DeleteMapping.class);
            if (deleteMapping != null && deleteMapping.value().length > 0) {
                return deleteMapping.value()[0];
            }

            // 从@RequestMapping 获取
            org.springframework.web.bind.annotation.RequestMapping requestMapping = 
                method.getAnnotation(org.springframework.web.bind.annotation.RequestMapping.class);
            if (requestMapping != null && requestMapping.value().length > 0) {
                return requestMapping.value()[0];
            }

            return "/unknown";
        }
    }

    /**
     * 应用启动完成后,打印所有已注册的重试方法
     */
    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady() {
        AnnotationBasedRetryer.printRegisteredMethods();
    }
}

2. 5 配置熔断降级

首先需要再启动类上开启feign 接口的扫描,并且开启熔断;

c 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;

/**
 * Spring Boot 主启动类
 * 启用 Feign 客户端和 Hystrix 熔断器
 */
@SpringBootApplication
// feign 接口的扫描路径
@EnableFeignClients(basePackages = "com.example.demo.feign")
@EnableHystrix
public class SpringbootFeignDemoApplication {

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

然后在feign接口中增加 fallback 的熔断降级后的方法回调

c 复制代码
/**
 * 外部 API 的 Feign 客户端
 * 
 * 重试策略:
 * - 标注 @Retryable 的方法:失败时重试
 * - 未标注的方法:失败时直接进入 Fallback,不重试
 */
@FeignClient(
    name = "external-api",
    url = "${external.api.url:http://localhost:3000}",
    fallback = ExternalApiFallback.class,
    configuration = FeignConfig.class
)
public interface ExternalApiClient {
}

ExternalApiFallback 通过实现feign 接口完成具体降级后的业务逻辑

c 复制代码
/**
 * ExternalApiClient 的降级处理类
 * 当外部 API 调用失败时,提供快速降级响应
 */
@Component
public class ExternalApiFallback implements ExternalApiClient {

    private static final Logger logger = LoggerFactory.getLogger(ExternalApiFallback.class);
}

总结

以上为本文自定义 openfeign 重试机制的过程,完整代码 可见百度网盘:

通过网盘分享的文件:springboot-feign-demo.zip

链接: https://pan.baidu.com/s/1LcW3jguP5B1U2dcbputV_g?pwd=8i6j 提取码: 8i6j

相关推荐
白露与泡影1 小时前
2026大厂Java面试题大全!牛客网最新版
java·开发语言
Ceelog1 小时前
久坐党自救指南:屏幕前 8 小时,身体到底在经历什么
前端·后端
EntyIU2 小时前
JVM内存与GC笔记
java·jvm·笔记
XS0301063 小时前
并发编程 六
java·后端
yaoxin5211233 小时前
419. 现代 Java IO 最佳实践 - 写入文本文件
java·windows·python
雪宫街道3 小时前
synchronized 锁的范围:对象锁、类锁与代码块锁
java·jvm·后端·面试
x***r1513 小时前
linux安装 jdk-8u291-linux-x64.tar.gz 详细步骤(解压配置环境变量)
java
极光代码工作室3 小时前
基于SpringBoot的校园论坛系统
java·springboot·web开发·后端开发