『OpenFeign』使用与配置篇

4,961 阅读4分钟

OpenFeign 简介

  Feign 是一个声明式 HTTP 客户端。

  使用Feign,需要创建一个接口并加上注解。它具有可插入的注释支持,包括外部注释和JAX-RS注释。

  Feign 还支持可插入编码器和解码器。Spring Cloud Open Feign 还支持 Spring MVC 注释、Spring Web 中默认使用的HttpMessageConverters。

  Spring Cloud Open Feign 还集成了 Ribbon(一种负载均衡器) 和 Eureka(一种注册中心)、Spring Cloud CircuitBreaker(一种断路器) 以及Spring Cloud LoadBalancer(一种负载均衡器),以在使用Feign时提供负载平衡的http客户端。

Feign 基础使用

  POM 坐标

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

  启动类

@SpringBootApplication
// 使用注解开启 Feign
// 如果依赖了第三方的Feign客户端,需要在此配置扫描地址。
@EnableFeignClients
public class Application {

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

}

  使用示例

@FeignClient("stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    Page<Store> getStores(Pageable pageable);

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);
}

  如果你的应用程序,已经接入了 注册中心,那么此时就已经可以发起调用了。

  调用 StoreClient 的 getStores() 方法,即会发出 Http 请求到 stores 服务的GET /stores路由上。

Feign 的构成 与 可配置项

覆盖 Feign 默认配置

单个 Feign 配置方式

  • Java Config 方式
@FeignClient(name = "goods",configuration = SkuFeignConfiguration.class)
public interface SkuServiceClient extends SkuService {
}
  • yml 配置方式
feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
       defaultQueryParameters:
          query: queryValue
        defaultRequestHeaders:
          header: headerValue
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

全局 Feign 配置方式

  • Java Config 方式

    使 SkuFeignConfiguration 被 Spring Boot 上下文加载,即成为 Feign 的全局配置。

  • Yml 配置方式

    feign:
      client:
        config:
          default:
            xxxxxxxx: xxxx
    

Feign 构成 与 可配置项

  一个 Feign Client 通常由三个主要部分构成:

  • Decoder- 解码器
  • Encoder - 编码器
  • Contract - 处理 Feign 注解的相关逻辑。

  除上述的三个主要组件之外,还有一些可选的组件:

  • Logger - 控制 Feign 的日志实现以及日志级别

    开启了 Debug 级别的日志如下:

    [SkuServiceClient#getSkuById] ---> POST http://goods/getskuById HTTP/1.1
    [SkuServiceClient#getSkuById] Content-Length: 8
    [SkuServiceClient#getSkuById] Content-Type: application/json
    [SkuServiceClient#getSkuById] traceId: 751853599039471616
    [SkuServiceClient#getSkuById] 
    [SkuServiceClient#getSkuById] {"id":1}
    [SkuServiceClient#getSkuById] ---> END HTTP (8-byte body)
    [SkuServiceClient#getSkuById] <--- HTTP/1.1 200 (768ms)
    [SkuServiceClient#getSkuById] connection: keep-alive
    [SkuServiceClient#getSkuById] content-type: application/json
    [SkuServiceClient#getSkuById] date: Tue, 02 Aug 2022 06:39:53 GMT
    [SkuServiceClient#getSkuById] keep-alive: timeout=60
    [SkuServiceClient#getSkuById] transfer-encoding: chunked
    [SkuServiceClient#getSkuById] 
    [SkuServiceClient#getSkuById] {"code":"000010","msg":"商品不存在。","data":null,"success":false}
    [SkuServiceClient#getSkuById] <--- END HTTP (72-byte body)
    
  • Retryer - 重试器

    默认关闭。

    如果开启的话,在抛出 IOExceptions 或 ErrorDecoder 抛出 RetryableException 时进行重试。

    Feign 提供的重试器: feign.Retryer.Default。也可自己实现Retryer 接口。

  • ErrorDecoder - 请求错误时的异常解码器

    当 Response.status() 不等于 2xx ,会进入此异常解码器。

    如果需要重试的,则包装为 RetryableException 重新抛出。

    通常可以在这里,把外部错误封装为自己项目中的统一异常类。

    默认实现:

    @Override
    public Exception decode(String methodKey, Response response) {
      FeignException exception = errorStatus(methodKey, response);
      Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
      if (retryAfter != null) {
        return new RetryableException(
            response.status(),
            exception.getMessage(),
            response.request().httpMethod(),
            exception,
            retryAfter,
            response.request());
      }
      return exception;
    }
    
  • Request.Options - 当前 Feign Client 下的 Http 请求可选项。

    • connectTimeout / connectTimeoutUnit - 连接超时时间
    • readTimeout / readTimeoutUnit - 请求超时时间
    • followRedirects - 是否可重定向
  • Collection - 请求拦截器

    这里可以用来透传一些参数,例如用户Id、链路Id等等。

  • SetterFactory - 用于控制 hystrix 命令的属性。

  • QueryMapEncoder - URL 上携带参数的编码器。

    使用方式:

    @Data
    public class Params {
        private String param1;
        private String param2;
    }
    
    @FeignClient("demo")
    public interface DemoTemplate {
        @GetMapping(path = "/demo")
        String demoEndpoint(@SpringQueryMap Params params);
    }
    

    一般情况下,不推荐这么写 Http 接口,所以少用甚至别用这个注解。

切换 Http 请求工具

  Feign 支持使用 OkHttpClient 和 ApacheHttpClient 以及 ApacheHC5 三种 Http 请求工具。通过设置feign.okhttp.enabled 或者 feign.httpclient.enabled 或者feign.httpclient.hc5.enabledtrue,来启用。

  您可以通过提供 org.apache.http.impl.client.CloseableHttpClient 的 实现类 Bean 来定制使用的HTTP客户端。

  不过一般没有必要定制,最多调整一下超时时间之类的属性。

手动创建 Feign Client

@Import(FeignClientsConfiguration.class)
class FooController {

    private FooClient fooClient;

    private FooClient adminClient;

    @Autowired
    public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
        this.fooClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                .requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
                .target(FooClient.class, "https://PROD-SVC");

        this.adminClient = Feign.builder().client(client)
                .encoder(encoder)
                .decoder(decoder)
                .contract(contract)
                .requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
                .target(FooClient.class, "https://PROD-SVC");
    }
}

Feign 集成熔断器

Feign 与 Hystrix

  尽管 Hystrix 已经停止新功能迭代,但是存量使用者还是海量的。

  • 启用 Hystrix

    feign.hystrix.enabled=true

    启用后,Hystrix 包装所有方法。

  • 配置 Feign 与 Hystrix

    当启动了 Hystrix 时,需要配置 Hystrix 的超时时间,并且 Hystrix 超时时间应该大于 Feign 的超时时间。

    因为 Hystrix 的默认超时时间为 1S,可能会导致 Hystrix 先抛出超时异常。

    feign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000
      hystrix:
        enabled: true
    
    hystrix:
      command:
        default:
          execution:
            timeout:
              enabled: true
            isolation:
              thread:
                timeoutInMilliseconds: 60000
    
  • 配置熔断 Fallbacks

    使用方式 1:

    @FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
    protected interface HystrixClient {
        @RequestMapping(method = RequestMethod.GET, value = "/hello")
        Hello iFailSometimes();
    }
    
    @Component
    static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
        @Override
        // 这里可以获取到导致熔断的原因。
        public HystrixClient create(Throwable cause) {
            return new HystrixClient() {
                @Override
                public Hello iFailSometimes() {
                    return new Hello("fallback; reason was: " + cause.getMessage());
                }
            };
        }
    }
    

    使用方式 2:

    @FeignClient(name = "hello", fallback = HystrixClientFallback.class)
    protected interface HystrixClient {
        @RequestMapping(method = RequestMethod.GET, value = "/hello")
        Hello iFailSometimes();
    }
    
    static class HystrixClientFallback implements HystrixClient {
        @Override
        public Hello iFailSometimes() {
            return new Hello("fallback");
        }
    }
    

Feign 与 CircuitBreaker

  • 启用 CircuitBreaker

    feign.circuitbreaker.enabled=true

    启用后,circuitbreaker 将包装所有方法。

  • 配置熔断

    同 Hystrix

Feign 扩展点示例

优先级

  通过设置优先级,我们可以在本地 Mock Feign 调用。这在本地开发时,比较方便。

@FeignClient(name = "goods",configuration = SkuFeignConfiguration.class,primary = false)
public interface SkuServiceClient extends SkuService {
}

@Component
@ConditionalOnProperty(value = "spring.profiles.active",havingValue = "dev")
public class SkuServiceClientMock implements SkuService {
  // mock 一些方法的实现
}

单继承

  我们可以将 UserService 暴露给第三方,UserResource 作为 Feign 实现,UserClient 可以作为项目内部的 Feign Client 使用。

public interface UserService {
    @RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
    User getUser(@PathVariable("id") long id);
}

@RestController
public class UserResource implements UserService {

}

@FeignClient("users")
public interface UserClient extends UserService {

}

基于 RequestInterceptor 透传链路Id示例

@Component
public class FeignTraceInterceptor implements RequestInterceptor {

    public FeignTraceInterceptor() {}

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header(TRACE_ID, ThreadMdcUtil.getTraceId());
    }
}

基于 ErrorDecoder 封装异常示例

Slf4j
public class SkuFeignConfiguration {

    @Bean
    public ErrorDecoder errorDecoder() {
        return (methodKey, response) -> {
            Exception exception = new GlobalException(ResultCommonEnum.REMOTE_SERVICE_EXCEPTION.code(), "");
            try {
                // 获取原始的返回内容
                String respBody = Util.toString(response.body().asReader());
                log.error("FeignError:{}", respBody);
                if (JSONUtil.isJsonObj(respBody)) {
                    ResultVO resultVO = JSON.parseObject(respBody, ResultVO.class);
                    if (Objects.nonNull(resultVO)) {
                        if (Objects.nonNull(resultVO.getCode()) && !resultVO.isSuccess()) {
                            exception = new GlobalException(resultVO.getCode(), resultVO.getMsg());
                        }
                    }
                }
            } catch (IOException ex) {
                log.error(ex.getMessage(), ex);
            }
            return exception;
        };
    }
}