Feign series (04) Contract parse the source code
[TOC]
Spring Cloud Series catalog ( https://www.cnblogs.com/binarylei/p/11563952.html#feign )
In the previous article we analyzed the roughly what Feign
works, and that Feign
in the end is how to adapt Feign, JAX-RS 1/2 of REST declarative annotations, the parameters of the method to resolve the row Http request, the request headers, request body do? Here we have to mention Contract
this interface.
1. Feign overall flow encoding parameters
Summary: The first two steps are Feign
Acting generation phase, parameters and analytical methods annotated meta-information. After three steps calling phase, the coding method parameter into a data format of the Http request.
public interface Contract {
List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}
Summary: Contract UserService interface method in each interface in its resolve to comment MethodMetadata, then use RequestTemplate # request encoded as a Request.
public final class RequestTemplate implements Serializable {
public Request request() {
if (!this.resolved) {
throw new IllegalStateException("template has not been resolved.");
}
return Request.create(this.method, this.url(), this.headers(), this.requestBody());
}
}
Summary: requestTemplate # Request encoded as a Request after you can call the Client # execute sends Http request.
public interface Client {
Response execute(Request request, Options options) throws IOException;
}
Summary: concrete realization Client has HttpURLConnection, Apache HttpComponnets, OkHttp3, Netty and so on. This article focuses on the first three steps: Feign method that is meta information analysis and parameter encoding process.
2. Contract annotations and meta information analysis method
To Feign
default Contract.Default
as an example:
First, look at Feign
the use of annotations ( @RequestLine @Headers @Body @Param @HeaderMap @QueryMap
):
@Headers("Content-Type: application/json")
interface UserService {
@RequestLine("POST /user")
@Headers("Content-Type: application/json")
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void user(@Param("user_name") String name, @Param("password") String password,
@QueryMap Map<String, Object> queryMap,
@HeaderMap Map<String, Object> headerMap, User user);
}
Summary: Contract.BaseContract#parseAndValidatateMetadata
will traverse each of resolved UserService method according annotations on the interface class, the method, the parameter, which is parsed into MethodMetadata.
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
MethodMetadata data = new MethodMetadata();
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
data.configKey(Feign.configKey(targetType, method));
// 1. 解析类上的注解
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType);
// 2. 解析方法上的注解
for (Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
Class<?>[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
// isHttpAnnotation 表示参数上是否有注解存在
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) {
// 已经设置过 @FormParam JAX-RS规范
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.");
// 已经设置过 bodyIndex,如 user(User user1, Person person) ×
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;
}
This method is also well understood, let's look at @RequestLine @Headers @Body @Param @HeaderMap @QueryMap
the specific resolution process these annotations.
2.1 processAnnotationOnClass
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> targetType) {
if (targetType.isAnnotationPresent(Headers.class)) {
String[] headersOnType = targetType.getAnnotation(Headers.class).value();
checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
targetType.getName());
Map<String, Collection<String>> headers = toMap(headersOnType);
headers.putAll(data.template().headers());
data.template().headers(null); // to clear
data.template().headers(headers);
}
}
Summary: the class has only one comment:
- @Headers -> data.template().headers
2.2 processAnnotationOnMethod
protected void processAnnotationOnMethod(
MethodMetadata data, Annotation methodAnnotation, Method method) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
if (annotationType == RequestLine.class) {
String requestLine = RequestLine.class.cast(methodAnnotation).value();
checkState(emptyToNull(requestLine) != null,
"RequestLine annotation was empty on method %s.", method.getName());
Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
if (!requestLineMatcher.find()) {
throw new IllegalStateException(String.format(
"RequestLine annotation didn't start with an HTTP verb on method %s",
method.getName()));
} else {
data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1)));
data.template().uri(requestLineMatcher.group(2));
}
data.template().decodeSlash(RequestLine.class.cast(methodAnnotation).decodeSlash());
data.template()
.collectionFormat(RequestLine.class.cast(methodAnnotation).collectionFormat());
} else if (annotationType == Body.class) {
String body = Body.class.cast(methodAnnotation).value();
checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.",
method.getName());
if (body.indexOf('{') == -1) {
data.template().body(body);
} else {
data.template().bodyTemplate(body);
}
} else if (annotationType == Headers.class) {
String[] headersOnMethod = Headers.class.cast(methodAnnotation).value();
checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.",
method.getName());
data.template().headers(toMap(headersOnMethod));
}
}
Summary: There may be annotated on three methods:
- @RequestLine -> data.template().method + data.template().uri
- @Body -> data.template().body
- @Headers -> data.template().headers
2.3 processAnnotationsOnParameter
protected boolean processAnnotationsOnParameter(
MethodMetadata data, Annotation[] annotations,int paramIndex) {
boolean isHttpAnnotation = false;
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType == Param.class) {
Param paramAnnotation = (Param) annotation;
String name = paramAnnotation.value();
checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
paramIndex);
nameParam(data, name, paramIndex);
Class<? extends Param.Expander> expander = paramAnnotation.expander();
if (expander != Param.ToStringExpander.class) {
data.indexToExpanderClass().put(paramIndex, expander);
}
data.indexToEncoded().put(paramIndex, paramAnnotation.encoded());
isHttpAnnotation = true;
// 即不是@Headers和@Body上的参数,只能是formParams了
if (!data.template().hasRequestVariable(name)) {
data.formParams().add(name);
}
} else if (annotationType == QueryMap.class) {
checkState(data.queryMapIndex() == null,
"QueryMap annotation was present on multiple parameters.");
data.queryMapIndex(paramIndex);
data.queryMapEncoded(QueryMap.class.cast(annotation).encoded());
isHttpAnnotation = true;
} else if (annotationType == HeaderMap.class) {
checkState(data.headerMapIndex() == null,
"HeaderMap annotation was present on multiple parameters.");
data.headerMapIndex(paramIndex);
isHttpAnnotation = true;
}
}
return isHttpAnnotation;
}
Summary: There may be annotated on three parameters:
@Param-> data.indexToName
@QueryMap-> data.queryMapIndex
@HeaderMap-> data.headerMapIndex
Table 1: Feign annotated parse the corresponding value Feign comment MethodMetadata the analytical value @Headers data.template().headers @RequestLine data.template().method + data.template().uri @Body data.template().body @Param data.indexToName @QueryMap data.queryMapIndex @HeaderMap data.headerMapIndex
2.4 MethodMetadata
Well, for a long time to explain above, it is for information analytical element method, the purpose is to shield Feign、JAX-RS 1/2、Spring Web MVC
and other differences REST declarative annotations, in the end there are those that MethodMetadata information it?
private String configKey; // 方法签名,类全限名+方法全限名
private transient Type returnType; // 方法返回值类型
private Integer urlIndex; // 方法参数为url时,为 urlIndex
private Integer bodyIndex; // 方法参数没有任务注解,默认为 bodyIndex
private Integer headerMapIndex; // @HeaderMap
private Integer queryMapIndex; // @QueryMap
private boolean queryMapEncoded;
private transient Type bodyType;
private RequestTemplate template = new RequestTemplate(); // 核心
private List<String> formParams = new ArrayList<String>();
private Map<Integer, Collection<String>> indexToName =
new LinkedHashMap<Integer, Collection<String>>();
private Map<Integer, Class<? extends Expander>> indexToExpanderClass =
new LinkedHashMap<Integer, Class<? extends Expander>>();
private Map<Integer, Boolean> indexToEncoded = new LinkedHashMap<Integer, Boolean>();
private transient Map<Integer, Expander> indexToExpander;
Summary: So far to the parameter method Method has been parsed into MethodMetadata, when the method is called, the yuan will become Request according to information MethodMetadata will argv resolution.
3. Request parameter parsing into
To BuildTemplateByResolvingArgs example.
public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = RequestTemplate.from(metadata.template());
// 1. 解析url参数
if (metadata.urlIndex() != null) {
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null,
"URI parameter %s was null", urlIndex);
mutable.target(String.valueOf(argv[urlIndex]));
}
// 2. 解析参数argv成对应的对象
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
for (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);
}
}
}
// 3. @Body中的参数占位符
RequestTemplate template = resolve(argv, mutable, varBuilder);
// 4. @QueryMap
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);
template = addQueryMapQueryParameters(queryMap, template);
}
// 5. @HeaderMap
if (metadata.headerMapIndex() != null) {
template =
addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
}
return template;
}
Summary: The method parameters resolve to RequestTemplate after simple, just call request to final resolution to Request. You can see the Request contains all the information Http request. This, the argument parsing Feign completed.
public Request request() {
if (!this.resolved) {
throw new IllegalStateException("template has not been resolved.");
}
return Request.create(this.method, this.url(), this.headers(), this.requestBody());
}
4. Thinking: Feign how compatible JAX-RS 1/2, Spring Web MVC
Surely we have guessed, only to realize their Contract, the annotation information corresponding to the parsed into MethodMetadata, to complete the adaptation work.
jaxrs
Feign native support, interested can look at its implementation:feign.jaxrs.JAXRSContract
Spring Web MVC
Spring Cloud OpenFeign provides support
The intentions of recording a little bit every day. Perhaps the content is not important, but the habit is very important!