Принцип и использование Feign Client

Spring Boot
Принцип и использование Feign Client

Недавно в новом проекте сравнивали выбор технологии серверной библиотеки HTTP.Spring WebClient,Spring RestTemplate,Retrofit,Feign,Okhttp. После всестороннего рассмотрения окончательно выбирается пакет верхнего уровня.Feign, Хотя наше приложение не присоединилось к микросервисам, но время истеклоFeignДо сих пор вкусно пахнет.

Наши системные целиFeignОсновной принцип и исходный код .

Автор статьи: сытяле, еще один умный и прилежный коллега

1. Принцип

Feign — это связыватель Java для HTTP-клиента, вдохновленныйRetrofitиJAXRS-2.0а такжеWebSocket. Первая цель Фейна — уменьшитьDenominatorНеизменные привязки к HTTP API, независимо от сложностиReSTfulness.

Feign пишет Java-клиенты для служб ReST или SOAP, используя такие инструменты, как Jersey и CXF. Кроме того, Feign позволяет вам писать собственный код поверх http-библиотек, таких как Apache HC. Feign подключает код к HTTP API с минимальными затратами и с настраиваемыми декодерами и обработкой ошибок (может писать в любые текстовые HTTP API).

Feign работает, обрабатывая аннотации в шаблонных запросах. Параметры применяются непосредственно к этим шаблонам перед выводом. Хотя Feign ограничен поддержкой текстовых API, он значительно упрощает системные аспекты, такие как воспроизведение запросов. Кроме того, Feign упрощает модульное тестирование преобразований.

Feign 10.x и выше построены на Java 8 и должны работать на Java 9, 10 и 11. Для тех, кому нужна совместимость с JDK 6, используйте Feign 9.x.

2. Схема процесса обработки

feign client 处理流程图
блок-схема обработки имитации клиента

3. Зависимость HTTP-клиента

feign по умолчанию использует собственный URLConnection JDK для отправки HTTP-запросов. (Нет пула соединений, сохраняйте долгое соединение).

Вы можете использовать базовый клиент, изменив клиентские зависимости.Разные http-клиенты могут иметь различную поддержку запросов. Конкретные примеры использования следующие:

feign: 
  httpclient:
    enable: false
  okhttp:
    enable: true

AND

<!-- Support PATCH Method-->
<dependency>    
  <groupId>org.apache.httpcomponents</groupId>    
  <artifactId>httpclient</artifactId> 
</dependency>
      
<!-- Do not support PATCH Method -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

В-четвертых, конфигурация Http-клиента

  • исходный код конфигурации okhttp
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
public class OkHttpFeignConfiguration {

 private okhttp3.OkHttpClient okHttpClient;
  
 @Bean
 @ConditionalOnMissingBean(ConnectionPool.class)
 public ConnectionPool httpClientConnectionPool(
   FeignHttpClientProperties httpClientProperties,
   OkHttpClientConnectionPoolFactory connectionPoolFactory) {
  Integer maxTotalConnections = httpClientProperties.getMaxConnections();
  Long timeToLive = httpClientProperties.getTimeToLive();
  TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
  return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
 }

 @Bean
 public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
   ConnectionPool connectionPool,
   FeignHttpClientProperties httpClientProperties) {
  Boolean followRedirects = httpClientProperties.isFollowRedirects();
  Integer connectTimeout = httpClientProperties.getConnectionTimeout();
  this.okHttpClient = httpClientFactory
    .createBuilder(httpClientProperties.isDisableSslValidation())
    .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
    .followRedirects(followRedirects).connectionPool(connectionPool).build();
  return this.okHttpClient;
 }

 @PreDestroy
 public void destroy() {
  if (this.okHttpClient != null) {
   this.okHttpClient.dispatcher().executorService().shutdown();
   this.okHttpClient.connectionPool().evictAll();
  }
 }
}
  • Исходный код конфигурации HttpClient
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CloseableHttpClient.class)
public class HttpClientFeignConfiguration {

 private final Timer connectionManagerTimer = new Timer(
   "FeignApacheHttpClientConfiguration.connectionManagerTimer", true);

 private CloseableHttpClient httpClient;

 @Autowired(required = false)
 private RegistryBuilder registryBuilder;

 @Bean
 @ConditionalOnMissingBean(HttpClientConnectionManager.class)
 public HttpClientConnectionManager connectionManager(
   ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
   FeignHttpClientProperties httpClientProperties) {
  final HttpClientConnectionManager connectionManager = connectionManagerFactory
    .newConnectionManager(httpClientProperties.isDisableSslValidation(),
      httpClientProperties.getMaxConnections(),
      httpClientProperties.getMaxConnectionsPerRoute(),
      httpClientProperties.getTimeToLive(),
      httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
  this.connectionManagerTimer.schedule(new TimerTask() {
   @Override
   public void run() {
    connectionManager.closeExpiredConnections();
   }
  }, 30000, httpClientProperties.getConnectionTimerRepeat());
  return connectionManager;
 }

 @Bean
 @ConditionalOnProperty(value = "feign.compression.response.enabled",
   havingValue = "true")
 public CloseableHttpClient customHttpClient(
   HttpClientConnectionManager httpClientConnectionManager,
   FeignHttpClientProperties httpClientProperties) {
  HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement()
    .useSystemProperties();
  this.httpClient = createClient(builder, httpClientConnectionManager,
    httpClientProperties);
  return this.httpClient;
 }

 @Bean
 @ConditionalOnProperty(value = "feign.compression.response.enabled",
   havingValue = "false", matchIfMissing = true)
 public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
   HttpClientConnectionManager httpClientConnectionManager,
   FeignHttpClientProperties httpClientProperties) {
  this.httpClient = createClient(httpClientFactory.createBuilder(),
    httpClientConnectionManager, httpClientProperties);
  return this.httpClient;
 }

 private CloseableHttpClient createClient(HttpClientBuilder builder,
   HttpClientConnectionManager httpClientConnectionManager,
   FeignHttpClientProperties httpClientProperties) {
  RequestConfig defaultRequestConfig = RequestConfig.custom()
    .setConnectTimeout(httpClientProperties.getConnectionTimeout())
    .setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
  CloseableHttpClient httpClient = builder
    .setDefaultRequestConfig(defaultRequestConfig)
    .setConnectionManager(httpClientConnectionManager).build();
  return httpClient;
 }

 @PreDestroy
 public void destroy() throws Exception {
  this.connectionManagerTimer.cancel();
  if (this.httpClient != null) {
   this.httpClient.close();
  }
 }
}
  • Свойства конфигурации HttpClient
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties {

 /**
  * Default value for disabling SSL validation.
  */
 public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false;

 /**
  * Default value for max number od connections.
  */
 public static final int DEFAULT_MAX_CONNECTIONS = 200;

 /**
  * Default value for max number od connections per route.
  */
 public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50;

 /**
  * Default value for time to live.
  */
 public static final long DEFAULT_TIME_TO_LIVE = 900L;

 /**
  * Default time to live unit.
  */
 public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS;

 /**
  * Default value for following redirects.
  */
 public static final boolean DEFAULT_FOLLOW_REDIRECTS = true;

 /**
  * Default value for connection timeout.
  */
 public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;

 /**
  * Default value for connection timer repeat.
  */
 public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000;

 private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION;

 private int maxConnections = DEFAULT_MAX_CONNECTIONS;

 private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;

 private long timeToLive = DEFAULT_TIME_TO_LIVE;

 private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;

 private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;

 private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;

 private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;

 //省略 setter 和 getter 方法
}

5. Некоторые примечания

  • Исходный код аннотации FeignClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {

  // 忽略了过时的属性
  
 /**
  * The name of the service with optional protocol prefix. Synonym for {@link #name()
  * name}. A name must be specified for all clients, whether or not a url is provided.
  * Can be specified as property key, eg: ${propertyKey}.
  * @return the name of the service with optional protocol prefix
  */
 @AliasFor("name")
 String value() default "";

 /**
  * This will be used as the bean name instead of name if present, but will not be used
  * as a service id.
  * @return bean name instead of name if present
  */
 String contextId() default "";

 /**
  * @return The service id with optional protocol prefix. Synonym for {@link #value()
  * value}.
  */
 @AliasFor("value")
 String name() default "";

 /**
  * @return the <code>@Qualifier</code> value for the feign client.
  */
 String qualifier() default "";

 /**
  * @return an absolute URL or resolvable hostname (the protocol is optional).
  */
 String url() default "";

 /**
  * @return whether 404s should be decoded instead of throwing FeignExceptions
  */
 boolean decode404() default false;

 /**
  * A custom configuration class for the feign client. Can contain override
  * <code>@Bean</code> definition for the pieces that make up the client, for instance
  * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
  *
  * @see FeignClientsConfiguration for the defaults
  * @return list of configurations for feign client
  */
 Class<?>[] configuration() default {};

 /**
  * Fallback class for the specified Feign client interface. The fallback class must
  * implement the interface annotated by this annotation and be a valid spring bean.
  * @return fallback class for the specified Feign client interface
  */
 Class<?> fallback() default void.class;

 /**
  * Define a fallback factory for the specified Feign client interface. The fallback
  * factory must produce instances of fallback classes that implement the interface
  * annotated by {@link FeignClient}. The fallback factory must be a valid spring bean.
  *
  * @see feign.hystrix.FallbackFactory for details.
  * @return fallback factory for the specified Feign client interface
  */
 Class<?> fallbackFactory() default void.class;

 /**
  * @return path prefix to be used by all method-level mappings. Can be used with or
  * without <code>@RibbonClient</code>.
  */
 String path() default "";

 /**
  * @return whether to mark the feign proxy as a primary bean. Defaults to true.
  */
 boolean primary() default true;
}

6. Конфигурация фиктивного клиента

  • Исходный код конфигурации FeignClient
 /**
  * Feign client configuration.
  */
 public static class FeignClientConfiguration {

  private Logger.Level loggerLevel;

  private Integer connectTimeout;

  private Integer readTimeout;

  private Class<Retryer> retryer;

  private Class<ErrorDecoder> errorDecoder;

  private List<Class<RequestInterceptor>> requestInterceptors;

  private Boolean decode404;

  private Class<Decoder> decoder;

  private Class<Encoder> encoder;

  private Class<Contract> contract;

  private ExceptionPropagationPolicy exceptionPropagationPolicy;

    //省略setter 和 getter
 }

7. Пример использования в службе загрузки Spring

  • Внесите зависимости в pom.xml, некоторые функции требуют дополнительных расширений зависимостей (например, отправка формы и т. д.).

    <dependencies>
      <!-- spring-cloud-starter-openfeign 支持负载均衡、重试、断路器等 -->
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        <version>2.2.2.RELEASE</version>
      </dependency>
      <!-- Required to use PATCH. feign-okhttp not support PATCH Method -->
      <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-httpclient</artifactId>
        <version>11.0</version>
      </dependency>
    </dependencies>
    
  • Включить поддержку - использоватьEnableFeignClientsаннотация

    @SpringBootApplication
    @EnableFeignClients
    public class TyaleApplication {
    
     public static void main(String[] args) {
      SpringApplication.run(TyaleApplication.class, args);
     }
    
    }
    
  • Аннотация интерфейса - пометить адрес запроса, заголовок запроса, метод запроса, параметры (необходимые) и т. д.

    //如果是微服务内部调用则 value 可以直接指定对方服务在服务发现中的服务名,不需要 url
    @FeignClient(value = "tyale", url = "${base.uri}")
    public interface TyaleFeignClient {
    
        @PostMapping(value = "/token", consumes ="application/x-www-form-urlencoded")
        Map<String, Object> obtainToken(Map<String, ?> queryParam);
      
        @GetMapping(value = Constants.STATION_URI)
        StationPage stations(@RequestHeader("Accept-Language") String acceptLanguage,
                             @RequestParam(name = "country") String country,
                             @RequestParam(name = "order") String order,
                             @RequestParam(name = "page", required = false) Integer page,
                             @RequestParam(name = "pageSize") Integer pageSize);
    
        @PostMapping(value = Constants.PAYMENT_URI)
        PaymentDTO payment(@RequestHeader("Accept-Language") String acceptLanguage,
                           @RequestBody PaymentRQ paymentRq);
    }
    
  • Поддержка FormEncoder

    @Configuration
    public class FeignFormConfiguration {
    
        @Autowired
        private ObjectFactory<HttpMessageConverters> messageConverters;
    
        @Bean
        @Primary
        public Encoder feignFormEncoder() {
            return new FormEncoder(new SpringEncoder(this.messageConverters));
        }
    }
    
  • Перехватчик - добавляется автоматическиheaderилиtokenЖдать

    @Configuration
    public class FeignInterceptor implements RequestInterceptor {
    
        @Override
        public void apply(RequestTemplate requestTemplate) {
            requestTemplate.header(Constants.TOKEN_STR, "Bearer xxx");
        }
    }
    
  • ErrorCode — можно настроить обработку кодов ответов об ошибках.

    @Configuration
    public class TyaleErrorDecoder implements ErrorDecoder {
    
        @Override
        public Exception decode(String methodKey, Response response) {
            TyaleErrorException errorException = null;
            try {
                if (response.body() != null) {
                   Charset utf8 = StandardCharsets.UTF_8;
                    var body = Util.toString(response.body().asReader(utf8));
                    errorException = GsonUtils.fromJson(body, TyaleErrorException.class);
                } else {
                    errorException = new TyaleErrorException();
                }
            } catch (IOException ignored) {
    
            }
            return errorException;
        }
    }
    
  • Пример класса TyaleErrorException — обработка данных при возврате кода ответа об отказе, разные серверы могут требовать разной обработки

    @EqualsAndHashCode(callSuper = true)
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class TyaleErrorException extends Exception {
    
        /**
         * example: "./api/{service-name}/{problem-id}"
         */
        private String type;
    
        /**
         * example: {title}
         */
        private String title;
    
        /**
         * example: https://api/docs/index.html#error-handling
         */
        private String documentation;
    
        /**
         * example: {code}
         */
        private String status;
    }
    
  • Пример использования FeignClient

    @RestController
    @RequestMapping(value = "/rest/tyale")
    public class TyaleController {
    
        @Autowired
        private TyaleFeignClient feignClient;
    
        @GetMapping(value="/stations")
        public BaseResponseDTO<StationPage> stations() {
            try {
                String acceptLanguage = "en";
                String country = "DE";
                String order = "NAME";
                Integer page = 0;
                Integer pageSize = 20;
                StationPage stationPage = feignClient.stations(acceptLanguage,
                        country, order, page, pageSize);
                return ResponseBuilder.buildSuccessRS(stationPage);
            } catch (TyaleErrorException tyaleError) {
                System.out.println(tyaleError);
                //todo 处理异常返回时的响应
            }
            return ResponseBuilder.buildSuccessRS();
        }
    }
    

Ознакомьтесь с другими статьями и подпишитесь на официальный аккаунт:лес любопытства Wechat