最近一个新项目在做后端HTTP库技术选型的时候对比了Spring WebClient,Spring RestTemplate,Retrofit,Feign,Okhttp。综合考虑最终选择了上层封装比较好的Feign,尽管我们的App没有加入微服务,但是时间下来Feign用着还是很香的。
我们的sytyale针对Feign的底层原理和源码进行了解析,最后用一个小例子总结怎么快速上手。
本文作者:sytyale,另外一个聪明好学的同事
一、原理
Feign 是一个 Java 到 HTTP 的客户端绑定器,灵感来自于 Retrofit 和 [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html) 以及 [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html)。Feign 的第一个目标是降低将 [Denominator](https://github.com/Netflix/Denominator)  无变化的绑定到 HTTP APIs 的复杂性,而不考虑 [ReSTfulness](http://www.slideshare.net/adrianfcole/99problems)。
Feign 使用 Jersey 和 CXF 等工具为 ReST 或 SOAP 服务编写 java 客户端。此外,Feign 允许您在 Apache HC 等http 库之上编写自己的代码。Feign 以最小的开销将代码连接到 http APIs,并通过可定制的解码器和错误处理(可以写入任何基于文本的 http APIs)将代码连接到 http APIs。
Feign 通过将注解处理为模板化请求来工作。参数在输出之前直接应用于这些模板。尽管 Feign 仅限于支持基于文本的 APIs,但它极大地简化了系统方面,例如重放请求。此外,Feign 使得对转换进行单元测试变得简单。
Feign 10.x 及以上版本是在 Java 8上构建的,应该在 Java 9、10 和 11上工作。对于需要 JDK 6兼容性的用户,请使用 Feign 9.x
二、处理过程图
三、Http Client 依赖
feign 在默认情况下使用 JDK 原生的 URLConnection 发送HTTP请求。(没有连接池,保持长连接) 。
可以通过修改 client 依赖换用底层的 client,不同的 http client 对请求的支持可能有差异。具体使用示例如下:
feign: 
  httpclient:
    enable: false
  okhttp:
    enable: true
AND
<dependency>    
  <groupId>org.apache.httpcomponents</groupId>    
  <artifactId>httpclient</artifactId> 
</dependency>
      
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
四、Http Client 配置
@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();
		}
	}
}
@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();
		}
	}
}
@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;
	
}
五、部分注解
@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;
}
六、Feign Client 配置
	
	 * 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;
    
	}
七、Spring boot 服务下使用示例
  ```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>
  ```
  ```java
  @SpringBootApplication
  @EnableFeignClients
  public class TyaleApplication {
  
  	public static void main(String[] args) {
  		SpringApplication.run(TyaleApplication.class, args);
  	}
  
  }
  ```
  ```java
  //如果是微服务内部调用则 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);
  }
  ```
  ```java
  @Configuration
  public class FeignFormConfiguration {
  
      @Autowired
      private ObjectFactory<HttpMessageConverters> messageConverters;
  
      @Bean
      @Primary
      public Encoder feignFormEncoder() {
          return new FormEncoder(new SpringEncoder(this.messageConverters));
      }
  }
  ```
  ```java
  @Configuration
  public class FeignInterceptor implements RequestInterceptor {
  
      @Override
      public void apply(RequestTemplate requestTemplate) {
          requestTemplate.header(Constants.TOKEN_STR, "Bearer xxx");
      }
  }
  ```
  ```java
  @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;
      }
  }
  ```
  ```java
  @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;
  }
  ```
  ```java
  @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();
      }
  }
  ```
查看更多文章关注公众号:好奇心森林 
评论