This article describes how to map data in a method to request data, such as which are request parameters, which are request bodies, and which are request headers...
Interface
The function of this interface is to parse the methods in the class. Each method resolves to MethodMetadata.
public interface Contract { /** * Contract Provides interfaces. feign's native implementation is BaseContract, and spring integration uses SpringMvcContract */ // TODO: break this and correct spelling at some point List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType); }
The interface has only one method, and the parameters passed in are the meta information of the interface written by the developer; The function is to parse each method into a MethodMetadata object.
Meta information MethodMetadata
// serialize private static final long serialVersionUID = 1L; // Unique identification of each method private String configKey; // Method private transient Type returnType; // If the method parameter has a URI type, remember the index private Integer urlIndex; // Record the index value of the method body private Integer bodyIndex; // The data in the head is encapsulated by map and the index value is recorded private Integer headerMapIndex; // The query data is encapsulated by map and the index value is recorded private Integer queryMapIndex; // Code query map private boolean queryMapEncoded; private transient Type bodyType; // Request data template. Contains the request method, request parameters, request body and url private RequestTemplate template = new RequestTemplate(); private List<String> formParams = new ArrayList<String>(); // Name of each method parameter, key: index position of the parameter; Value: the value in the annotation private Map<Integer, Collection<String>> indexToName = new LinkedHashMap<Integer, Collection<String>>(); // Expander type, pass in an Object object and return a String type.. 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;
The important thing is these properties. The rest are the get and set methods. Unlike JavaBeans, the methods of this class are the same as the property names. If there are parameters, it is the set method, and if there are no parameters, it is the get method. In fact, anything is OK. There is no need to follow the format of JavaBean.
The parsing process introduced here is based on the process of integrating Spring. If you want feign to parse its own annotations, you only need to implement the Contract interface and then implement its own logic.
Implementation class BaseContract
BaseContract#parseAndValidatateMetadata
@Override // Type class information is passed in, which is the interface class information written by the developer. public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) { // Cannot have generics checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s", targetType.getSimpleName()); // There can be at most one inherited interface 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()); } Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>(); // Traversal method, for (Method method : targetType.getMethods()) { // Both default and static methods are skipped if (method.getDeclaringClass() == Object.class || (method.getModifiers() & Modifier.STATIC) != 0 || Util.isDefault(method)) { continue; } // Parse the method information and put it into the cache. 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()); }
This is equivalent to Feign's specification:
- Interfaces cannot have generics
- There can be at most one inherited interface (the inherited interface can no longer inherit the interface, that is, there can be at most one parent interface, and the parent interface can no longer have a parent interface)
Traverse all Public methods of the interface, parse MethodMetadata, put it into the collection and return.
analytic method
BaseContract#parseAndValidateMetadata
Resolve methods in interface classes. Input parameter: targetType: interface class; Method: the method in the interface class.
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) { // Since each method corresponds to a MethodMetadata, you can directly create an object without saying a word. MethodMetadata data = new MethodMetadata(); // Resolve the return value type data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType())); // Create unique ID data.configKey(Feign.configKey(targetType, method)); // If there is an interface, first resolve the of the parent interface, and then resolve the of this interface. if (targetType.getInterfaces().length == 1) { processAnnotationOnClass(data, targetType.getInterfaces()[0]); } // Parsing annotations on classes processAnnotationOnClass(data, targetType); // Annotation on parsing method for (Annotation methodAnnotation : method.getAnnotations()) { processAnnotationOnMethod(data, methodAnnotation, method); } // At this point, the annotation on the method will parse the request method. Check here checkState(data.template().method() != null, "Method %s not annotated with HTTP method type (ex. GET, POST)", method.getName()); // Meta information of each parameter Class<?>[] parameterTypes = method.getParameterTypes(); // Generics for each parameter Type[] genericParameterTypes = method.getGenericParameterTypes(); // Annotation for each method parameter Annotation[][] parameterAnnotations = method.getParameterAnnotations(); int count = parameterAnnotations.length; for (int i = 0; i < count; i++) { boolean isHttpAnnotation = false; // If there are annotations, parse the annotations if (parameterAnnotations[i] != null) { isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i); } // If it is a URI type, record the index, indicating that the url is not parsed from RequestMapping, but passed in from the method. if (parameterTypes[i] == URI.class) { data.urlIndex(i); } else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) { // Not http annotations? Record the index of the request body and resolve the parameter type 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])); } } // If the headerMapIndex has a value after parsing this parameter, it is a verification.. The key must be of type String. 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; }
Process sorting:
- First resolve the annotation information on the class. If you inherit the interface, first resolve the annotation information of the parent interface
- Annotation in re parsing method
- Finally, the annotation information of method parameters is parsed.
What does parsing mean here? You need to know which are the url, which are the request parameters, which are the request body and which are the request headers. How can you know the real value without calling the method? Use placeholders instead of... How to operate it?
Let's start the analysis..
Parsing annotations on classes
SpringMvcContract#processAnnotationOnClass
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) { //First of all, this class must be each class that inherits other classes if (clz.getInterfaces().length == 0) { // Only the RequestMapping annotation was found RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class); if (classAnnotation != null) { // Prepend path from class annotation if specified if (classAnnotation.value().length > 0) { // Get path information String pathValue = emptyToNull(classAnnotation.value()[0]); // Resolve placeholder ${} in path pathValue = resolve(pathValue); if (!pathValue.startsWith("/")) { pathValue = "/" + pathValue; } // Assignment url data.template().uri(pathValue); } } } }
Here, we mainly parse the url value and assign it to RequestTemplate. Let's see what RequestTemplate.uri() does?
public RequestTemplate uri(String uri, boolean append) { /* validate and ensure that the url is always a relative one */ if (UriUtils.isAbsolute(uri)) { throw new IllegalArgumentException("url values must be not be absolute."); } if (uri == null) { uri = "/"; } else if ((!uri.isEmpty() && !uri.startsWith("/") && !uri.startsWith("{") && !uri.startsWith("?") && !uri.startsWith(";"))) { /* if the start of the url is a literal, it must begin with a slash. */ uri = "/" + uri; } // If there are request parameters in the uri, resolve the QueryTemplate // uri intercepts the part of its own url Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri); if (queryMatcher.find()) { String queryString = uri.substring(queryMatcher.start() + 1); /* parse the query string */ this.extractQueryTemplates(queryString, append); /* reduce the uri to the path */ uri = uri.substring(0, queryMatcher.start()); } int fragmentIndex = uri.indexOf('#'); if (fragmentIndex > -1) { fragment = uri.substring(fragmentIndex); uri = uri.substring(0, fragmentIndex); } // Conditions determine whether to append or create if (append && this.uriTemplate != null) { this.uriTemplate = UriTemplate.append(this.uriTemplate, uri); } else { this.uriTemplate = UriTemplate.create(uri, !this.decodeSlash, this.charset); } return this; }
Resolve the part of the request parameter:
private void extractQueryTemplates(String queryString, boolean append) { // One thing to note here is to put the same name in a collection. Map<String, List<String>> queryParameters = Arrays.stream(queryString.split("&")) .map(this::splitQueryParameter) .collect(Collectors.groupingBy( SimpleImmutableEntry::getKey, LinkedHashMap::new, Collectors.mapping(Entry::getValue, Collectors.toList()))); /* add them to this template */ if (!append) { /* clear the queries and use the new ones */ this.queries.clear(); } // Create a QueryTemplate for each entry queryParameters.forEach(this::query); }
for instance:
uri: /user/info?name={name1}&age={age}&name={name2}
Finally, it is analyzed as follows:
url:/user/info
Values in query:map: key: name, value: {name1}, {Name2}; key:age,value:{age}
Parsing annotations on Methods
SpringMvcContract#processAnnotationOnMethod
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { // If the RequestMapping annotation does not exist or the passed in annotation is not a RequestMapping annotation, it is returned directly. // When parsing the method, only the RequestMapping annotation is parsed. 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) { methods = new RequestMethod[] { RequestMethod.GET }; } checkOne(method, methods, "method"); // Resolve the request method. If it is not set, the GET method is used by default data.template().method(Request.HttpMethod.valueOf(methods[0].name())); // path 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; } // Parse the url, this time after the class url. data.template().uri(pathValue, true); } } // The following three can be classified as setting request headers parseProduces(data, method, methodMapping); // consumes parseConsumes(data, method, methodMapping); // headers parseHeaders(data, method, methodMapping); data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>()); }
When parsing the annotation on the method, you can only parse the RequestMapping annotation to get the request method and url. In addition, parseProduces, parseConsumes and parseHeaders are the values of the request header and create a QueryTemplate. It should be noted that parseProduces and parseConsumes specify the type. If more than one is specified in RequestMapping, only the first one will be used. Personally, don't specify the object header information in RequestMapping, but specify it in parameters.
Parsing annotations on method parameters
SpringMvcContract#processAnnotationsOnParameter
Parameter passing: Method meta information; Annotation array of method parameters, because there may be multiple; Index of method parameters
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { boolean isHttpAnnotation = false; AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext( data, paramIndex); Method method = this.processedMethods.get(data.configKey()); // Traversal annotation for (Annotation parameterAnnotation : annotations) { // Get the corresponding parameter processor 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); // Processor resolution. isHttpAnnotation |= processor.processArgument(context, processParameterAnnotation, method); } } // Here is only the type converter to obtain the position parameter if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) { 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; }
What are the parameter processors
Parse request parameters
The RequestParam annotation is parsed
RequestParamParameterProcessor#processArgument
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { // Get the parameter index int parameterIndex = context.getParameterIndex(); // Get the type of the parameter Class<?> parameterType = method.getParameterTypes()[parameterIndex]; MethodMetadata data = context.getMethodMetadata(); // If it is of map type, it will not be resolved. Set the index position of queryMapIndex if (Map.class.isAssignableFrom(parameterType)) { checkState(data.queryMapIndex() == null, "Query map can only be present once."); data.queryMapIndex(parameterIndex); return true; } // If it is not a map type, resolve it RequestParam requestParam = ANNOTATION.cast(annotation); String name = requestParam.value(); checkState(emptyToNull(name) != null, "RequestParam.value() was empty on parameter %s", parameterIndex); // Set the index name correspondence of the location context.setParameterName(name); // Add the information of the placeholder to the old collection of list and reset it to template Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name)); data.template().query(name, query); return true; }
The setting placeholder here uses the RequestParam.value value of the annotation plus {}, that is {requestparam. Value}; Add the value to the query collection.
Parse request header
What is resolved is the RequestHeader annotation
RequestHeaderParameterProcessor#processArgument
It is consistent with the logic of processing request parameters and is no longer analyzed.
@Override public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { int parameterIndex = context.getParameterIndex(); Class<?> parameterType = method.getParameterTypes()[parameterIndex]; MethodMetadata data = context.getMethodMetadata(); if (Map.class.isAssignableFrom(parameterType)) { checkState(data.headerMapIndex() == null, "Header map can only be present once."); data.headerMapIndex(parameterIndex); return true; } String name = ANNOTATION.cast(annotation).value(); checkState(emptyToNull(name) != null, "RequestHeader.value() was empty on parameter %s", parameterIndex); context.setParameterName(name); Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name)); data.template().header(name, header); return true; }
Parse form parameters
The annotation parsed is: PathVariable
PathVariableParameterProcessor#processArgument
@Override public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { String name = ANNOTATION.cast(annotation).value(); checkState(emptyToNull(name) != null, "PathVariable annotation was empty on param %s.", context.getParameterIndex()); context.setParameterName(name); MethodMetadata data = context.getMethodMetadata(); // Get the annotated value; String varName = '{' + name + '}'; // The value here: if the url is not, the request parameter is not, and the request header is not, it will be added to the expression parameter. if (!data.template().url().contains(varName) && !searchMapValues(data.template().queries(), varName) && !searchMapValues(data.template().headers(), varName)) { data.formParams().add(name); } return true; }
The annotation of each parameter has different processors. At present, only @ requestParam handles the request parameters@ requestHead handles the request header@ pathVariable handles the parameters in path@ Spring query map handles the request parameters. The first three will record the alias and index, and then resolve them respectively.
Resolve request parameter set
The annotation parsed is spring querymap
QueryMapParameterProcessor#processArgument
@Override public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { int paramIndex = context.getParameterIndex(); MethodMetadata metadata = context.getMethodMetadata(); // If this annotation is used for annotation, the map type is equivalent to the RequestParam map type. if (metadata.queryMapIndex() == null) { metadata.queryMapIndex(paramIndex); metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded()); } return true; }
Summary:
The RequestParam annotation sets the parameters spliced on the url
The ReqestHead annotation sets the request header parameter
The PathVariable annotation sets the post method and the parameters in the request body.
I see. Why is there no logic to parse the request body? In fact, it's not. It's just the index of the recorded body. It just parses the above four annotations. If not, it will not be analyzed. The returned is ishttpanannotation. If it is false and the type is not Request.Options.class, judgment will be made and the index of the request body will be set.
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])); }
call
ReflectiveFeign.ParseHandlersByName#apply
public Map<String, MethodHandler> apply(Target key) { // Parse out all method meta information List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type()); Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>(); // Go through the meta information and use different RequestTemplate creators according to different attributes in the meta information for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; // If the expression parameter is not empty and the request body is empty if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else if (md.bodyIndex() != null) { // If the index of the request body is not null buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else { // Finally: there is nothing. Only the url, request parameters and request header are parsed buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder); } result.put(md.configKey(), factory.create(key, md, buildTemplate, options, decoder, errorDecoder)); } return result; } }