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
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 {
ListparseAndValidatateMetadata(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
@HeaderMap Map
}
< 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:
- @Headers -> data.template().headers
2.2 processAnnotationOnMethod
protected void processAnnotationOnMethod(
MethodMetadata data, Annotation methodAnnotation, Metho d 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).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>
- @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;
// 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:
-
@Param-> data.indexToName
< /li>
-
@QueryMap-> data.queryMapIndex
-
@HeaderMap-> data.headerMapIndex
Table 1: Feign annotation analysis corresponding value 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 < /tr>
@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 ListformParams = new ArrayList ();
private Map> indexToName =
new LinkedHashMap>();
private Map> indexToExpanderClass =
new LinkedHashMap>();
private MapindexToEncoded = new LinkedHashMap ();
private transient MapindexToExpander;
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()];
MapqueryMap = 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.
jaxrs
Feign native support, if you are interested, you can take a look at its implementation:feign.jaxrs.JAXRSContract
-
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) p>
sequenceDiagram Contract ->> Method: 1. processAnnotationOnClass Contract ->> Method: 2. processAnnotationOnMethod Contract ->> Method: 3. processAnnotationsOnParameter Note right of Method: Validity will also be verified during parsing