[Feign source code] the class of parsing method -- Contract

Posted by Swede78 on Fri, 10 Sep 2021 12:10:24 +0200

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:

  1. Interfaces cannot have generics
  2. 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:

  1. First resolve the annotation information on the class. If you inherit the interface, first resolve the annotation information of the parent interface
  2. Annotation in re parsing method
  3. 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;
    }
  }

Topics: Spring Spring Cloud feign