FeIGN Series (04) Contract Source Code Analysis

Feign series (04) Contract source code analysis

[TOC]

Spring Cloud series catalog (https:// www.cnblogs.com/binarylei/p/11563952.html#feign)

In the previous article, we roughly analyzed the working principle of Feign. So how does Feign adapt to Feign, JAX-RS 1/2’s REST declarative annotations, and parse method parameters into Http request lines, request headers, and request bodies? Here we have to mention the Contract interface.

1. Feign parameter encoding overall process

Figure 1: Feign parameter encoding overall process

sequenceDiagram participant Client Contract ->> MethodMetadata: 1. Parse method meta-information: parseAndValidatateMetadata(Class targetType) MethodMetadata ->> RequestTemplate.Factory: 2. Encapsulate MethodMetadata: buildTemplate RequestTemplate.Factory ->> RequestTemplate: 3 . Analysis method parameters: create(argv) RequestTemplate.Factory ->> Request: 4. request Client ->> Request: 5. Send Http request: execute(Request request, Options options)

Summary: The first two steps are the Feign proxy generation phase, which resolves method parameters and annotation meta-information. The last three steps are the invocation phase, which encodes the method parameters into the data format of the Http request.

public interface Contract {
List parseAndValidatateMetadata(Class targetType);
}

Summary: Contract The interface parses the methods and annotations in each interface in UserService into MethodMetadata, and then encodes them as a Request using RequestTemplate#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: After RequestTemplate#request is encoded as a Request, you can call Client#execute to send an Http request.

public interface Client {
Response execute(Request request, Options options) throws IOException;
}

Summary: Client’s Specific implementations include HttpURLConnection, Apache HttpComponnets, OkHttp3, Netty, etc. This article focuses on the first three steps: Feign method meta-information analysis and parameter encoding process.

2. Contract method annotation and meta-information analysis

The default Contract.Default of Feign is Example:

First review the use of Feign annotations (@RequestLine @Headers @Body @Param @HeaderMap @QueryMap):

< pre>@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 queryMap,
@HeaderMap Map headerMap, User user );
}

Figure 2: Contract method meta-information analysis

sequenceDiagram Contract ->> Method: 1. processAnnotationOnClass Contract ->> Method: 2. processAnnotationOnMethod Contract ->> Method: 3. processAnnotationsOnParameter Note right of Method: Validity will also be verified during parsing

< strong>Summary: Contract.BaseContract#parseAndValidatateMetadata will traverse and parse each method in UserService, and parse it into MethodMetadata according to the annotations on the interface class, method, and parameter.

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. Analyze the annotations on the class
if (targetType. getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType);

// 2. Annotations on the analysis method
for (Annotation methodAnnotation: method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
Class [] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();

Annotation[][] parameterAnnotations = method.getParameterAnnotations();< br /> int count = parameterAnnotation s.length;
for (int i = 0; i // isHttpAnnotation indicates whether there is an annotation on the parameter
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
// There is no annotation on method parameters
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
// @FormParam JAX-RS specification has been set
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.");
/ / BodyIndex has been set, such as 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, target Type, genericParameterTypes[i]));
}
}

return data;
}

This method is also very easy to understand, then Let’s take a look at the specific parsing process of these annotations @RequestLine @Headers @Body @Param @HeaderMap @QueryMap.

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> headers = toMap(headersOnType);
headers.putAll(data.template().headers( ));
data.template().headers(null); // to clear
data.template().headers(headers);
}
}

Summary: There is only one annotation on the class:

  1. @Headers -> data.template().headers

2.2 processAnnotationOnMethod

protected void processAnnotationOnMethod(
MethodMetadata data, Annotation methodAnnotation, Metho d method) {
Class 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).de codeSlash());
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 three annotations on the method: p>

    @RequestLine -> data.template().method + data.template().uri
  1. @Body -> data.template().body
  2. @Headers -> data.template().headers

2.3 processAnnotationsOnParameter

protected boolean processAnnotationsOnParameter(
MethodMetadata data, Annotation[] annotations,int paramIndex ) {
boolean isHttpAnnotation = false;
for (Annotation annotation: annotations) {
Class 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 expander = paramAnnotation.expander();
if (expander != Param.ToStringExpander.class) {
data.indexToExpanderClass().put(paramIndex, expander);
}
data.indexToEncoded().put(paramIndex, paramAnnotation. encoded());
isHttpAnnotation = true;
// That is not the parameters on @Headers and @Body, it can only be 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,
"H eaderMap annotation was present on multiple parameters.");
data.headerMapIndex(paramIndex);
isHttpAnnotation = true;
}
}
return isHttpAnnotation;
}

Summary: There may be three comments on the parameter:

  1. @Param-> data.indexToName

    < /li>

  2. @QueryMap-> data.queryMapIndex

  3. @HeaderMap-> data.headerMapIndex

    Table 1: Feign annotation analysis corresponding value

    < /tr>

    Feign annotation Analysis value in MethodMetadata
    @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

Okay, I have explained it for a long time, all for the purpose of parsing the meta-information of the method, the purpose is to shield Feign, JAX-RS 1/2, Spring Web MVC and other REST declarative annotations Difference, what kind of information does MethodMetadata have?

private String configKey; // Method signature, class full name + method full name
private transient Type returnType; // method return value type
private Integer urlIndex; // When the method parameter is url, it is urlIndex
private Integer bodyIndex; // The method parameter has no task annotation, and the default is bodyIndex
private Integer headerMapIndex; // @HeaderMap
private Integer queryMapIndex; // @ QueryMap
private boolean queryMapEncoded;
private transient Type bodyType;
private RequestTemplate template = new RequestTemplate(); // core
private List formParams = new ArrayList ();
private Map> indexToName =
new LinkedHashMap>();
private Map> indexToExpanderClass =
new LinkedHashMap>();
private Map indexToEncoded = new LinkedHashMap();
private transient Map indexToExpander;

Summary: So far, the method of Method The parameters have been parsed into MethodMetadata. When the method is called, argv will be parsed into Request according to the meta-information of MethodMetadata.

3. Parameter parsing into Request

Take BuildTemplateByResolvingArgs as an example.

public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = RequestTemplate.from(metadata.template());
// 1. Parse url parameters
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. Parse the parameter argv into the corresponding object
Map< String, Object> varBuilder = new LinkedHashMap();
for (Entry> 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. Parameter placeholder in @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 queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}

// 5. @HeaderMap
if (metadata.headerMapIndex() != null) {
template =
addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template);
}

return template;
}

Summary: After parsing the parameters of the method into RequestTemplate, it is simple, only need to call request to finally parse into Request. You can see that Request contains all the information of the Http request. At this point, Feign’s parameter analysis is complete.

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: How is Feign compatible with JAX-RS 1/2 and Spring Web MVC

Presumably you have already guessed that you only need to implement your own contract and analyze the corresponding annotation information After becoming MethodMetadata, the adaptation can be completed.

  1. jaxrs Feign native support, if you are interested, you can take a look at its implementation: feign.jaxrs.JAXRSContract
  2. Spring Web MVC Spring Cloud OpenFeign provides support

Record a little bit of heart every day. Content may not be important, but habits are important!

sequenceDiagram participant Client Contract ->> MethodMetadata: 1. Parse method meta information: parseAndValidatateMetadata(Class targetType) MethodMetadata ->> RequestTemplate.Factory: 2. Encapsulate MethodMetadata: buildTemplate RequestTemplate .Factory ->> RequestTemplate: 3. Analysis method parameters: create(argv) RequestTemplate.Factory ->> Request: 4. request Client ->> Request: 5. Send Http request: execute(Request request, Options options)

sequenceDiagram Contract ->> Method: 1. processAnnotationOnClass Contract ->> Method: 2. processAnnotationOnMethod Contract ->> Method: 3. processAnnotationsOnParameter Note right of Method: Validity will also be verified during parsing

Leave a Comment

Your email address will not be published.