Skip to content
清晨的一缕阳光
返回

Feign 高级特性

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
    }
}

日志级别说明

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 提供了丰富的高级特性,包括超时配置、重试机制、日志记录、请求压缩等。

合理配置这些特性可以显著提升服务调用的可靠性和性能。

在生产环境中,建议配置合理的超时和重试策略,实现完善的日志记录,并优化连接池配置。


分享这篇文章到:

上一篇文章
Spring Boot 生产最佳实践
下一篇文章
Spring Boot 自动配置原理详解