• Feign Client 原理和使用


    最近一个新项目在做后端HTTP库技术选型的时候对比了Spring WebClient,Spring RestTemplate,Retrofit,Feign,Okhttp。综合考虑最终选择了上层封装比较好的Feign,尽管我们的App没有加入微服务,但是时间下来Feign用着还是很香的。

    我们的sytyale针对Feign的底层原理和源码进行了解析,最后用一个小例子总结怎么快速上手。

    本文作者:sytyale,另外一个聪明好学的同事

    一、原理

    Feign 是一个 Java 到 HTTP 的客户端绑定器,灵感来自于 RetrofitJAXRS-2.0 以及 WebSocket。Feign 的第一个目标是降低将 Denominator 无变化的绑定到 HTTP APIs 的复杂性,而不考虑 ReSTfulness

    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

    二、处理过程图

    feign client 处理流程图

    三、Http Client 依赖

    feign 在默认情况下使用 JDK 原生的 URLConnection 发送HTTP请求。(没有连接池,保持长连接) 。

    可以通过修改 client 依赖换用底层的 client,不同的 http client 对请求的支持可能有差异。具体使用示例如下:

    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 Client 配置

    • 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 方法
    }
    

    五、部分注解

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

    六、Feign Client 配置

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

    七、Spring boot 服务下使用示例

    • 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);
      	}
      
      }
      
    • 接口注解-标记请求地址、请求header、请求方式、参数(是否必填)等

      //如果是微服务内部调用则 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

  • 相关阅读:
    迁移式学习
    VMware Workstation 16激活码
    OpenStack安装部署
    git码云操作
    vs 2019 正则替换
    linux中Redis单机安装
    ASP.NET/C#执行数据库过程函数带RETURN的项目接收。
    IDEA配置部属Tomcat
    Java集合之HashMap源码分析(put()方法)
    反编译一款APP然后重新打包(Windows环境)
  • 原文地址:https://www.cnblogs.com/hackingForest/p/13173251.html
Copyright © 2020-2023  润新知