Feign源码解析之自定义配置和注意点

/ Java / 没有评论 / 1750浏览

Feign源码解析之自定义配置和注意点

前文

一. 对feign属性的覆盖

主要可以覆盖的属性及其默认值如下:

//feign拦截器
private final List<RequestInterceptor> requestInterceptors =
  new ArrayList<RequestInterceptor>(); 
//日志级别
private Logger.Level logLevel = Logger.Level.NONE;
//对方法和接口上的注解进行解析
private Contract contract = new Contract.Default();
//Http客户端接口,默认为Client.Default,也可以是OkHttp,ApacheHttpClient
private Client client = new Client.Default(null, null);
//失败重试
private Retryer retryer = new Retryer.Default();
//日志
private Logger logger = new NoOpLogger();
//编码,由feign方法数据到请求数据
private Encoder encoder = new Encoder.Default();
//解码,由请求响应到feign方法返回值
private Decoder decoder = new Decoder.Default();
//错误时的解码,请求返回错误时的处理
private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
//请求相关的设置,如connectTimeoutMillis连接超时和readTimeoutMillis读超时
private Options options = new Options();
//请求返回404时是否进行编码
private boolean decode404;
protected Feign.Builder feign(FeignContext context) {
  FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
  Logger logger = loggerFactory.create(this.type);

  // @formatter:off
  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));
  // @formatter:on

  configureFeign(context, builder);

  return builder;
}

protected void configureFeign(FeignContext context, Feign.Builder builder) {
  FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
  if (properties != null) {
    if (properties.isDefaultToProperties()) {
      configureUsingConfiguration(context, builder);
      configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
      configureUsingProperties(properties.getConfig().get(this.name), builder);
    } else {
      configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
      configureUsingProperties(properties.getConfig().get(this.name), builder);
      configureUsingConfiguration(context, builder);
    }
  } else {
    configureUsingConfiguration(context, builder);
  }
}

如上面的代码,具体内容我们已经在Feign源码解析之生成jdk动态代理进行分析过。 有以下几种方式可以对feign的属性进行覆盖。

  1. EnableFeignClients#defaultConfiguration属性和FeignClient#configuration属性
  1. 定义配置类将自定义的feign属性注入IOC容器
  1. 配置文件

总结:

-上述3种方式修改的优先级为:2 < 1 < 3

需要注意的是:

二. RequestInterceptor

RequestInterceptor也是上文说的feign中属性的一种,是对feign方法的拦截器,在SynchronousMthodHandler类里转发请求前,会先进入RequestInterceptor进行处理。

Request targetRequest(RequestTemplate template) {
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(new RequestTemplate(template));
}

我们可以自定义RequestInterceptor的实现类对Feign请求增加公共操作,如公共的请求头数据等等。

在我们的项目里,会针对请求头的token进行鉴权,获取到登录人信息然后放到线程本地变量ThreadLocal中,但是一旦经过feign转发后,另一个模块无法进行获取,因此我们需要增加1个RequesInterceptor,获取到ThreadLocal值后按照一定的规则加入到请求头中。

不过,需要注意的是当feign中启用了hystrix,默认通过线程池隔离的方式会另起一个线程处理,此时ThreadLocal同样无法获取到。具体的解决方法有不少相关资料,可以参考Hystrix实现ThreadLocal上下文的传递了解。

三. AnnotatedParameterProcessor接口

AnnotatedParameterProcessor用来处理参数注解,springboot中默认的AnnotatedParameterProcessor的实现类有PathVariableParameterProcessor、RequestParamParameterProcessor、RequestHeaderParameterProcessor用来处理@PathVariable、@RequestParam、@RequstHeader。

用户可以自定义一些feign方法参数的注解,并自定义相关的AnnotatedParameterProcessor实现类将其映射到MethodMetaData中,方法参数的类型对应AnnotatedParameterProcessor的ANNOTATION属性。详细内容可以查看SpringMvcContract类的protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex)方法。

需要注意,用户一旦自定义AnnotatedParameterProcessor实现类并注入IOC容器,默认的3个AnnotatedParameterProcessor类将失效。如下面的代码所示,当新建并初始化SpringMvcContract时,只有传入的annotatedParameterProcessors参数是空的list时,才会调用getDefaultAnnotatedArgumentsProcessors方法获取默认的3个AnnotatedParameterProcessor类。而SpringMvcContract类在FeignClientsConfiguration配置类中被初始化,annotatedParameterProcessors参数对应的正是注入IOC容器的AnnotatedParameterProcessor实现类。

public SpringMvcContract(
  List<AnnotatedParameterProcessor> annotatedParameterProcessors,
  ConversionService conversionService) {
  Assert.notNull(annotatedParameterProcessors,
                 "Parameter processors can not be null.");
  Assert.notNull(conversionService, "ConversionService can not be null.");

  List<AnnotatedParameterProcessor> processors;
  if (!annotatedParameterProcessors.isEmpty()) {
    processors = new ArrayList<>(annotatedParameterProcessors);
  }
  else {
    processors = getDefaultAnnotatedArgumentsProcessors();
  }
  this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
  this.conversionService = conversionService;
  this.expander = new ConvertingExpander(conversionService);
}

private List<AnnotatedParameterProcessor> getDefaultAnnotatedArgumentsProcessors() {

  List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();

  annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
  annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
  annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());

  return annotatedArgumentResolvers;
}

如果feign方法的参数没有被AnnotatedParameterProcessor进行过解析,而且不是URI类,sprpingboot默认会按照RequestBody注解处理成请求的body参数,一个方法中最多只能有1个body参数。该限制在BaseContract的protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method)方法中进行校验。

for (int i = 0; i < count; i++) {
  boolean isHttpAnnotation = false;
  if (parameterAnnotations[i] != null) {
    isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
  }
  if (parameterTypes[i] == URI.class) {
    data.urlIndex(i);
  } else if (!isHttpAnnotation) {
    checkState(data.formParams().isEmpty(),
               "Body parameters cannot be used with form parameters.");
    checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
    data.bodyIndex(i);
    data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
  }
}

四. 其它注意点

1. Feign接口不能是泛型接口,最多只能有1个且1层父接口。

checkState(targetType.getTypeParameters().length == 0, 
           "Parameterized types unsupported: %s",
           targetType.getSimpleName());
checkState(targetType.getInterfaces().length <= 1, 
           "Only single inheritance supported: %s",
           targetType.getSimpleName());
if (targetType.getInterfaces().length == 1) {
  checkState(targetType.getInterfaces()[0].getInterfaces().length == 0,
             "Only single-level inheritance supported: %s",
             targetType.getSimpleName());
}

2. 写在Feign接口的父接口里的方法同样有效

for (Method method : targetType.getMethods()) {
  if (method.getDeclaringClass() == Object.class ||
      (method.getModifiers() & Modifier.STATIC) != 0 ||
      Util.isDefault(method)) {
    continue;
  }
  MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
  checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
             metadata.configKey());
  result.put(metadata.configKey(), metadata);
}

3. RequestMapping注解的value属性只有使用在Feign接口的最上面一层接口才具有前缀作用。

if(targetType.getInterfaces().length == 1) {
  processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType);

在SpringMvcContract的protected abstract void processAnnotationOnClass(MethodMetadata data, Class<?> clz);进行了判断,当clz.getInterfaces().length == 0才会进行处理。

@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
  if (clz.getInterfaces().length == 0) {
    RequestMapping classAnnotation = findMergedAnnotation(clz,
                                                          RequestMapping.class);
    if (classAnnotation != null) {
      // Prepend path from class annotation if specified
      if (classAnnotation.value().length > 0) {
        String pathValue = emptyToNull(classAnnotation.value()[0]);
        pathValue = resolve(pathValue);
        if (!pathValue.startsWith("/")) {
          pathValue = "/" + pathValue;
        }
        data.template().insert(0, pathValue);
      }
    }
  }
}

4. 对于RequestMapping注解的produces、consumers、headers属性,Feign接口和父接口上的RequestMapping注解都有效,且Feign接口的RequestMapping注解会覆盖父接口上的注解。

@Override
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
  this.processedMethods.put(Feign.configKey(targetType, method), method);
  MethodMetadata md = super.parseAndValidateMetadata(targetType, method);

  RequestMapping classAnnotation = findMergedAnnotation(targetType,
                                                        RequestMapping.class);
  if (classAnnotation != null) {
    // produces - use from class annotation only if method has not specified this
    if (!md.template().headers().containsKey(ACCEPT)) {
      parseProduces(md, method, classAnnotation);
    }

    // consumes -- use from class annotation only if method has not specified this
    if (!md.template().headers().containsKey(CONTENT_TYPE)) {
      parseConsumes(md, method, classAnnotation);
    }

    // headers -- class annotation is inherited to methods, always write these if
    // present
    parseHeaders(md, method, classAnnotation);
  }
  return md;
}

综合一下2、3、4,举一个例子。有Feign接口如下:

@FeignClient(value = "shop")
@RequestMapping(value = "/shops2", produces = "application/xml")
public interface ShopFeignClient extends BaseFeignClient {
}

其父接口如下:

@RequestMapping(value = "/shops", produces = "text/html", consumes = "application/json")
public interface BaseFeignClient {

  @GettMapping("/{shopId}")
  Shop getShopById(@PathVariable("shopId") Long shopId);
}

最后在SynchronousMethodHandler中的RequestTemplate的值如下图:

定义多个指向同一个服务的FeignClient需要保证bean alias不同且configuration属性相同。虽然理论上可以创建多个指向同一个服务的FeignClient,因为指向同一个服务说明FeignClient的name相同,那么需要注意一下两点:

  1. bean的别名alias:FeignClient注解的接口注入后的bean的alias默认为name + “.FeignClient”,因此如果有多个name相同的FeignClient,需要通过设置FeignClient的qualifier属性来保证alias的不同。
  2. FeignClient的configuration属性:在FeignContext中configurations是由一个map结果,key值为name,value为EnbaleFeignClient个FeignClient的configuration属性,根据configurations生成的contexts也是以name为key值。因此,需要保证多个指向同一个服务的FeignClient的configuration属性应该保持一致,否则springboot无法进行区分。因此,不建议定义多个指向同一个服务的FeignClient,如果真的需要这么做,务必保证bean alias不同且configuration属性相同。