Feign源码分析:记初次使用Feign踩的一些坑

492 阅读11分钟
    data.urlIndex(i);
  } else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
    // 如果既不是HTTP的注解,参数也不是Request.Option类型,则当做http的body来处理
    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]));
  }
}
// 省略部分代码

return data;

} 复制代码 parseAndValidateMetadata() 方法会接着来处理方法参数级别的注解,遍历所有的参数依次进行处理:  如果参数上有注解,则调用 processAnnotationsOnParameter() 方法进行处理注解。  protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {

  // ... 省略代码
Method method = this.processedMethods.get(data.configKey());
    // 获取参数上所有的注解,依次处理
for (Annotation parameterAnnotation : annotations) {

// 根据注解类型,获取对应的注解处理器
	AnnotatedParameterProcessor processor = this.annotatedArgumentProcessors
			.get(parameterAnnotation.annotationType());
	if (processor != null) {
		Annotation processParameterAnnotation;
		// synthesize, handling @AliasFor, while falling back to parameter name on
		// missing String #value():
		processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(
				parameterAnnotation, method, paramIndex);
        //调用处理处理器进行处理参数注解
		isHttpAnnotation |= processor.processArgument(context,
				processParameterAnnotation, method);
	}
}

if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) {
//  如果是http注解,则保存对应的expander
	TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex);
	if (this.conversionService.canConvert(typeDescriptor,
			STRING_TYPE_DESCRIPTOR)) {
		Param.Expander expander = this.convertingExpanderFactory
				.getExpander(typeDescriptor);
		if (expander != null) {
			data.indexToExpander().put(paramIndex, expander);
		}
	}
}
return isHttpAnnotation;

} 复制代码  processAnnotationsOnParameter() 方法会遍历参数上的注解,对每一个注解,会根据注解的类型找到对应的注解处理器进行处理注解,目前Feign中支持的处理器有   如果参数是 URI 类型,则标记该参数的索引(即使方法的第几个参数)为URI的索引,可以实现动态变更URL。   如果参数是被注解为HTTP请求的参数(由第一步中的注解处理器来判断)且 MethodMetadata对象的 <索引,Expander> 映射中没有对应的索引的 Expander 映射,则会在映射表中加入当前参数索引的 Expander 映射。( Expander 是应用于 Header 、 RequestLine 和 Body 的命名模板参数A named template parameter applied to {@link Headers}, {@linkplain RequestLine} or{@linkplain Body})  代码继续执行,回到 ReflectiveFeign.apply() 方法中: public Map<String, MethodHandler> apply(Target key) { List metadata = contract.parseAndValidatateMetadata(key.type()); Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>(); for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else if (md.bodyIndex() != null) { buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else { buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder); } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; } } 复制代码 apply() 在把所有自定义的方法转换成 MethodMetadata 对象之后,开始为每一个 MethodMetadata 构建 MethodHandler 对象, MethodHandler 对象通过工厂类 SynchronousMethodHandler.Factory 生成,生成的是 SynchronousMethodHandler 对象。 public MethodHandler create(Target<?> target, MethodMetadata md, RequestTemplate.Factory buildTemplateFromArgs, Options options, Decoder decoder, ErrorDecoder errorDecoder) { return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger, logLevel, md, buildTemplateFromArgs, options, decoder, errorDecoder, decode404, closeAfterDecode, propagationPolicy); } 复制代码 代码执行流继续回到 ReflectiveFeign.newInstance() 方法中: public T newInstance(Target target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List defaultMethodHandlers = new LinkedList();

for (Method method : target.type().getMethods()) { // 省略代码。把<配置名,MethodHandler>映射转换成<Method,MethodHandler>映射 if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler);

for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } 复制代码 在获取到<配置key,MethodHandler>映射之后,把映射转换成<Method,MethodHandler>对象,然后就会生成 InvocationHandler 对象。 看到了 InvocationHandler 出现,熟悉Java代理的同学应该会会心一笑! InvocationHandler 对象由工厂类 InvocationHandlerFactory 生成,默认实现为 static final class Default implements InvocationHandlerFactory {

@Override public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { return new ReflectiveFeign.FeignInvocationHandler(target, dispatch); } } 复制代码 生成的是 ReflectiveFeign.FeignInvocationHandler 对象,该对象实现了 InvocationHandler 接口。生成 ReflectiveFeign.FeignInvocationHandler 对象的时候会传入上一步生成的 <Method,MethodHander> 映射。 生成了 InvocationHandler 对象之后同样是熟悉的 Proxy.newProxyInstance 调用,用来生成代理对象。 分析到了这里先总结下: 在使用Feign当做HTTP Client的时候,只需要创建一个接口并用注解的方式来配置,就可以完成对服务提供方的接口绑定,然后直接注入使用就可以了。 对于每一个自定义的接口,Feign都会生成一个代理对象,所有对接口方法的调用最终都会执行到 ReflectiveFeign.FeignInvocationHandler 类的 invoke() 方法中。 整个过程时序图如下:

咱们继续往下分析: 上面说了,对自定义的所有的接口都会走到 ReflectiveFeign.FeignInvocationHandler 类的 invoke() 方法, invoke() 方法实现如下: public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// ... 省略一些基本方法的处理 // 本质上就是先根据被调用的方法找到对应的Methodhandler对象,然后执行invoke方法 return dispatch.get(method).invoke(args); } 复制代码 invoke() 方法本质上就是根据被调用的方法找到对应的 MethodHandler 对象,然后执行对应的 invoke() 方法。(dispatch保存的数据是由ParseHandlersByName解析接口得到的映射转换而来)。 MethodHandler.invoke() 的实现是在 SynchronousMethodHandler 中, public Object invoke(Object[] argv) throws Throwable { // 1. 构建请求模板 RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); Retryer retryer = this.retryer.clone(); while (true) { try { // 发起http请求已经解析结果 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; } } } 复制代码 MethodHandler.invoke() 方法总的来说就是构建请求模板,然后发起HTTP请求并把返回的响应转换成接口中定义的结构并返回。 现在已经知道怎么解决第一个坑(设置header和contenttype)了。 在 RequestMapping 注解及其子类注解( PostMapping 、 GetMapping )的 headers 选项中设置。 在接口方法的参数中使用 @RequestHeader 注解。 如果只是要设置ContentType的话,还可以在 RequestMapping 注解的 consumes 选项中设置。 @FeignClient(name = "remote-service",configuration = FeignConfiguration.class) public interface RemoteCall {

@PostMapping(value = "/api/auth/login?ts={ts}",
        consumes = {"application/x-www-form-urlencoded"},
        headers = {"Cookie=xxxx=yyyy"}
)
LoginResponse login(URI uri, @PathVariable("ts") long ts,Map<String,String> param);

} 复制代码 重新发起请求,结果返回还是报错,报错原因是数据格式有问题,日志打印出来的请求如下: [DEBUG][2019-12-07T18:15:30.489+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] ---> POST http://127.0.0.1:8080/api/auth/login?ts=1575713098 HTTP/1.1 [DEBUG][2019-12-07T18:15:30.489+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] Content-Length: 209 [DEBUG][2019-12-07T18:15:30.489+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] Content-Type: application/x-www-form-urlencoded; charset=UTF-8 [DEBUG][2019-12-07T18:15:30.489+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] Cookie: xxxx=yyyy [DEBUG][2019-12-07T18:15:30.490+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] modCount=4&size=4&threshold=12&&table=password%3Dxxxxxx&table=phone%3D18621019537&table=nickname%3Dxxxx&table=avatar%3Dxxxxx [DEBUG][2019-12-07T17:15:30.490+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] ---> END HTTP (209-byte body) 复制代码 现在ContentType正确了,可是为什么编码之后的参数不对呢? 现在碰到了踩的第二个坑:编码之后的参数很奇怪:多了modCount、size和threshold字段,且传入的参数都在table字段后面。 第二个坑 根据文档,传给body的参数只需要放在Map中,Feign框架会自动解析到body中,但是现在编码之后的参数怎么看都像是HashMap本身的字段,为什么会出现这种情况呢? 同样的来源码中找答案吧! 根据前面的源码分析,我们了解到,每次请求最终都会执行到 SynchronousMethodHandler.invoke() 中,重新把 invoke() 方法代码列在下面: 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) { // ... 省略重试代码 } } } 复制代码 invoke() 方法会首先构建 RequestTemplate 对象,然后发起请求。 进行编码肯定是发生在发送请求之前,所以肯定是在 buildTemplateFromArgs.create() 方法中进行的, create() 方法实现如下: public RequestTemplate create(Object[] argv) { RequestTemplate mutable = RequestTemplate.from(metadata.template());

// ... 省略部分代码 RequestTemplate template = resolve(argv, mutable, varBuilder);

// ... 省略代码 return template; } 复制代码 在 create() 中主要调用 resolve() 方法完成解析,而 resolve() 主要是调用 BuildEncodedTemplateFromArgs.resolve() 方法: protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) { // 找到body参数 Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex()); try { // 进行编码 encoder.encode(body, metadata.bodyType(), mutable); } catch (EncodeException e) { throw e; } catch (RuntimeException e) { throw new EncodeException(e.getMessage(), e); } return super.resolve(argv, mutable, variables); } } 复制代码 在 BuildEncodedTemplateFromArgs.resolve() 方法中首先会找到属于body的参数,在本例中也就是本文传入的 Map<String,String> 对象,然后调用 encoder.encode() 方法。 在本例的 FeignConfiguration 中配置了一个 encoder : @Beanpublic Encoder feignFormEncoder() { return new FormEncoder(new SpringEncoder(messageConverters)); } 复制代码 所以最终调用的还是 FormEncoder 类中的 encode() 方法,实现如下: public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException { // 1.先找到本次请求设置的ContentType类型 String contentTypeValue = getContentTypeValue(template.headers()); val contentType = ContentType.of(contentTypeValue); if (!processors.containsKey(contentType)) { delegate.encode(object, bodyType, template); return; }

Map<String, Object> data; // 2. 根据参数的类型,来判断是否应该对参数做进一步处理 if (MAP_STRING_WILDCARD.equals(bodyType)) { data = (Map<String, Object>) object; } else if (isUserPojo(bodyType)) { data = toMap(object); } else { delegate.encode(object, bodyType, template); return; }

val charset = getCharset(contentTypeValue); processors.get(contentType).process(template, charset, data); } 复制代码 在 encode() 会根据传入的body参数的类型来判断要不要对参数做进一步处理:  如果bodyType是 MAP_STRING_WILDCARD 类型,则只是简单的做一个类型转换就好。  MAP_STRING_WILDCARD 类型就是表示 Map<String,?> 类型。在Fegin中,如果参数类型是 Map<String,?> ,则表示这个参数是要进行表单编码。   如果参数的名字不是以"class java."开头的,则强制转换成 Map<String,Object> 对象。   如果都不是,则直接进行编码操作。  本例中传入的类型是 java.util.Map<java.lang.String, java.lang.String> ,正好符合第二条,则被调用 toMap() 方法转换成 Map<String,Object> 对象了。 toMap() 实现如下: public static Map<String, Object> toMap (@NonNull Object object) { val result = new HashMap<String, Object>(); val type = object.getClass(); val setAccessibleAction = new SetAccessibleAction(); for (val field : type.getDeclaredFields()) { //对所有的属性进行遍历 val modifiers = field.getModifiers(); // 忽略掉final和static属性 if (isFinal(modifiers) || isStatic(modifiers)) { continue; } setAccessibleAction.setField(field); AccessController.doPrivileged(setAccessibleAction);

val fieldValue = field.get(object);
if (fieldValue == null) {
  continue;
}

val propertyKey = field.isAnnotationPresent(FormProperty.class)
                  ? field.getAnnotation(FormProperty.class).value()
                  : field.getName();

result.put(propertyKey, fieldValue);

} return result; } 复制代码 toMap() 会把传入参数的所有的非 static 和非 final 类型的变量放入一个Map中,key是属性名,value是属性值。 在本文中传入的是 Map<String,String> 类型的Map,而每一个Map中都有 modCount 、 threshold 、 size 这几个属性,且Map中的数据是保存在名为 table 的数组中的,在经过 toMap之后变成了一个新的Map,新的Map中就出现了 modCount 、 threshold 、 size 和 table 这几个属性,我们原本传入的数据都以数组的形式保存在在key为 table 的值中。 所以最终发起请求的时候发出去的 body 变成了 modCount=4&size=4&threshold=12&&table=password%3Dxxxxxx&table=phone%3D18621019537&table=nickname%3Dxxxx&table=avatar%3Dxxxxx 复制代码 这种。 修复:只需要把接口定义中的参数类型 Map<String,String>换成Map<String,?> 就可以了。 第三个坑 在使用的过程中,因为要排查问题所以需要打印出http请求的数据,所以需要配置feign打印日志功能。 按照文档配置(设置 Logger.Level=Logger.Level.FULL 以及在properties中设置对应的接口 logging.level.com.beautyboss.slogen.resource.call.UserCall=DEBUG )之后,日志还是死活打印不出来。 排查很久之后,才找到原因: logback中配置的日志打印级别是info,而feign日志只能打印debug级别的,info级别高于debug ,所以导致日志死活打不出来。 只需要把logback的日志级别设置为debug就可以打印出来了。 但是问题来了,线上也想要打印出feign日志的话,logback只能设置debug级别,那么线上就会有很多debug的日志,这对线上环境来说是不能接受的。 那么,怎么样才能即打印出feign的日志,又不影响其他的日志呢? 简单!只需要在logback的配置文件中新增一个appender和logger,指定日志级别为debug,专门用来打印feign的请求就好了。 debug // 指定日志级别为debug {root.log.path}/rpc.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>{root.log.path}/rpc.log.%d{yyyyMMdd} 30 [%level][%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}][%thread:%file:%line] %msg%n UTF-8

复制代码 其他 总的来说,第一次使用feign还是碰到不少问题的。 对于碰到的问题,不一定要看源码才能解决,比如本例中,发现header设置没有生效,只需要Google下Feign怎么设置header就能解决问题的。 但是我觉得,作为现代的程序猿,基本上都是框架工程师,大部分的工作都依赖于各种框架。对于自己开发工作中使用到的框架,最好能够做到了然于胸,不要求对框架的具体实现的方方面面都要清楚,最起码要对请求的来龙去脉有所了解。 要做到知其然,更要知其所以然。