Feign 高级特性
超时配置
1. 连接超时和读取超时
feign:
client:
config:
default: # 全局配置
connectTimeout: 5000 # 连接超时 5 秒
readTimeout: 10000 # 读取超时 10 秒
order-service: # 指定服务配置
connectTimeout: 10000
readTimeout: 30000
2. 自定义超时配置
@Configuration
public class FeignTimeoutConfig {
@Bean
@Primary
public Request.Options requestOptions() {
// 连接超时 5 秒,读取超时 10 秒,跟随重定向
return new Request.Options(5000, 10000, true);
}
@Bean
@Qualifier("order-service")
public Request.Options orderServiceOptions() {
// 订单服务:连接超时 10 秒,读取超时 30 秒
return new Request.Options(10000, 30000, true);
}
}
3. 使用 HttpClient 配置超时
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
feign:
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
time-to-live: 60000
connection-timeout: 5000
socket-timeout: 10000
重试机制
1. 基础重试配置
@Configuration
public class FeignRetryConfig {
@Bean
public Retryer retryer() {
// 初始间隔 100ms,最大间隔 1 秒,最多重试 3 次
return new Retryer.Default(100, 1000, 3);
}
}
2. 自定义重试策略
@Component
public class CustomRetryer implements Retryer {
private static final int MAX_ATTEMPTS = 3;
private static final long PERIOD = 1000;
private int attempt;
private long sleep;
public CustomRetryer() {
this.attempt = 0;
this.sleep = PERIOD;
}
@Override
public void continueOrPropagate(RetryableException e) {
attempt++;
if (attempt >= MAX_ATTEMPTS) {
throw e;
}
try {
Thread.sleep(sleep);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw e;
}
sleep = Math.min(sleep * 2, 5000); // 指数退避,最大 5 秒
}
@Override
public Retryer clone() {
return new CustomRetryer();
}
}
3. 基于异常类型的重试
@Bean
public Retryer retryer() {
return new Retryer() {
@Override
public void continueOrPropagate(RetryableException e) {
// 只重试特定异常
if (e.cause() instanceof ConnectException ||
e.cause() instanceof SocketTimeoutException) {
// 执行重试逻辑
throw e;
}
// 其他异常不重试
throw new RetryableException(
e.request(),
e.message(),
e.httpStatus(),
e.request().httpMethod(),
e
);
}
@Override
public Retryer clone() {
return this;
}
};
}
4. 结合 Sentinel 的重试
@FeignClient(
name = "order-service",
fallback = OrderFallback.class
)
public interface OrderClient {
@GetMapping("/orders/{id}")
@SentinelResource(
value = "getOrder",
fallback = "getOrderFallback"
)
Result<Order> getOrder(@PathVariable("id") Long id);
}
@Component
public class OrderFallback implements OrderClient {
@Override
public Result<Order> getOrder(Long id) {
// 降级逻辑
return Result.fail("服务暂时不可用");
}
}
日志配置
1. 日志级别
@Configuration
public class FeignLogConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // NONE, BASIC, HEADERS, FULL
}
}
日志级别说明:
- NONE:不记录日志
- BASIC:记录请求方法、URL、响应状态码、执行时间
- HEADERS:BASIC + 请求头和响应头
- FULL:HEADERS + 请求体和响应体
2. 指定服务的日志级别
logging:
level:
com.example.client.OrderClient: DEBUG
com.example.client.UserClient: INFO
3. 自定义日志记录器
@Component
public class CustomFeignLogger implements Logger {
private static final Logger log = LoggerFactory.getLogger(CustomFeignLogger.class);
@Override
protected void log(String configKey, String format, Object... args) {
log.info(format, args);
}
@Override
protected void logException(String configKey, String format, Object... args) {
log.error(format, args);
}
@Override
protected void logRetry(String configKey, Level logLevel) {
log.warn("Retry for {}", configKey);
}
@Override
protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) {
log.info("{} - {} ({}ms)", configKey, response.status(), elapsedTime);
return response;
}
@Override
protected Request logAndRebufferRequest(String configKey, Level logLevel, Request request) {
log.info("{} - {} {}", configKey, request.httpMethod(), request.url());
return request;
}
}
4. 日志脱敏
@Component
public class SensitiveFeignLogger extends Logger {
@Override
protected Request logAndRebufferRequest(String configKey, Level logLevel, Request request) {
// 脱敏敏感信息
Request sanitizedRequest = sanitizeRequest(request);
if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {
logHeaders(sanitizedRequest.headers());
}
if (logLevel.ordinal() >= Level.FULL.ordinal()) {
logBody(configKey, sanitizedRequest.body());
}
return sanitizedRequest;
}
private Request sanitizeRequest(Request request) {
// 脱敏 Authorization 头
Map<String, Collection<String>> headers = new HashMap<>(request.headers());
if (headers.containsKey("Authorization")) {
headers.put("Authorization", Arrays.asList("Bearer ***"));
}
return Request.create(
request.httpMethod(),
request.url(),
headers,
request.body(),
request.charset(),
request.requestTemplate()
);
}
}
请求压缩
1. 启用压缩
feign:
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json
min-request-size: 2048 # 最小 2KB 才压缩
response:
enabled: true
2. 自定义压缩配置
@Configuration
public class FeignCompressionConfig {
@Bean
public RequestInterceptor compressionInterceptor() {
return template -> {
template.header("Accept-Encoding", "gzip, deflate");
template.header("Content-Encoding", "gzip");
};
}
}
错误解码器
1. 自定义错误解码器
public class CustomErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultDecoder = new Default();
@Override
public Exception decode(String methodKey, Response response) {
String body = Util.toString(response.body().asReader());
log.error("Feign 调用失败:method={}, status={}, body={}",
methodKey, response.status(), body);
switch (response.status()) {
case 400:
return new BadRequestException(body);
case 401:
return new UnauthorizedException(body);
case 403:
return new ForbiddenException(body);
case 404:
return new NotFoundException(body);
case 409:
return new ConflictException(body);
case 422:
return new ValidationException(body);
case 429:
return new RateLimitException(body);
case 500:
return new InternalServerException(body);
case 502:
case 503:
case 504:
return new ServiceUnavailableException(body);
default:
return defaultDecoder.decode(methodKey, response);
}
}
}
2. 注册错误解码器
@Configuration
public class FeignErrorConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}
3. 异常处理
@FeignClient(
name = "user-service",
configuration = FeignErrorConfig.class
)
public interface UserClient {
@GetMapping("/users/{id}")
Result<User> getUser(@PathVariable("id") Long id);
}
@Service
public class UserService {
@Autowired
private UserClient userClient;
public User getUser(Long id) {
try {
Result<User> result = userClient.getUser(id);
if (result.isSuccess()) {
return result.getData();
}
throw new BusinessException(result.getMessage());
} catch (BadRequestException e) {
log.error("参数错误", e);
throw e;
} catch (UnauthorizedException e) {
log.error("未授权", e);
throw new SecurityException("未授权访问", e);
} catch (NotFoundException e) {
log.error("资源不存在", e);
throw new ResourceNotFoundException("用户不存在", e);
} catch (ServiceUnavailableException e) {
log.error("服务不可用", e);
throw new ServiceUnavailableException("用户服务不可用", e);
} catch (Exception e) {
log.error("未知错误", e);
throw new SystemException("系统异常", e);
}
}
}
请求拦截器
1. 添加认证信息
@Component
public class AuthRequestInterceptor implements RequestInterceptor {
@Autowired
private HttpServletRequest request;
@Override
public void apply(RequestTemplate template) {
// 传递 Authorization 头
String authorization = request.getHeader("Authorization");
if (authorization != null) {
template.header("Authorization", authorization);
}
// 传递用户信息
String userId = request.getHeader("X-User-ID");
if (userId != null) {
template.header("X-User-ID", userId);
}
// 传递 Trace ID
String traceId = MDC.get("traceId");
if (traceId != null) {
template.header("X-Trace-ID", traceId);
}
}
}
2. 添加公共参数
@Component
public class CommonParamInterceptor implements RequestInterceptor {
@Value("${app.version:1.0.0}")
private String appVersion;
@Value("${app.env:prod}")
private String appEnv;
@Override
public void apply(RequestTemplate template) {
// 添加应用版本
template.query("app_version", appVersion);
// 添加环境标识
template.query("env", appEnv);
// 添加时间戳
template.query("timestamp", String.valueOf(System.currentTimeMillis()));
// 添加签名
String signature = generateSignature(template);
template.query("signature", signature);
}
private String generateSignature(RequestTemplate template) {
// 生成签名逻辑
String data = template.url() + "|" + System.currentTimeMillis();
return DigestUtils.md5Hex(data);
}
}
3. 多拦截器配置
@Configuration
public class FeignInterceptorConfig {
@Bean
@Order(1)
public RequestInterceptor authInterceptor() {
return new AuthRequestInterceptor();
}
@Bean
@Order(2)
public RequestInterceptor commonParamInterceptor() {
return new CommonParamInterceptor();
}
@Bean
@Order(3)
public RequestInterceptor traceInterceptor() {
return new TraceRequestInterceptor();
}
}
性能优化
1. 连接池配置
feign:
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
okhttp:
enabled: true
max-idle-connections: 50
keep-alive-duration: 300 # 5 分钟
2. 缓存配置
@Configuration
public class FeignCacheConfig {
@Bean
public RequestInterceptor cacheInterceptor() {
return template -> {
// 设置缓存控制头
template.header("Cache-Control", "no-cache");
template.header("If-None-Match", "*");
};
}
}
3. 异步调用
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/users/{id}")
CompletableFuture<Result<User>> getUser(@PathVariable("id") Long id);
}
@Service
public class AsyncUserService {
@Autowired
private UserClient userClient;
public CompletableFuture<User> getUserAsync(Long id) {
return userClient.getUser(id)
.thenApply(result -> {
if (result.isSuccess()) {
return result.getData();
}
throw new BusinessException(result.getMessage());
});
}
public CompletableFuture<List<User>> getUsersAsync(List<Long> ids) {
List<CompletableFuture<User>> futures = ids.stream()
.map(this::getUserAsync)
.collect(Collectors.toList());
return CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
)
.thenApply(v -> futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList())
);
}
}
测试
1. 单元测试
@SpringBootTest
@AutoConfigureWireMock(port = 0)
public class UserClientTest {
@Autowired
private UserClient userClient;
@Value("${wiremock.server.baseUrl}")
private String wireMockUrl;
@Test
public void testGetUser() {
// Mock 响应
stubFor(get(urlEqualTo("/users/1"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"code\":200,\"data\":{\"id\":1,\"name\":\"test\"}}")));
// 调用
Result<User> result = userClient.getUser(1L);
// 验证
assertNotNull(result);
assertTrue(result.isSuccess());
assertEquals("test", result.getData().getName());
}
}
2. 集成测试
@SpringBootTest
@Import(TestFeignConfig.class)
public class FeignIntegrationTest {
@Autowired
private UserClient userClient;
@Test
public void testGetUserNotFound() {
assertThrows(NotFoundException.class, () -> {
userClient.getUser(999L);
});
}
}
@Configuration
public class TestFeignConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new CustomErrorDecoder();
}
}
常见问题
1. 404 错误
问题:Feign 调用返回 404
解决方案:
feign:
client:
config:
default:
decode404: true # 将 404 交给错误解码器处理
2. 中文乱码
问题:中文参数乱码
解决方案:
@Bean
public RequestInterceptor charsetInterceptor() {
return template -> {
template.header("Content-Type", "application/json;charset=UTF-8");
};
}
3. 大文件上传超时
问题:文件上传超时
解决方案:
feign:
client:
config:
default:
readTimeout: 60000 # 60 秒
总结
OpenFeign 提供了丰富的高级特性,包括超时配置、重试机制、日志记录、请求压缩等。
合理配置这些特性可以显著提升服务调用的可靠性和性能。
在生产环境中,建议配置合理的超时和重试策略,实现完善的日志记录,并优化连接池配置。