Feign的核心API:Contract
前言
最近碰到一个比较有意思的问题:Open Feign如何增加自定义注解的支持。比如增加的注解类似RequestBody,但是可以
使用在非复杂对象上,并且这个注解可能在一个方法,中会有多个。简单说就是不想增加实体类,又想实现类似RequestBody
的作用,在MvC中已经简单实现,但是Feign中咋玩呢?
Feign
Feign是Netflix开发的声明式、模板化的HTTP客户端,其灵感来自Retrofit、JAXRS-2.0以及WebSocket。Feign可帮助我们更加便捷、优雅地调用HTTP API。在Spring Cloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。
Spring Cloud对Feign进行了增强,使Feign支持了Spring MVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。
Contract
上面我们说到Feign是基于接口和注解开发,当真正发送http请求时,就需要对接口上的注解进行转化,提取http请求需要用到的参数,包括但不限于返回类型,参数,url,请求体等;
而Contract
正式用来提取注解上有用信息,封装成MethodMetadata
元数据的。
该接口继承结构如下图:
parseAndValidateMetadata
Contract
接口里唯一 的方法,需要被实现类实现,主要用于组装HTTP请求的MethodMetadata
元信息
BaseContract
内部抽象基类,实现Contract
接口,我们看看BaseContract
实现的parseAndValidateMetadata
方法:
@Override
public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) {
//首先会进行类检验:
//1. 类上不能存在泛型变量
checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
targetType.getSimpleName());
//2. 父接口最多只能存在一个
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());
}
final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
//3. getMethods()获取本类和父类所有方法
for (final Method method : targetType.getMethods()) {
//排除掉object、static、default方法
if (method.getDeclaringClass() == Object.class ||
(method.getModifiers() & Modifier.STATIC) != 0 ||
Util.isDefault(method)) {
continue;
}
//调用本类的parseAndValidateMetadata方法,对每个method转换
final MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
metadata.configKey());
//用metadata的configKey作为result的key
result.put(metadata.configKey(), metadata);
}
return new ArrayList<>(result.values());
}
通过代码我们能看到,Feign对接口是有要求的:
- 接口不能包含泛型
- 接口只能有一个或没有父类接口
- 会排除掉接口里的静态,默认,以及Object的方法
校验通过后,把每个方法解析元信息就落在了parseAndValidateMetadata()
方法上:
其中不同级别的注解解析类都是抽象方法,需要子类去实现的:
- processAnnotationOnClass
- processAnnotationOnMethod
- processAnnotationsOnParameter
这三个抽象方法支持扩展,其中,Feign实现类是DeclarativeContract
,它也是一个抽象类,最终的实现是在Contract.Default
,这也是Feign的默认实现。
这三个方法的另一个实现类是
SpringMvcContract
,springmvc利用它实现了RequestMapping
等注解。
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
final MethodMetadata data = new MethodMetadata();
data.targetType(targetType);
data.method(method);
//方法返回支持泛型
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
//生成唯一的configKey,上面result会用到
data.configKey(Feign.configKey(targetType, method));
//处理class级别的注解,把父类也一起处理了
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType);
//处理method级别的注解
for (final Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
if (data.isIgnored()) {
return data;
}
//校验请求方式:GET或POST
checkState(data.template().method() != null,
"Method %s not annotated with HTTP method type (ex. GET, POST)%s",
data.configKey(), data.warnings());
//参数类型,支持泛型
final Class<?>[] parameterTypes = method.getParameterTypes();
final Type[] genericParameterTypes = method.getGenericParameterTypes();
//参数级别注解
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
final 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 (isHttpAnnotation) {
data.ignoreParamater(i);
}
//参数类型若是URI,url就以此为准
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
if (data.isAlreadyProcessed(i)) {
checkState(data.formParams().isEmpty() || data.bodyIndex() == null,
"Body parameters cannot be used with form parameters.%s", data.warnings());
} else {
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.%s", data.warnings());
checkState(data.bodyIndex() == null,
"Method has too many Body parameters: %s%s", method, data.warnings());
data.bodyIndex(i);
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
}
}
if (data.headerMapIndex() != null) {
checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()],
genericParameterTypes[data.headerMapIndex()]);
}
if (data.queryMapIndex() != null) {
if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
}
}
return data;
}
Default
Default
是Feign的默认实现,类中主要是通过构造器去判断注解类型,再分别调用父类DeclarativeContract
对应的processAnnotationOnClass()、processAnnotationOnMethod()、processAnnotationsOnParameter()
,而DeclarativeContract
是BaseContract
的子类,通过Default
构造器我们能看到Feign具体支持哪些注解:
static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$");
public Default() {
super.registerClassAnnotation(Headers.class, (header, data) -> {
...
});
super.registerMethodAnnotation(RequestLine.class, (ann, data) -> {
...
});
super.registerMethodAnnotation(Body.class, (ann, data) -> {
...
});
super.registerMethodAnnotation(Headers.class, (header, data) -> {
...
});
super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
...
});
super.registerParameterAnnotation(QueryMap.class, (queryMap, data, paramIndex) -> {
...
});
super.registerParameterAnnotation(HeaderMap.class, (queryMap, data, paramIndex) -> {
...
});
}
SynchronousMethodHandler
如果学习过Feign的源码,并且了解Feign的实现机制,那么对这个类不会感到陌生,Feign是基于动态代理实现的,并且是层层代理的invoke()
实现,最终会执行SynchronousMethodHandler
类里的invoke()
方法,发送HTTP请求。
对于被
@FeignClients
注解的接口,我们会根据其属性在IOC容器里注入一个FeignClientFactoryBean
,而FeignClientFactoryBean
实现了FactoryBean
接口,因此实际上我们对该bean进行初始化后得到的是其getObject()
的返回值。这也是我们能够通过类似于调用服务的方法实现http请求发送的关键所在。
通过getObject()
方法最后会来到这样一段代码:
@Override
public <T> T newInstance(Target<T> target) {
...
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
new Class<?>[] {target.type()}, handler);
...
}
这里的proxy
就是代理对象,而handler
,Feign的默认实现是FeignInvocationHandler
,在它的invoke()
方法中,会获取到每个方法对应的SynchronousMethodHandler
,执行其invoke()
方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
//dispatch是一个Map<Method, MethodHandler>,SynchronousMethodHandler是MethodHandler子类
return dispatch.get(method).invoke(args);
}
再来看看SynchronousMethodHandler
的invoke()
方法:
@Override
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = buildTemplateFromArgs.create(argv);
Options options = findOptions(argv);
Retryer retryer = this.retryer.clone();
//注意这里是while(true),如果是需要重试会走continue
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;
}
}
}
executeAndDecode(template, options)
会发送HTTP请求,以及解析Response
,如果出现异常,会抛出RetryableException
,然后进行重试。
总结
以上是关于Feign的核心API,最后的SynchronousMethodHandler
类值得细致研究,很枯燥但也很硬核。
回到最开始的问题,如果要在Feign中自定义一个用于解析注解与接口之间的元数据的类,可以参考Contrac.Default
中三个抽象方法的实现,自己对解析过程进行封装即可。