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)