springcloud feign resolves multiobject participation

Keywords: Programming JSON SpringBoot Java encoding

background

Business Split creates a new project that wants to reference a new technology stack (using the springcloud family bucket, temporarily unable to upgrade to springboot 2.0 for project reasons, so feign is used here for rpc), but it still uses the previous RPC interface (the original RPC framework is thrift)

  • Problem occurs when there are multiple objects participating in the original rpc interface

The following demonstrates how to use feign to resolve multiobject participation

SpringMvcContract is used here The feign.Contract.Default style change is too large to be used temporarily will also describe how this contract passes over multiple objects

Be careful

Because the entry format has changed, the corresponding provider also needs to make the corresponding parameter resolution Annotation resolution parameters can be customized by providers, with a large number of cases Online

SpringMvcContract

Supports the use of annotations as in springmvc Only annotations on parameters are listed here

@PathVariable @RequestHeader @RequestParam feign is compatible with the above parameters. If no annotation is used, the default is Body parameters

The three annotations implement the classes corresponding to the AnnotatedParameterProcessor interface as follows

  • PathVariableParameterProcessor
  • RequestHeaderParameterProcessor
  • RequestParamParameterProcessor

The following is an explanation of the parameter notes

feign.Contract.BaseContract#parseAndValidateMetadata
processAnnotationsOnParameter stay SpringMvcContract Rewrite and process AnnotatedParameterProcessor Subclass
/**
     * Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
     */
    protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
      ......Omit Code
      int count = parameterAnnotations.length;
      for (int i = 0; i < count; i++) {
        boolean isHttpAnnotation = false;
        if (parameterAnnotations[i] != null) {
          isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
        }
        if (parameterTypes[i] == URI.class) {
          data.urlIndex(i);
        } else if (!isHttpAnnotation) {
          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]));
        }
      }
      ......Omit Code

      return data;
    }

The above is an overview of feign's handling of springmvc comments

How to solve the problem of feign multi-participation

feign did not provide any extra annotations we will customize the annotations

Custom Note CustomRequestParam

/**
 * copy @RequestParam
 *
 * @author sunmingji
 * @date 2019-12-27
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomRequestParam {

	@AliasFor("name")
	String value() default "";

	@AliasFor("value")
	String name() default "";

	boolean required() default true;

	String defaultValue() default ValueConstants.DEFAULT_NONE;

}

Implement AnnotatedParameterProcessor

@Slf4j
public class CustomRequestParamParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<CustomRequestParam> ANNOTATION = CustomRequestParam.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@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.queryMapIndex() == null, "Query map can only be present once.");
			data.queryMapIndex(parameterIndex);

			return true;
		}

		CustomRequestParam requestParam = ANNOTATION.cast(annotation);
		String name = requestParam.value();
		checkState(emptyToNull(name) != null,
				"CustomRequestParam.value() was empty on parameter %s",
				parameterIndex);
		context.setParameterName(name);
		
		/*
			data.template().query(name, query) is used if the parameter is placed behind the request header, depending on the situation.
			Use data.formParams().add(name) in the body of the request;
			Because we're storing objects, we need to make a reference source under Expander
			data.indexToExpander().put(context.getParameterIndex(), new JsonStrExpander());
		*/
		
		//Parameters are placed in the request header and after the url
//		Collection<String> query = context.setTemplateParameter(name,
//				data.template().queries().get(name));
//		data.template().query(name, query);

		//Place parameters in the body of the request
		//Reference feign.Contract.Default.processAnnotationsOnParameter
		/**
			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;
			  String varName = '{' + name + '}';
			  if (!data.template().url().contains(varName) &&
				  !searchMapValuesContainsSubstring(data.template().queries(), varName) &&
				  !searchMapValuesContainsSubstring(data.template().headers(), varName)) {
				data.formParams().add(name);
			  }
			}
		 */
		data.formParams().add(name);

		data.indexToExpander().put(context.getParameterIndex(), new JsonStrExpander());
		return true;
	}
}

Configure Contract

You need to add a few notes that come with it

@Configuration
@Slf4j
public class FeignSupportConfig {

	@Bean
	public Contract feignContract() {

		List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();

		annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
		annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
		annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
		annotatedArgumentResolvers.add(new CustomRequestParamParameterProcessor());
		return new SpringMvcContract(annotatedArgumentResolvers);
	}
}

Declare Interface

@FeignClient(value = "cloud-zuul", configuration = FeignSupportConfig.class)
public interface IUserService {

    @RequestMapping(value = "api-a/getUser", method = RequestMethod.POST, consumes = "application/json")
    String getUserComplex(@RequestParam("user") User user, @RequestParam("dept") Dept dept, @RequestParam("schoolId") String schoolId,
                          @CustomRequestParam("userArray") User[] userArray);

    @RequestMapping(value = "api-a/getUser", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    String getUserComplex_(@RequestParam("user") User user, @RequestParam("dept") Dept dept, @RequestParam("schoolId") String schoolId,
                        @CustomRequestParam(value = "userArray") User[] userArray, @CustomRequestParam(value = "deptList") List<Dept> deptList);


    @RequestMapping(value = "api-a/getUser", method = RequestMethod.POST, consumes = "application/json")
    String getUserRequestBody(@CustomRequestParam("userArray") User[] userArray);

    @RequestMapping(value = "api-a/getUser", method = RequestMethod.POST, consumes = "application/json")
    String getUserRequestBody_(@RequestBody User[] userArray2);
}

Provider's Message

Parameters of the @RequestParam annotation @CustomRequestParam annotation after url are in body Use @RequestParam to override toString on objects but not on List s

2019-12-29 12:51:31.518 DEBUG 61973 --- [nio-8086-exec-2] o.a.coyote.http11.Http11InputBuffer      : Received [POST /getUser?user=%7B%22userId%22%3A%22userId%22%2C%22userName%22%3A%22userName%22%7D&dept=%7B%22deptId%22%3A%22deptId%22%2C%22deptName%22%3A%22deptName%22%7D&schoolId=schoolIdParam HTTP/1.1
content-type: application/json;charset=UTF-8
accept: */*
user-agent: Java/1.8.0_171
x-forwarded-host: 192.168.199.108:8007
x-forwarded-proto: http
x-forwarded-prefix: /api-a
x-forwarded-port: 8007
x-forwarded-for: 192.168.199.108
Accept-Encoding: gzip
Content-Length: 132
Host: 192.168.199.108:8086
Connection: Keep-Alive

{"userArray":"[{\"userId\":\"userId\",\"userName\":\"userName\"}]","deptList":["{\"deptId\":\"deptId\",\"deptName\":\"deptName\"}"]}]

feign.Contract.Default

Use feign.Contract.Default

Declare Interface

@FeignClient(value = "cloud-zuul", configuration = FeignSupportConfig.class)
public interface IUserService {

    @RequestLine("POST /api-a/getUser")
    @Headers("Content-Type: application/json")
    // json curly braces must be escaped!
    // Here the curly braces needed for JSON format actually need transcoding
    @Body("%7B\"user\": {user}, \"dept\": {dept}, \"userArray\": {userArray}, \"deptList\": {deptList}%7D")
    String json(@Param("user") User user, @Param("dept") Dept dept,
                @Param(value = "userArray", expander = JsonStrExpander.class) User[] userArray, @Param("deptList") List<Dept> deptList);
}

Configure Contract

@Configuration
@Slf4j
public class FeignSupportConfig {

	@Bean
	public Contract feignContract() {

		
		return new feign.Contract.Default();
	}
}

Param.Expander

public class JsonStrExpander implements Param.Expander {
	@Override
	public String expand(Object value) {
		return JSON.toJSONString(value);
	}
}

summary

Which Contract s are used depends on which configuration can be specified when declaring an interface @FeignClient(value = "cloud-zuul", configuration = FeignSupportConfig.class)

Posted by avo on Sat, 28 Dec 2019 21:53:41 -0800