【面向过程】feign是什么?在springcloud中怎么用?

什么是feign?

Feign is a Java to HTTP client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.
Feign 是一个 Java 到 HTTP 客户端绑定器,其灵感来自于 Retrofit、JAXRS-2.0 和 WebSocket。 Feign 的第一个目标是降低将 Denominator 统一绑定到 HTTP API 的复杂性,无论 ReSTativity 如何。

feign能做什么?

feign的项目github仓库上这样描述

Feign works by processing annotations into a templatized request. Arguments are applied to these templates in a straightforward fashion before output. Although Feign is limited to supporting text-based APIs, it dramatically simplifies system aspects such as replaying requests. Furthermore, Feign makes it easy to unit test your conversions knowing this.
Feign 的工作原理是将注释处理为模板化请求。 在输出之前,参数会以简单的方式应用于这些模板。 尽管 Feign 仅限于支持基于文本的 API,但它极大地简化了系统方面,例如重放请求。 此外,知道这一点后,Feign 可以轻松地对您的转换进行单元测试。

总结一下,就是代理http请求,让访问网络api(http\grpc等)像访问内存中的代码方法块一样便捷,把复杂的请求前后处理过程交给feign

feign 的图谱

图片引用来自feign github项目 url:github.com/OpenFeign/f...

contracts

  • contracts 的中文释义是合同顾名思义就是feign所遵守的协议

clients

  • 执行http请求的客户端实现类,默认使用的是HttpURLConnection
  • 可以选择替代的 Apache HTTP、JDK11的 HTTP2等

async clients

  • 异步请求客户端,可选Apache HC5、OKHttp等

extras

  • 附加组件,提供了对hytrix(熔断器)、slf4j(日志)、mock等的支持

metrics

  • 度量工具,提供对Dropwizard 、Micrometer 检测工具的支持

encoders/decoders

  • 序列化反序列化支持

feign提供的方法注解

  • Feign 注释定义了接口之间的契约以及底层客户端应该如何工作。 Feign的默认合约定义了以下注解:
Annotation Interface Target Usage 中文
@RequestLine Method Defines the HttpMethod and UriTemplate for request. Expressions, values wrapped in curly-braces {expression} are resolved using their corresponding @Param annotated parameters. 定义请求的 HttpMethod 和 UriTemplate。 表达式、包含在大括号 {expression} 中的值使用其相应的 @Param 注解参数进行解析。
@Param Parameter Defines a template variable, whose value will be used to resolve the corresponding template Expression, by name provided as annotation value. If value is missing it will try to get the name from bytecode method parameter name (if the code was compiled with -parameters flag). 定义一个模板变量,其值将用于解析相应的模板表达式,通过作为注释值提供的名称。 如果值丢失,它将尝试从字节码方法参数名称中获取名称(如果代码是使用 -parameters 标志编译的)。
@Headers Method, Type Defines a HeaderTemplate; a variation on a UriTemplate. that uses @Param annotated values to resolve the corresponding Expressions. When used on a Type, the template will be applied to every request. When used on a Method, the template will apply only to the annotated method. 定义名称-值对(或 POJO)的 Map,以扩展为查询字符串。
@QueryMap Parameter Defines a Map of name-value pairs, or POJO, to expand into a query string. 定义一个 HeaderTemplate; UriTemplate 的变体。 使用@Param注释值来解析相应的表达式。 当用于类型时,模板将应用于每个请求。 当用于方法时,模板将仅应用于带注释的方法。
@HeaderMap Parameter Defines a Map of name-value pairs, to expand into Http Headers 定义名称-值对的映射,以扩展为 Http 标头
@Body Method Defines a Template, similar to a UriTemplate and HeaderTemplate, that uses @Param annotated values to resolve the corresponding Expressions. 定义一个模板,类似于 UriTemplate 和 HeaderTemplate,它使用 @Param 注解的值来解析相应的表达式。

特殊的

如果需要访问不同的主机,可以这样使用 @RequestLine 注解

java 复制代码
import java.net.URI;

/**
* host {@link java.net.URI} feign将使用该主机
*/
@RequestLine("POST /{owner}/api")
void api(URI host, @Param("param") String param,);

默认情况下,RequestLine 模板不对斜杠/字符进行编码。 要更改此行为,请将@RequestLine 上的decodeSlash 属性设置为false。

java 复制代码
@RequestLine(value = "GET /api/{param}",decodeSlash = false)
public Response api(@Param("param")String param){
    return new Response();
}

URL中的 + 在某些遗留系统中,+ 相当于空格。 Feign 采用现代系统的方法,其中 + 符号不应代表空格,并且在查询字符串中找到时显式编码为 %2B。如果需要使用空格,需要使用 %20 表示

以上介绍来自于 feign的仓库介绍页

使用他是怎么样的一个过程?

我们来看下feign-core包中的代码执行过程

先拿到bean

以 Spring Cloud OpenFeign 为例子(verion为当前spring官网最新版本 V4.0.4)

从spring的官网中示例可以看到,他是这样用的

Getting Started

java 复制代码
@SpringBootApplication
@EnableFeignClients
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);
   } 
   
   @FeignClient("name") 
   static interface NameService {
       @RequestMapping("/") 
       public String getName();
   }
}

示例来自spring官网

我们注意到,在springboot 的 application入口上,例子使用了注解 @EnableFeignClients 顾名思义是 启用feignclient的,通过查看@EnableFeignClients注解的源码

java 复制代码
@Retention(RetentionPolicy.RUNTIME)  //注解级别
@Target(ElementType.TYPE)  //类、接口类型的注解
@Documented  //java doc
@Import(FeignClientsRegistrar.class) //import FeignClientsRegistrar
public @interface EnableFeignClients {
	/**
	 * basePackages的别名
	 */
	String[] value() default {};

	/**
	 * 扫描包路径
	 */
	String[] basePackages() default {};

	/**
	 * 指定扫描类型
	 */
	Class<?>[] basePackageClasses() default {};

	/**
	 * 默认的feignclient配置,可以指定为自定义配置
	 */
	Class<?>[] defaultConfiguration() default {};

	/**
	 * 如果不是空的,则禁用类路径,用于标识feiclient列表
	 * @return list of FeignClient classes
	 */
	Class<?>[] clients() default {};
}
  • 我们发现,他import了FeignClientsRegistrar.class,通过查看FeignClientsRegistrar的源码可以看到
java 复制代码
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    ......
}

其实现了ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware,他的功能就是把feignclient注入到spring容器中交由spring管理,当我们需要用到feign的api时,我们可以通过 @Autowired或者@Resource注解引入,其主要使用的方法是#registerDefaultConfiguration

  • registerDefaultConfiguration
java 复制代码
@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
                //如果类上有@EnableFeignClients注解并且有配置,则执行 registerClientConfiguration 方法,注入client配置
		registerDefaultConfiguration(metadata, registry);
                //注册client到spring中
		registerFeignClients(metadata, registry);
	}
  • registerFeignClients
java 复制代码
	private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
			Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
                //是否lazy注册
		if (String.valueOf(false).equals(
				environment.getProperty("spring.cloud.openfeign.lazy-attributes-resolution", String.valueOf(false)))) {
			eagerlyRegisterFeignClientBeanDefinition(className, attributes, registry);
		}
		else {
			lazilyRegisterFeignClientBeanDefinition(className, attributes, registry);
		}
	}

这里我们主要来看看 #eagerlyRegisterFeignClientBeanDefinition方法

java 复制代码
private void eagerlyRegisterFeignClientBeanDefinition(String className, Map<String, Object> attributes,
			BeanDefinitionRegistry registry) {
         //校验参数
		validate(attributes);
       //构建FactoryBean
		BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
     //配置 factory信息
		definition.addPropertyValue("url", getUrl(null, attributes));
		definition.addPropertyValue("path", getPath(null, attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		String contextId = getContextId(null, attributes);
		definition.addPropertyValue("contextId", contextId);
		definition.addPropertyValue("type", className);
		definition.addPropertyValue("dismiss404", Boolean.parseBoolean(String.valueOf(attributes.get("dismiss404"))));
      //配置feign 的 fallback
		Object fallback = attributes.get("fallback");
		if (fallback != null) {
			definition.addPropertyValue("fallback",
					(fallback instanceof Class ? fallback : ClassUtils.resolveClassName(fallback.toString(), null)));
		}
		Object fallbackFactory = attributes.get("fallbackFactory");
		if (fallbackFactory != null) {
			definition.addPropertyValue("fallbackFactory", fallbackFactory instanceof Class ? fallbackFactory
					: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
		}
		definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		definition.addPropertyValue("refreshableClient", isClientRefreshEnabled());
		String[] qualifiers = getQualifiers(attributes);
		if (ObjectUtils.isEmpty(qualifiers)) {
			qualifiers = new String[] { contextId + "FeignClient" };
		}
		// 生成 AOT 时可以检索限定符
		definition.addPropertyValue("qualifiers", qualifiers);
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
		Class<?> type = ClassUtils.resolveClassName(className, null);
		beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, type);
		//primary有默认值,不会为空
		boolean primary = (Boolean) attributes.get("primary");
		beanDefinition.setPrimary(primary);
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
                
           //执行注入
		registerRefreshableBeanDefinition(registry, contextId, Request.Options.class, OptionsFactoryBean.class);
		registerRefreshableBeanDefinition(registry, contextId, RefreshableUrl.class, RefreshableUrlFactoryBean.class);
	}
  • #registerRefreshableBeanDefinition 方法的内容
java 复制代码
    /**
    * 该方法向refreshScope 注册bean 定义。
    * @paramregistry spring bean定义注册表
    * @param contextId feign 客户端名称
    * @param beanType bean的类型
    * @paramfactoryBeanType指向相关的bean工厂
    */
	private void registerRefreshableBeanDefinition(BeanDefinitionRegistry registry, String contextId, Class<?> beanType,
			Class<?> factoryBeanType) {
		if (isClientRefreshEnabled()) {
			String beanName = beanType.getCanonicalName() + "-" + contextId;
			BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(factoryBeanType);
			definitionBuilder.setScope("refresh");
			definitionBuilder.addPropertyValue("contextId", contextId);
			BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(),
					beanName);
			definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true);
			BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
		}
	}

概括一下就是,启用feign后,在启动阶段会扫描代码里的feiclient注册成bean写入到spring中,从而我们可以通过自动注入拿到bean从而使用bean

@feignClient注解

前面看了feignClient是怎么注入到spring容器中,现在来看看怎么生成feignClient的bean,我们先来看看feignClient注解

java 复制代码
public @interface FeignClient {

	/**
	 * name的别名
	 */
	@AliasFor("name")
	String value() default "";

	/**
	 * 如果有值作为beanName但不用作于服务id
	 */
	String contextId() default "";

	/**
	 * 服务id
	 */
	@AliasFor("value")
	String name() default "";

	/**
	 * @return the <code>@Qualifiers</code> value for the feign client.
	 */
	String[] qualifiers() default {};

	/**
	 * 服务地址  比如说  https://xxx.com
	 */
	String url() default "";

	/**
	 * 是否处理404
	 */
	boolean dismiss404() default false;

	/**
	 * client的自定义配置
	 */
	Class<?>[] configuration() default {};

	/**
	 * 指定错误回调类
	 */
	Class<?> fallback() default void.class;

	/**
	 * fallback工厂
	 */
	Class<?> fallbackFactory() default void.class;

	/**
	 * 路径前缀  比如说 path("/v1/api/")  -> https://xxx.com/v1/api/
	 */
	String path() default "";

	/**
	 * 是否标记为主bean,默认为true
	 */
	boolean primary() default true;

}

feign注解提供了一系列对client的配置,可以在FeignClientsRegistrar#eagerlyRegisterFeignClientBeanDefinition中看到,构建bean是通过FeignClientFactoryBean进行构建,我们阿里看看FeignClientFactoryBean是如何进行构建的

FeignClientFactoryBean.java

java 复制代码
public class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware{
                ......
}

FeignClientFactoryBean类实现 ApplicationContextAware, BeanFactoryAware 接口,

FeignClientFactoryBean 定义了个生产的 feignClient的factory,来看看他的 #getObject()方法

java 复制代码
	@Override
	public Object getObject() {
		return getTarget();
	}

	/**
	 * @param FeignClient 的类型
	 * @return 构建一个  Feign 
	 */
	@SuppressWarnings("unchecked")
	<T> T getTarget() {
		FeignClientFactory feignClientFactory = beanFactory != null ? beanFactory.getBean(FeignClientFactory.class)
				: applicationContext.getBean(FeignClientFactory.class);
		Feign.Builder builder = feign(feignClientFactory);
		if (!StringUtils.hasText(url) && !isUrlAvailableInConfig(contextId)) {

			if (LOG.isInfoEnabled()) {
				LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
			}
			if (!name.startsWith("http://") && !name.startsWith("https://")) {
				url = "http://" + name;
			}
			else {
				url = name;
			}
			url += cleanPath();
			return (T) loadBalance(builder, feignClientFactory, new HardCodedTarget<>(type, name, url));
		}
		if (StringUtils.hasText(url) && !url.startsWith("http://") && !url.startsWith("https://")) {
			url = "http://" + url;
		}
		Client client = getOptional(feignClientFactory, Client.class);
		if (client != null) {
			if (client instanceof FeignBlockingLoadBalancerClient) {
				//处理负载均衡
				client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
			}
			if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
				// 处理负载均衡
				client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
			}
			builder.client(client);
		}
                // 处理 FeignClientFacty
		applyBuildCustomizers(feignClientFactory, builder);
                
		Targeter targeter = get(feignClientFactory, Targeter.class);
		return targeter.target(this, builder, feignClientFactory, resolveTarget(feignClientFactory, contextId, url));
	}

从源码中可以知道的是,构建了个Feign.Builder,通过builder构建出client,#feign方法如下

java 复制代码
	protected Feign.Builder feign(FeignClientFactory context) {
		FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
		Logger logger = loggerFactory.create(type);

		//配置builder的logger、encode、decode、contract(该处为默认配置)
		Feign.Builder builder = get(context, Feign.Builder.class)
				// required values
				.logger(logger)
				.encoder(get(context, Encoder.class))
				.decoder(get(context, Decoder.class))
				.contract(get(context, Contract.class));


           //对client进行配置,不同的配置(如果配置里有自定义的logger、encode、decode、contract,会对builder进行替换)
		configureFeign(context, builder);

		return builder;
	}

builder从feignContext取,然后配置feignContext中取出的encoder、decoder、contract,再根据feignClient的是否有自定义配置对builder进行配置

java 复制代码
protected void configureFeign(FeignClientFactory context, Feign.Builder builder) {
    FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
          : applicationContext.getBean(FeignClientProperties.class);

    FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
    setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());

    if (properties != null && inheritParentContext) {
       if (properties.isDefaultToProperties()) {
          configureUsingConfiguration(context, builder);
          configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
          configureUsingProperties(properties.getConfig().get(contextId), builder);
       }
       else {
          configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
          configureUsingProperties(properties.getConfig().get(contextId), builder);
          configureUsingConfiguration(context, builder);
       }
    }
    else {
       configureUsingConfiguration(context, builder);
    }
}

这样一个feignClient的builder就构建完成

总结

概括一下,就是spring启动的时候,会去扫描指定(没指定有默认包路径)包路径下代码,找到被@FeignClient注解的类,构建feignContext并注入client到其中,feignClient必须注入logger、encoder、decoder、contract

FeignClient中方法的执行过程

我们从SpringCloud OpenFeign文档中中看到了这个简单示例

java 复制代码
@FeignClient(url = "http://localhost:8080") 
public interface DemoClient {
    @GetMapping("demo") 
    String getDemo(String param); 
 }
  • 代码里写了个被FeignClient注解的DemoClient接口
  • 代码里有个 @GetMapping注解方法getDemo()
  • client配置了URLhttp://localhost:8080
  • @GetMapping配置了接口名 demo
java 复制代码
@Resource
private DemoClient demoClinet;

......

String demoRes=demoClinet.getDemo("param")

SpringCloud OpenFeign 依赖了OpenFeign,feignClient执行请求我们使用OpenFeignClient和SpringCloud OpenFeig的源码来进行分析

当我们注入Client并调用其方法时,他是怎么执行的,如上代码所示 我们调用了 DemoClient#getDemo(),(这里使用同步feign)通过断点,我们可以看到其实调用了 SynchronousMethodHandler#invoke,invoke代码如下(以下代码块均以OpenFeign为例):

java 复制代码
@Override
public Object invoke(Object[] argv) throws Throwable {
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Options options = findOptions(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      return executeAndDecode(template, options);
    } catch (RetryableException e) {
      try {
        retryer.continueOrPropagate(e);
      } catch (RetryableException th) {
        Throwable cause = th.getCause();
        if (propagationPolicy == UNWRAP && cause != null) {
          throw cause;
        } else {
          throw th;
        }
      }
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
      }
      continue;
    }
  }
}

RequestTemplate

先来看看 buildTemplateFromArgs.create是怎么生成RequestTemplate,create方法如下

java 复制代码
@Override
public RequestTemplate create(Object[] argv) {
  RequestTemplate mutable = RequestTemplate.from(metadata.template());
  mutable.feignTarget(target);
  if (metadata.urlIndex() != null) {
    int urlIndex = metadata.urlIndex();
    checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
    mutable.target(String.valueOf(argv[urlIndex]));
  }
  Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
  for (Map.Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
    int i = entry.getKey();
    Object value = argv[entry.getKey()];
    if (value != null) { // Null values are skipped.
      if (indexToExpander.containsKey(i)) {
        value = expandElements(indexToExpander.get(i), value);
      }
      for (String name : entry.getValue()) {
        varBuilder.put(name, value);
      }
    }
  }

  RequestTemplate template = resolve(argv, mutable, varBuilder);
  if (metadata.queryMapIndex() != null) {
    // add query map parameters after initial resolve so that they take
    // precedence over any predefined values
    Object value = argv[metadata.queryMapIndex()];
    Map<String, Object> queryMap = toQueryMap(value, metadata.queryMapEncoder());
    template = addQueryMapQueryParameters(queryMap, template);
  }

  if (metadata.headerMapIndex() != null) {
    // add header map parameters for a resolution of the user pojo object
    Object value = argv[metadata.headerMapIndex()];
    Map<String, Object> headerMap = toQueryMap(value, metadata.queryMapEncoder());
    template = addHeaderMapHeaders(headerMap, template);
  }

  return template;
}

构建template的过程就是把请求的属性加入到request对象中,其中queryMap和headerMap都是通过toQueryMap来的

toQueryMap

java 复制代码
private Map<String, Object> toQueryMap(Object value, QueryMapEncoder queryMapEncoder) {
  if (value instanceof Map) {
    return (Map<String, Object>) value;
  }
  try {
    // encode with @QueryMap annotation if exists otherwise with the one from this resolver
    return queryMapEncoder != null ? queryMapEncoder.encode(value)
        : this.queryMapEncoder.encode(value);
  } catch (EncodeException e) {
    throw new IllegalStateException(e);
  }
}

toQueryMap方法中可以看出,对所有的数据都进行了encode,如果没有传递encode对象就对factory中的encode对象来进行encode

encode

接下来我们来看看encode做了什么

encode有2种实现,看名字我们可以看出是对对象字段的处理,将请求参数转换成map。

  • BeanQueryMapEncoder#encode()
java 复制代码
@Override
public Map<String, Object> encode(Object object) throws EncodeException {
  if (object == null) {
    return Collections.emptyMap();
  }
  try {
    ObjectParamMetadata metadata = getMetadata(object.getClass());
    Map<String, Object> propertyNameToValue = new HashMap<String, Object>();
    for (PropertyDescriptor pd : metadata.objectProperties) {
      Method method = pd.getReadMethod();
      Object value = method.invoke(object);
      if (value != null && value != object) {
        Param alias = method.getAnnotation(Param.class);
        String name = alias != null ? alias.value() : pd.getName();
        propertyNameToValue.put(name, value);
      }
    }
    return propertyNameToValue;
  } catch (IllegalAccessException | IntrospectionException | InvocationTargetException e) {
    throw new EncodeException("Failure encoding object into query map", e);
  }
}
  • FieldQueryMapEncoder#encode()
java 复制代码
@Override
public Map<String, Object> encode(Object object) throws EncodeException {
  if (object == null) {
    return Collections.emptyMap();
  }
  ObjectParamMetadata metadata =
      classToMetadata.computeIfAbsent(object.getClass(), ObjectParamMetadata::parseObjectType);

  return metadata.objectFields.stream()
      .map(field -> this.FieldValuePair(object, field))
      .filter(fieldObjectPair -> fieldObjectPair.right.isPresent())
      .collect(Collectors.toMap(this::fieldName,
          fieldObjectPair -> fieldObjectPair.right.get()));

}

executeAndDecode

#invoke方法中可以看到,主要的业务处理是在executeAndDecode方法中,executeAndDecode方法代码如下

java 复制代码
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
  Request request = targetRequest(template);

  if (logLevel != Logger.Level.NONE) {
    logger.logRequest(metadata.configKey(), logLevel, request);
  }

  Response response;
  long start = System.nanoTime();
  try {
    response = client.execute(request, options);
    // ensure the request is set. TODO: remove in Feign 12
    response = response.toBuilder()
        .request(request)
        .requestTemplate(template)
        .build();
  } catch (IOException e) {
    if (logLevel != Logger.Level.NONE) {
      logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
    }
    throw errorExecuting(request, e);
  }

  long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
  return responseHandler.handleResponse(
      metadata.configKey(), response, metadata.returnType(), elapsedTime);
}

第一行代码中通过#targetRequest(template)构建了Reuqest,我们再来看看怎么构建Request

targetRequest()

java 复制代码
Request targetRequest(RequestTemplate template) {
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(template);
}


// apply方法,该段代码截取自Target的HardCodedTarget实现
@Override
public Request apply(RequestTemplate input) {
  if (input.url().indexOf("http") != 0) {
    input.target(url());
  }
  return input.request();
}


//RequestTemplate#request()
public Request request() {
  if (!this.resolved) {
    throw new IllegalStateException("template has not been resolved.");
  }
  return Request.create(this.method, this.url(), this.headers(), this.body, this);
}

#targetRequest之中就是将feign拦截器加入到request中去,最后调用RequestTemplate#request方法去构建Request

Request Object

request包含了

  • 请求方法 httpMethod
  • 请求地址 url
  • 请求头参数 headers
  • 请求体 body
  • http版本 protocolVersion 默认HTTP/1.1
  • requestTemplate Request构建模板

execute

构建完request对象,通过client的excute提交request从而拿到respone,我们来看看excure是怎么执行的

java 复制代码
@Override
public Response execute(Request request, Options options) throws IOException {
  HttpURLConnection connection = convertAndSend(request, options);
  return convertResponse(connection, request);
}
convertAndSend

先是通过 #convertAndSend方法 拿到HttpURLConnection,来一探究竟#convertAndSend做了什么

java 复制代码
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
      final URL url = new URL(request.url());
      final HttpURLConnection connection = this.getConnection(url);
      if (connection instanceof HttpsURLConnection) {
        HttpsURLConnection sslCon = (HttpsURLConnection) connection;
        if (sslContextFactory != null) {
          sslCon.setSSLSocketFactory(sslContextFactory);
        }
        if (hostnameVerifier != null) {
          sslCon.setHostnameVerifier(hostnameVerifier);
        }
      }
      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
      connection.setAllowUserInteraction(false);
      connection.setInstanceFollowRedirects(options.isFollowRedirects());
      connection.setRequestMethod(request.httpMethod().name());

      Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
      boolean gzipEncodedRequest = this.isGzip(contentEncodingValues);
      boolean deflateEncodedRequest = this.isDeflate(contentEncodingValues);

      boolean hasAcceptHeader = false;
      Integer contentLength = null;
      for (String field : request.headers().keySet()) {
        if (field.equalsIgnoreCase("Accept")) {
          hasAcceptHeader = true;
        }
        for (String value : request.headers().get(field)) {
          if (field.equals(CONTENT_LENGTH)) {
            if (!gzipEncodedRequest && !deflateEncodedRequest) {
              contentLength = Integer.valueOf(value);
              connection.addRequestProperty(field, value);
            }
          }
          // Avoid add "Accept-encoding" twice or more when "compression" option is enabled
          if (field.equals(ACCEPT_ENCODING)) {
            connection.addRequestProperty(field, String.join(", ", request.headers().get(field)));
            break;
          } else {
            connection.addRequestProperty(field, value);
          }
        }
      }
      // Some servers choke on the default accept string.
      if (!hasAcceptHeader) {
        connection.addRequestProperty("Accept", "*/*");
      }

      boolean hasEmptyBody = false;
      byte[] body = request.body();
      if (body == null && request.httpMethod().isWithBody()) {
        body = new byte[0];
        hasEmptyBody = true;
      }

      if (body != null) {
        /*
         * Ignore disableRequestBuffering flag if the empty body was set, to ensure that internal
         * retry logic applies to such requests.
         */
        if (disableRequestBuffering && !hasEmptyBody) {
          if (contentLength != null) {
            connection.setFixedLengthStreamingMode(contentLength);
          } else {
            connection.setChunkedStreamingMode(8196);
          }
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        if (gzipEncodedRequest) {
          out = new GZIPOutputStream(out);
        } else if (deflateEncodedRequest) {
          out = new DeflaterOutputStream(out);
        }
        try {
          out.write(body);
        } finally {
          try {
            out.close();
          } catch (IOException suppressed) { // NOPMD
          }
        }
      }
      return connection;
    }

从代码中可以看到其逻辑就是

  • 从Request对象中获connection参数并设定HttpURLConnection参数
  • 建立链接
  • 获取链接的输出流OutputStream
  • 将Request对象的body写入OutputStream
  • 关闭OutputStream
  • 返回HttpURLConnection对象
convertResponse

通过 #convertAndSend方法 拿到HttpURLConnection,通过#convertResponse拿到response

java 复制代码
    Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
      int status = connection.getResponseCode();
      String reason = connection.getResponseMessage();

      if (status < 0) {
        throw new IOException(format("Invalid status(%s) executing %s %s", status,
            connection.getRequestMethod(), connection.getURL()));
      }

      Map<String, Collection<String>> headers = new TreeMap<>(CASE_INSENSITIVE_ORDER);
      for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
        // response message
        if (field.getKey() != null) {
          headers.put(field.getKey(), field.getValue());
        }
      }

      Integer length = connection.getContentLength();
      if (length == -1) {
        length = null;
      }
      InputStream stream;
      if (status >= 400) {
        stream = connection.getErrorStream();
      } else {
        stream = connection.getInputStream();
      }
      if (this.isGzip(headers.get(CONTENT_ENCODING))) {
        stream = new GZIPInputStream(stream);
      } else if (this.isDeflate(headers.get(CONTENT_ENCODING))) {
        stream = new InflaterInputStream(stream);
      }
      return Response.builder()
          .status(status)
          .reason(reason)
          .headers(headers)
          .request(request)
          .body(stream, length)
          .build();
    }

#convertResponse方法中可以看到他的执行流程是

  • 从HttpURLConnection对象中获取返回内容
  • 判断返回结果
    • 成功
      • code是否正常的httpCode (<400)
        • 获取正常返回流数据
      • code是否正常的httpCode (>=400)
        • 获取错误返回流数据
      • 处理流数据
      • 构建response
    • 失败
      • 抛出异常
Response Object

handleResponse

executeAndDecode方法中,通过client.execute拿到response后,在返回去前会调用ResponseHandler#handleResponse方法处理response

java 复制代码
public Object handleResponse(String configKey,
                             Response response,
                             Type returnType,
                             long elapsedTime)
    throws Exception {
  try {
    response = logAndRebufferResponseIfNeeded(configKey, response, elapsedTime);
    return executionChain.next(
        new InvocationContext(configKey, decoder, errorDecoder, dismiss404, closeAfterDecode,
            decodeVoid, response, returnType));
  } catch (final IOException e) {
    if (logLevel != Level.NONE) {
      logger.logIOException(configKey, logLevel, e, elapsedTime);
    }
    throw errorReading(response.request(), response, e);
  } catch (Exception e) {
    ensureClosed(response.body());
    throw e;
  }
}

handleResponse方法将respone处理成对象返回,来看看 InvocationContext#proceed()

proceed
java 复制代码
public Object proceed() throws Exception {
  if (returnType == Response.class) {
    return disconnectResponseBodyIfNeeded(response);
  }

  try {
    final boolean shouldDecodeResponseBody =
        (response.status() >= 200 && response.status() < 300)
            || (response.status() == 404 && dismiss404
                && !isVoidType(returnType));

    if (!shouldDecodeResponseBody) {
      throw decodeError(configKey, response);
    }

    if (isVoidType(returnType) && !decodeVoid) {
      ensureClosed(response.body());
      return null;
    }

    Class<?> rawType = Types.getRawType(returnType);
    if (TypedResponse.class.isAssignableFrom(rawType)) {
      Type bodyType = Types.resolveLastTypeParameter(returnType, TypedResponse.class);
      return TypedResponse.builder(response)
              .body(decode(response, bodyType))
              .build();
    }

    return decode(response, returnType);
  } finally {
    if (closeAfterDecode) {
      ensureClosed(response.body());
    }
  }
}

#proceed方法根据返回类型和response的httpCode进行处理,错误的处理方式是throw出对应的错误,如果返回type为void则直接返回null,我们以成功且returnType不是void为例,我们可以看到代码中是Types.getRawType(returnType)拿到rawType#getRawType方法代码如下,通过对type类型的判断获得Class<?>

java 复制代码
public static Class<?> getRawType(Type type) {
  if (type instanceof Class<?>) {
    // Type is a normal class.
    return (Class<?>) type;

  } else if (type instanceof ParameterizedType) {
    ParameterizedType parameterizedType = (ParameterizedType) type;

    // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but
    // suspects some pathological case related to nested classes exists.
    Type rawType = parameterizedType.getRawType();
    if (!(rawType instanceof Class)) {
      throw new IllegalArgumentException();
    }
    return (Class<?>) rawType;

  } else if (type instanceof GenericArrayType) {
    Type componentType = ((GenericArrayType) type).getGenericComponentType();
    return Array.newInstance(getRawType(componentType), 0).getClass();

  } else if (type instanceof TypeVariable) {
    // We could use the variable's bounds, but that won't work if there are multiple. Having a raw
    // type that's more general than necessary is okay.
    return Object.class;

  } else if (type instanceof WildcardType) {
    return getRawType(((WildcardType) type).getUpperBounds()[0]);

  } else {
    String className = type == null ? "null" : type.getClass().getName();
    throw new IllegalArgumentException("Expected a Class, ParameterizedType, or "
        + "GenericArrayType, but <" + type + "> is of type "
        + className);
  }
}

拿到Class<?>后,通过Class的native方法isAssignableFrom判断是否feign.TypedResponse的同族类

  • TypedResponse的同族类
    • 通过Types.resolveLastTypeParameter()拿到bodyType
    • 对body进行decode成bodyType后通过TypedResponse的builder进行构建TypedResponse对象
  • 不是 TypedResponse的同族类
    • 对body进行decode成returnType类型
decode

查看feign项目core包中的feign.codec.Decoder接口可以看到,有多种方式的实现,core中提供了StringDecoderResponseMappingDecoderOptionalDecoderStreamDecoder

默认的是使用了

java 复制代码
public class Default extends StringDecoder {

  @Override
  public Object decode(Response response, Type type) throws IOException {
    if (response.status() == 404 || response.status() == 204)
      return Util.emptyValueOf(type);
    if (response.body() == null)
      return null;
    if (byte[].class.equals(type)) {
      return Util.toByteArray(response.body().asInputStream());
    }
    return super.decode(response, type);
  }
}

回到SpringCloud OpenFeign

到这,我们回归到SpringCloud OpenFeign中来,在SpringCloud OpenFeign中,其提供了更适合spring中使用的相关代码

LoadBalancer

相比openFeign提供了更多功能,我们可以看到他生成的Client有2种类型

  • FeignBlockingLoadBalancerClient
  • RetryableFeignBlockingLoadBalancerClient

我们来看看FeignBlockingLoadBalancerClient,FeignBlockingLoadBalancerClient实现了Client,在execute的时候通过负载均衡或得到了对应的服务url

java 复制代码
	@Override
	public Response execute(Request request, Request.Options options) throws IOException {
		final URI originalUri = URI.create(request.url());
		String serviceId = originalUri.getHost();
		Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
		String hint = getHint(serviceId);
		DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
				new RequestDataContext(buildRequestData(request), hint));
		Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
				.getSupportedLifecycleProcessors(
						loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
						RequestDataContext.class, ResponseData.class, ServiceInstance.class);
		supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
		ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
		org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
				instance);
		if (instance == null) {
			String message = "Load balancer does not contain an instance for the service " + serviceId;
			if (LOG.isWarnEnabled()) {
				LOG.warn(message);
			}
			supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
					.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
							CompletionContext.Status.DISCARD, lbRequest, lbResponse)));
			return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
					.body(message, StandardCharsets.UTF_8).build();
		}
		String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
		Request newRequest = buildRequest(request, reconstructedUrl, instance);
		return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse,
				supportedLifecycleProcessors);
	}
decode

FeignClientsConfiguration中可以看到默认注入的decode为OpenFeign的OptionalDecoder

java 复制代码
	@Bean
	@ConditionalOnMissingBean
	public Decoder feignDecoder(ObjectProvider<HttpMessageConverterCustomizer> customizers) {
		return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters, customizers)));
	}

OptionalDecoder是这样实现的,我们可以看到实际的decoder是ResponseEntityDecoder,OptionalDecoder工作代码如下:

java 复制代码
public final class OptionalDecoder implements Decoder {
  final Decoder delegate;

  public OptionalDecoder(Decoder delegate) {
    Objects.requireNonNull(delegate, "Decoder must not be null. ");
    this.delegate = delegate;
  }

  @Override
  public Object decode(Response response, Type type) throws IOException {
    if (!isOptional(type)) {
      return delegate.decode(response, type);
    }
    if (response.status() == 404 || response.status() == 204) {
      return Optional.empty();
    }
    Type enclosedType = Util.resolveLastTypeParameter(type, Optional.class);
    return Optional.ofNullable(delegate.decode(response, enclosedType));
  }

  static boolean isOptional(Type type) {
    if (!(type instanceof ParameterizedType)) {
      return false;
    }
    ParameterizedType parameterizedType = (ParameterizedType) type;
    return parameterizedType.getRawType().equals(Optional.class);
  }
}

我们再来看看 ResponseEntityDecoder,具体解析decode的也是通过构造函数携带的decoder

java 复制代码
public class ResponseEntityDecoder implements Decoder {

	private final Decoder decoder;

	public ResponseEntityDecoder(Decoder decoder) {
		this.decoder = decoder;
	}
        
        ......

所以SpringCloud OpenFeign默认的decoder是 SpringDecoder

java 复制代码
//SpringDecoder.java
public SpringDecoder(ObjectFactory<HttpMessageConverters> messageConverters,
                ObjectProvider<HttpMessageConverterCustomizer> customizers) {
        this.messageConverters = messageConverters;
        this.customizers = customizers;
}

//内部类
private final class FeignResponseAdapter implements ClientHttpResponse {

    private final Response response;

    private FeignResponseAdapter(Response response) {
            this.response = response;
    }

    @Override
    public HttpStatus getStatusCode() {
            return HttpStatus.valueOf(response.status());
    }

    @Override
    public int getRawStatusCode() {
            return response.status();
    }

    @Override
    public String getStatusText() {
            return response.reason();
    }

    @Override
    public void close() {
            try {
                    response.body().close();
            }
            catch (IOException ex) {
                    // Ignore exception on close...
            }
    }

    @Override
    public InputStream getBody() throws IOException {
            return response.body().asInputStream();
    }

    @Override
    public HttpHeaders getHeaders() {
            return getHttpHeaders(response.headers());
    }

}

SpringDecoder的主要业务方法是#decode

java 复制代码
@Override
public Object decode(final Response response, Type type) throws IOException, FeignException {
        if (type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType) {
                List<HttpMessageConverter<?>> converters = messageConverters.getObject().getConverters();
                customizers.forEach(customizer -> customizer.accept(converters));
                @SuppressWarnings({ "unchecked", "rawtypes" })
                HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, converters);

                return extractor.extractData(new FeignResponseAdapter(response));
        }
        throw new DecodeException(response.status(), "type is not an instance of Class or ParameterizedType: " + type,
                        response.request());
}


//HttpMessageConverterExtractor.java #extractData
public T extractData(ClientHttpResponse response) throws IOException {
    MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
    if (responseWrapper.hasMessageBody() && !responseWrapper.hasEmptyMessageBody()) {
        MediaType contentType = this.getContentType(responseWrapper);

        try {
            Iterator var4 = this.messageConverters.iterator();

            while(var4.hasNext()) {
                HttpMessageConverter<?> messageConverter = (HttpMessageConverter)var4.next();
                if (messageConverter instanceof GenericHttpMessageConverter) {
                    GenericHttpMessageConverter<?> genericMessageConverter = (GenericHttpMessageConverter)messageConverter;
                    if (genericMessageConverter.canRead(this.responseType, (Class)null, contentType)) {
                        if (this.logger.isDebugEnabled()) {
                            ResolvableType resolvableType = ResolvableType.forType(this.responseType);
                            this.logger.debug("Reading to [" + resolvableType + "]");
                        }

                        return genericMessageConverter.read(this.responseType, (Class)null, responseWrapper);
                    }
                }

                if (this.responseClass != null && messageConverter.canRead(this.responseClass, contentType)) {
                    if (this.logger.isDebugEnabled()) {
                        String className = this.responseClass.getName();
                        this.logger.debug("Reading to [" + className + "] as "" + contentType + """);
                    }

                    return messageConverter.read(this.responseClass, responseWrapper);
                }
            }
        } catch (HttpMessageNotReadableException | IOException var8) {
            throw new RestClientException("Error while extracting response for type [" + this.responseType + "] and content type [" + contentType + "]", var8);
        }

        throw new UnknownContentTypeException(this.responseType, contentType, responseWrapper.getRawStatusCode(), responseWrapper.getStatusText(), responseWrapper.getHeaders(), getResponseBody(responseWrapper));
    } else {
        return null;
    }
}

这里对reponse的解析类似于@RequestBody注解,将http中的数据序列化回bean(对象通过json、字符串直接转化成String)

总结

feign帮我们完成了从构建http请求到发送http请求再到处理http返回内容的整个过程,我们可以在构建feignClient的时候对encode和decode进行重写以达到我们想要的效果,比如说可以请求进行自定义格式的压缩、对请求自动填充某些数据比如说用户对象的某些属性等以提升不同服务间互相访问的效率和减少交互频率来提升性能。我们也可以自定义logger,来实现打印不同的日志以达到请求链路的染色需求等,还可以自定义http客户端等等。

相关推荐
sdg_advance1 小时前
Spring Cloud之OpenFeign的具体实践
后端·spring cloud·openfeign
杨荧2 小时前
【JAVA开源】基于Vue和SpringBoot的旅游管理系统
java·vue.js·spring boot·spring cloud·开源·旅游
Java探秘者11 小时前
Maven下载、安装与环境配置详解:从零开始搭建高效Java开发环境
java·开发语言·数据库·spring boot·spring cloud·maven·idea
gobeyye16 小时前
spring loC&DI 详解
java·spring·rpc
杨荧16 小时前
【JAVA开源】基于Vue和SpringBoot的水果购物网站
java·开发语言·vue.js·spring boot·spring cloud·开源
半夜下雨20 小时前
SpringCloud学习记录|day2
spring cloud
杨荧1 天前
【JAVA开源】基于Vue和SpringBoot的周边产品销售网站
java·开发语言·vue.js·spring boot·spring cloud·开源
kong79069282 天前
SpringCloud入门(十一)路由过滤器和路由断言工厂
spring cloud·gateway 网关路由
customer082 天前
【开源免费】基于SpringBoot+Vue.JS美容院管理系统(JAVA毕业设计)
android·java·vue.js·spring boot·spring cloud·开源
半夜下雨2 天前
SpringCloud学习记录|day1
spring cloud