【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
背景
业务分割创建一个新的项目,希望引用新的技术栈(使用springcloud 全家桶,因项目原因暂时无法升级到springboot2.0 所以这里rpc使用feign),但还是沿用之前的rpc接口入参(原rpc框架为thrift)
- 出现的问题 原rpc接口存在多个对象入参的情况
下面演示如何使用feign解决多对象入参的情况
这里使用的是SpringMvcContract feign.Contract.Default 风格变化太大暂时未使用 也会介绍此契约传多个对象的情况
注意
因为入参格式改变了,对应的提供者也需要做出对应的参数解析 可以在提供者自定义注解解析参数,网上有大量案例
SpringMvcContract
支持springmvc中那样使用注解 这里只列举参数上的注解
@PathVariable @RequestHeader @RequestParam feign对以上参数进行了兼容 若没有使用注解 默认是Body parameters
三个注解分别实现了AnnotatedParameterProcessor接口对应的类如下
- PathVariableParameterProcessor
- RequestHeaderParameterProcessor
- RequestParamParameterProcessor
下面是对参数注解的解析
feign.Contract.BaseContract#parseAndValidateMetadata
processAnnotationsOnParameter在SpringMvcContract重写并处理AnnotatedParameterProcessor子类
/**
* Called indirectly by {@link #parseAndValidatateMetadata(Class)}.
*/
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
......省略代码
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]));
}
}
......省略代码
return data;
}
以上是对feign处理springmvc注解的一个大概介绍
如何解决feign多入参的问题
feign 没有提供多余的注解 我们将自定义注解
自定义注解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;
}
实现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)
放在请求体中使用data.formParams().add(name);
因为我们存放的是对象需要制定Expander 下面有参考的源码
data.indexToExpander().put(context.getParameterIndex(), new JsonStrExpander());
*/
//参数放在请求头及在url后面
// Collection<String> query = context.setTemplateParameter(name,
// data.template().queries().get(name));
// data.template().query(name, query);
//参数放在请求体中
//参考 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;
}
}
配置Contract
要把自带的几个注解加上去
@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);
}
}
声明接口
@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);
}
提供者的报文
@RequestParam注解的参数在url后面 @CustomRequestParam注解的参数在body中 使用@RequestParam在对象需要重写toString 但是对于List不能重写需要
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
使用feign.Contract.Default
声明接口
@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!
// 这里JSON格式需要的花括号居然需要转码
@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);
}
配置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);
}
}
总结
具体使用哪种Contract 是情况而定 在声明接口的时候可以指定配置 @FeignClient(value = "cloud-zuul", configuration = FeignSupportConfig.class)
来源:oschina
链接:https://my.oschina.net/u/3876288/blog/3149175