文章目录
- 前言
- 一、配置文件:
- [二、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