背景
最近要对接公司信息中心那边提供的注册登录接口,这些接口是以HTTP形式对外提供的。之前没怎么用过HTTP框架,宇神推荐使用Feign。第一次听说Feign这么个框架,去查了下,原来是SpringCloud的一份子。使用Spring Cloud Feign,调用HTTP的时候只需要创建一个接口并用注解的方式来配置,就可以完成对服务提供方的接口绑定,然后直接注入使用就可以了,用起来还是很方便的。
信息中心提供的接口是application/x-www-form-urlencoded
格式编码的POST
请求,http header里面需要传入cookie,每次请求的服务器的IP是不一样的,需要每次查询新的服务器地址。
问题及排查分析
基于以上场景,Google下Fegin的基本用法,很快就写出了代码.
首先是自定义接口,使用注解来配置
@FeignClient(name = "remote-service",configuration = FeignConfiguration.class)
public interface RemoteCall {
@PostMapping(value = "/api/auth/login?ts={ts}")
@Headers({
"Cookie: xxx=yyy",
"ContentType: application/x-www-form-urlencoded"
})
//传给body进行x-www-form-urlencoded编码的参数统一放到map中,feign会自动解析成body的数据
LoginResponse login(URI uri, @PathVariable("ts") long ts,Map<String,String> param);
}
复制代码
FeignConfiguration
类实现如下:
@Configuration
public class FeignConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Encoder feignFormEncoder() {
return new FormEncoder(new SpringEncoder(messageConverters));
}
@Bean
public Logger.Level logger() {
// 为了方便问题排查,把http的请求和响应全部打印出来
return Logger.Level.FULL;
}
}
复制代码
然后只需要在调用HTTP请求的地方注入RemoteCall
就可以了
@Repository
public class RemoteDal implements RemoteRepository {
@Resource
private RemoteCall remoteCall;
@Override
public Long login(String phone, String md5Password, long ts) {
URI uri = getUri(); // 获取uri
Map<String,String> para = buildLoginParas(phone, md5Password);
LoginResponse response = remoteCall.login(uri,ts,para);
return response.getUserId();
}
private Map<String,String> buildLoginParas(String phone,String md5Password) {
Map<String,String> para = new HashMap<>();
para.put("phone",phone);
para.put("password",md5Password);
para.put("nickname","xxxx");
para.put("avatar","xxxx");
return para;
}
}
复制代码
程序写好后执行一遍,接口返回报错,显示cookie
不能为空,说明cookie没有正确的设置,但是明明有在header里面设置cookie。 这个时候只需要看一下打印出来的的HTTP的请求,就一目了然了。
[DEBUG][2019-12-07T18:05:12.874+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:05:12.875+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] Content-Length: 151
[DEBUG][2019-12-07T18:05:12.875+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] Content-Type: application/json
[DEBUG][2019-12-07T18:05:12.876+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login]
[DEBUG][2019-12-07T18:05:12.876+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] {"password":"xxxxxx","phone":"18621019537","nickname":"xxxx","avatar":"xxxxx"}
[DEBUG][2019-12-07T18:05:12.876+0800][http-nio-8081-exec-1:Slf4jLogger.java:72] [RemoteCall#login] ---> END HTTP (151-byte body)
复制代码
从日志里面可以很明显看到,通过@Headers
注解设置的header
都没有生效,所以cookie
和ContentType
为空,编码方式设被置为默认的application/json
。
踩到的第一个坑:为什么@Headers
注解没生效呢?
第一个坑
源码面前,了无秘密,来一块从Feign
的源码中找答案吧。
但是源码的入口在哪里呢?
在Java里面,一般这种只需要配置接口和注解就可以直接使用的框架的本质基本上都是通过代理和反射来实现的,而在Spring里面,一般都会有对应的FactoryBean
,在idea中连续按下两次Command+N
,输入FeignFactoryBean
,结果如下图所示:
果然有一个类FeignClientFactoryBean
,那么入口应该就是它了。
FeignClientFactoryBean
的getObject()
方法直接调用getTarget()
方法返回生成的对象,
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context); // 构造器模式构造Feign对象
// ... 省略参数校验的代码
String url = this.url + cleanPath();
// 如果配置了client,则设置client
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class); // 生成的是HystrixFeignTargeterConfiguration对象,参考FeignAutoConfiguration.java
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
复制代码
getTarget()
方法初始化FeignContext
上下文以及做了必要的参数校验之后,会从Spring上下文中生成Target
对象(Target
对象会生成最终的代理对象),接着调用生成的Target
对象的target()
方法生成代理对象,Target.target()
方法实际上还是调用传入的Feign.Builder
对象的target()
方法:
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
复制代码
target()
方法首先调用build()
方法完成最终的Feign
对象的构建,生成的是ReflectiveFeign
对象:
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode, propagationPolicy);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
// 生成实际的feign对象
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
复制代码
build()
首先会构造MethodHander
工厂类以及根据名字获取MethodHandler
的解析类之后,直接生成ReflectiveFeign
类对象作为最终的Feign
对象。
接着继续调用刚生成返回的ReflectiveFeign
对象的newInstance()
方法生成代理对象:
public <T> T newInstance(Target<T> target) {
// 首先解析定义的FeignClient接口,其中定义的每一个方法都是一个MethodHandler
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
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;
}
复制代码
newInstance()
方法的入参是自定义的FeicnClient
注解的接口,在本例中就是RemoteCall
类,在newInstance()
方法中首先调用targetToHandlersByName.apply()
方法,targetToHandlersByName
是ParseHandlersByName
类实例,该实例是在build()
方法中生成的。从名字可以看出,这个类的作用是解析自定义的FeicnClient
注解了的接口,把其中的每一个方法都生成一个MethodHander
对象。
那么可以想象的到,对接口中方法设置的参数极有可能是在这个方法中进行解析的,继续跟进去瞧一瞧:
public Map<String, MethodHandler> apply(Target key) {
// 首先校验并解析自定义的FeignClient注解的接口配置
List<MethodMetadata> 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()
中首先会调用Contract.parseAndValidatateMetadata()
方法校验并解析自定义的FeicnClient
注解的接口(Contract
中定义了Feign中可使用的注解和对应的值),
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
// ... 省略校验代码
for (Method method : targetType.getMethods()) {
// 校验解析定义
MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
metadata.configKey());
result.put(metadata.configKey(), metadata);
}
return new ArrayList<>(result.values());
}
复制代码
parseAndValidatateMetadata()
方法首先会进行必要的参数校验,校验通过之后会调用parseAndValidateMetadata()
方法完成具体的解析动作:
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
MethodMetadata data = new MethodMetadata(); // 实例化一个对象
// ... 省略设置必要的参数
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
// 解析类(接口)级别的注解
processAnnotationOnClass(data, targetType);
for (Annotation methodAnnotation : method.getAnnotations()) {\
// 解析方法级别的注解
processAnnotationOnMethod(data, methodAnnotation, method);
}
// ... 省略部分代码
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
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 && parameterTypes[i] != Request.Options.class) {
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()
方法的主要功能就是解析自定义的方法并且生成对应的MethodMetadata
对象,所以在具体实现中首先生成一个MethodMetadata
对象,并且设置必要的参数之后就开始解析注解了。
在Feign中,注解可以分为三类:类(接口)级别的注解,方法级别的注解以及方法参数级别的注解。
processAnnotationOnClass()
方法用来解析类级别的注解。在Feign中实现在SpringMvcContract
类中,
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
if (clz.getInterfaces().length == 0) {
// 只处理RequestMapping注解
RequestMapping classAnnotation = findMergedAnnotation(clz,
RequestMapping.class);
if (classAnnotation != null) {
// 填充url里面的路径变量
if (classAnnotation.value().length > 0) {
String pathValue = emptyToNull(classAnnotation.value()[0]);
pathValue = resolve(pathValue);
if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
data.template().uri(pathValue);
}
}
}
}
复制代码
从实现中可以看到,Feign中只会处理类级别的RequestMapping
注解:如果注解中的url配置有路径变量的话,则会进行填充。
processAnnotationOnMethod()
方法会处理方法级别的注解,其实现同样是在SpringMvcContract
中:
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation methodAnnotation, Method method) {
// 对于方法级别的注解,同样也是只处理RequestMapping注解
if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation
.annotationType().isAnnotationPresent(RequestMapping.class)) {
return;
}
RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
// HTTP Method
RequestMethod[] methods = methodMapping.method();
if (methods.length == 0) {
// 默认是get请求
methods = new RequestMethod[] { RequestMethod.GET };
}
checkOne(method, methods, "method");
data.template().method(Request.HttpMethod.valueOf(methods[0].name()));
// url中的路径变量填充
checkAtMostOne(method, methodMapping.value(), "value");
if (methodMapping.value().length > 0) {
String pathValue = emptyToNull(methodMapping.value()[0]);
if (pathValue != null) {
pathValue = resolve(pathValue);
// Append path from @RequestMapping if value is present on method
if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
pathValue = "/" + pathValue;
}
data.template().uri(pathValue, true);
}
}
// produces
parseProduces(data, method, methodMapping);
// consumes
parseConsumes(data, method, methodMapping);
// headers
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
}
复制代码
在processAnnotationOnMethod()
方法的一开始,就会进行校验:如果不是RequestMapping
(及其子类)注解,则直接返回不处理。
这里就解释了为什么通过@Headers注解设置的ContentType和Cookie每一偶生效了:因为Feign压根就不处理@Headers注解。
在processAnnotationOnMethod()
中同样会检查url中是否有路径变量,有的话则填充路径变量。
接下来会连续调用三个方法:parseProduces()
、parseConsumes()
和parseHeaders()
分别解析方法上的produces、consumes和headers。
private void parseProduces(MethodMetadata md, Method method,
RequestMapping annotation) {
// 只是简单的把RequestMapping注解的produces选项设置为HTTP请求的ACCEPT参数
String[] serverProduces = annotation.produces();
String clientAccepts = serverProduces.length == 0 ? null
: emptyToNull(serverProduces[0]);
if (clientAccepts != null) {
md.template().header(ACCEPT, clientAccepts);
}
}
复制代码
parseProduces()
方法只是从RequestMapping
注解上获取produces
配置,然后设置为HTTP的Accept的配置。
private void parseConsumes(MethodMetadata md, Method method,
RequestMapping annotation) {
// 把RequestMapping注解的consumes选项设置为HTTP请求的CONTENT_TYPE
String[] serverConsumes = annotation.consumes();
String clientProduces = serverConsumes.length == 0 ? null
: emptyToNull(serverConsumes[0]);
if (clientProduces != null) {
md.template().header(CONTENT_TYPE, clientProduces);
}
}
复制代码
parseConsumes()
方法只是简单的把RequestMapping注解的consumes
选项的配置设置为HTTP请求的ContentType。
从这里可以看出要设置ContentType,只需要在方法级别的RequestMapping注解中设置consumes选项即可。
private void parseHeaders(MethodMetadata md, Method method,
RequestMapping annotation) {
// TODO: only supports one header value per key
if (annotation.headers() != null && annotation.headers().length > 0) {
for (String header : annotation.headers()) {
int index = header.indexOf('=');
if (!header.contains("!=") && index >= 0) {
md.template().header(resolve(header.substring(0, index)),
resolve(header.substring(index + 1).trim()));
}
}
}
}
复制代码
从上面代码可以看出,parseHeaders()
方法会把RequestMapping注解的headers
选项配置的以"="分隔的键值对分解出来设置到HTTP请求的header中去。
在执行完processAnnotationOnMethod()
方法之后,重新回到parseAndValidateMetadata()
方法中来:
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
// ... 省略部分代码
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
// 循环遍历每一个参数,如果参数上有注解的话则处理注解
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
// 如果参数是URI类型的,则标志该参数为url,可以实现动态变更请求URL
if (parameterTypes[i] == URI.class) {
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<MethodMetadata> 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> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
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
:
@Bean
public 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的请求就好了。
<appender name="RPC_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level> // 指定日志级别为debug
</filter>
<file>${root.log.path}/rpc.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${root.log.path}/rpc.log.%d{yyyyMMdd}</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>[%level][%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ}][%thread:%file:%line] %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<logger name="com.beautyboss.slogen.resource.call" level="debug" additivity="true">
<appender-ref ref="RPC_FILE"/>
</logger>
复制代码
其他
总的来说,第一次使用feign还是碰到不少问题的。
对于碰到的问题,不一定要看源码才能解决,比如本例中,发现header设置没有生效,只需要Google下Feign怎么设置header就能解决问题的。
但是我觉得,作为现代的程序猿,基本上都是框架工程师,大部分的工作都依赖于各种框架。对于自己开发工作中使用到的框架,最好能够做到了然于胸,不要求对框架的具体实现的方方面面都要清楚,最起码要对请求的来龙去脉有所了解。
要做到知其然,更要知其所以然。