本篇文章介绍的是如何将方法中数据映射为请求数据,例如哪些是请求参数,哪些是请求体,哪些是请求头。。。
接口
该接口的作用就是解析类中的方法。每个方法解析为MethodMetadata。
public interface Contract {
/**
* Contract 提供接口,feign的原生实现是BaseContract,整合spring使用的是SpringMvcContract
*/
// TODO: break this and correct spelling at some point
List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}
该接口只有一个方法,传入的参数是开发者编写的接口的元信息;作用是将每个方法解析为一个MethodMetadata对象。
元信息MethodMetadata
// 序列化
private static final long serialVersionUID = 1L;
// 每个方法的唯一标识
private String configKey;
// 方法的返回值
private transient Type returnType;
// 如果这个方法参数有URI类型的,记住这个索引
private Integer urlIndex;
// 记录方法体的索引值
private Integer bodyIndex;
// head中的数据使用map封装,记录索引值
private Integer headerMapIndex;
// 查询数据使用map封装,记录索引值
private Integer queryMapIndex;
// 是否编码查询map
private boolean queryMapEncoded;
private transient Type bodyType;
// 请求数据模板。包含请求方法,请求参数,请求体和url
private RequestTemplate template = new RequestTemplate();
private List<String> formParams = new ArrayList<String>();
// 每个方法参数的名称,key:参数的索引位置;value:注解中的value值
private Map<Integer, Collection<String>> indexToName =
new LinkedHashMap<Integer, Collection<String>>();
// Expander类型,传入一个Object对象,返回一个String类型。。
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;
重要的是这些属性,剩下的就是get,set方法了,不同于JavaBean,这个类的方法和属性名一样,有参数就是set方法,没有参数就是get方法。其实是什么都行,没必要一个要按照JavaBean的格式。
这里介绍的解析过程是基于整合Spring的流程,如果你想让让feign解析自己的注解,只需要实现Contract接口,之后实现自己的逻辑即可。
实现类BaseContract
BaseContract#parseAndValidatateMetadata
@Override
// 类型class信息传入,该为开发者编写的接口class信息。
public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
// 不能有泛型
checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
targetType.getSimpleName());
// 继承的接口最多只能有一个
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>();
// 遍历方法,
for (Method method : targetType.getMethods()) {
// 默认方法和静态方法都跳过
if (method.getDeclaringClass() == Object.class ||
(method.getModifiers() & Modifier.STATIC) != 0 ||
Util.isDefault(method)) {
continue;
}
// 解析出方法信息,放入缓存。
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());
}
这里相当于Feign的规范了:
- 接口不能有泛型
- 继承的接口最多一个(继承的那个接口不能再继承接口了,也就是说最多有一个父接口,父接口不能再有父接口了)
遍历接口的所有Public方法,解析出MethodMetadata,放入集合返回。
解析方法
BaseContract#parseAndValidateMetadata
解析接口类中的方法。入参:targetType:接口类;method:接口类中的方法。
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
// 既然每个方法对应一个MethodMetadata,二话不说,直接创建对象。
MethodMetadata data = new MethodMetadata();
// 解析出返回值类型
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
// 创建唯一标识
data.configKey(Feign.configKey(targetType, method));
// 有接口就先解析父接口的,之后再解析本接口的。
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
// 解析类上的注解
processAnnotationOnClass(data, targetType);
// 解析方法上的注解
for (Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
// 到此为止,方法上的注解会解析出请求方法的。此处校验下
checkState(data.template().method() != null,
"Method %s not annotated with HTTP method type (ex. GET, POST)",
method.getName());
// 每个参数的元信息
Class<?>[] parameterTypes = method.getParameterTypes();
// 每个参数的泛型
Type[] genericParameterTypes = method.getGenericParameterTypes();
// 每个方法参数的注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
boolean isHttpAnnotation = false;
// 如果有注解的话,解析注解
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
// 如果是URI类型的,记录下索引,说明url不是从RequestMapping中解析出url,而是方法中传进来的。
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
// 不是http注解?记录请求体的索引,解析出参数类型
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]));
}
}
// 如果解析完这个参数之后,headerMapIndex有值了,就是做个校验。。key一定是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;
}
流程梳理:
- 先解析类上的注解信息,如果继承接口,先解析父接口的
- 再解析方法中的注解
- 最后解析方法参数的注解信息。
这里的解析是什么意思呢?就是要知道哪些是url,哪些是请求参数,哪些是请求体,哪些是请求头,还没有调用方法,怎么知道真正的值呢?使用占位符代替。。。怎么操作的呢?
下面就开始分析。。
解析标注在类上的注解
SpringMvcContract#processAnnotationOnClass
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
//首先这个类一定是每个继承其他的类
if (clz.getInterfaces().length == 0) {
// 只找到RequestMapping注解
RequestMapping classAnnotation = findMergedAnnotation(clz,
RequestMapping.class);
if (classAnnotation != null) {
// Prepend path from class annotation if specified
if (classAnnotation.value().length > 0) {
// 得到path信息
String pathValue = emptyToNull(classAnnotation.value()[0]);
// 解析路径中的占位符 ${}
pathValue = resolve(pathValue);
if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
// 赋值url
data.template().uri(pathValue);
}
}
}
}
这里主要是解析出url的值,赋值给RequestTemplate,看下RequestTemplate.uri()做了什么?
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;
}
// 如果uri中有请求参数,解析出QueryTemplate
// uri截取自己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);
}
// 条件判断是追加还是创建
if (append && this.uriTemplate != null) {
this.uriTemplate = UriTemplate.append(this.uriTemplate, uri);
} else {
this.uriTemplate = UriTemplate.create(uri, !this.decodeSlash, this.charset);
}
return this;
}
解析请求参数的部分:
private void extractQueryTemplates(String queryString, boolean append) {
// 这里有一点需要注意,把name一样的放在一个集合中。
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();
}
// 每个entry创建QueryTemplate.
queryParameters.forEach(this::query);
}
举个例子:
uri: /user/info?name={name1}&age={age}&name={name2}
最后解析出:
url:/user/info
query:map中的值:key:name value:{name1},{name2}; key:age,value:{age}
解析标注在方法上的注解
SpringMvcContract#processAnnotationOnMethod
protected void processAnnotationOnMethod(MethodMetadata data,
Annotation methodAnnotation, Method method) {
// 如果不存在RequestMapping注解或者传入的注解不是RequestMapping注解,直接返回了。
// 解析方法额时候,只解析RequestMapping注解。
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");
// 解析出请求方法,如果没有设置,默认使用的是GET方法
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;
}
// 解析出url,这次是拼接在类url之后。
data.template().uri(pathValue, true);
}
}
// 下面的三个可以归为设置请求头
parseProduces(data, method, methodMapping);
// consumes
parseConsumes(data, method, methodMapping);
// headers
parseHeaders(data, method, methodMapping);
data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
}
解析方法上的注解,只能解析RequestMapping注解,得到请求方法,url,另外parseProduces,parseConsumes,parseHeaders是得到请求头的值,创建QueryTemplate.需要注意的是parseProduces,parseConsumes指定了类型,如果在RequestMapping中指定了多个,只会去第一个。个人觉得,不要在RequestMapping中指定对象头信息,在参数中指定。
解析标注在方法参数上的注解
SpringMvcContract#processAnnotationsOnParameter
传参:方法元信息;方法参数的注解数组,因为可能有多个;方法参数的索引
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());
// 遍历注解
for (Annotation parameterAnnotation : annotations) {
// 得到对应的参数处理器
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);
// 处理器解析。
isHttpAnnotation |= processor.processArgument(context,
processParameterAnnotation, method);
}
}
// 这里只是获得该位置参数的类型转换器
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;
}
看下参数处理器有哪些
解析请求参数
解析的是RequestParam注解
RequestParamParameterProcessor#processArgument
public boolean processArgument(AnnotatedParameterContext context,
Annotation annotation, Method method) {
// 得到该参数索引
int parameterIndex = context.getParameterIndex();
// 得到该参数的类型
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
MethodMetadata data = context.getMethodMetadata();
// 如果map类型的,就不再解析了,设置queryMapIndex的索引位置
if (Map.class.isAssignableFrom(parameterType)) {
checkState(data.queryMapIndex() == null,
"Query map can only be present once.");
data.queryMapIndex(parameterIndex);
return true;
}
// 如果不是map类型,进行解析
RequestParam requestParam = ANNOTATION.cast(annotation);
String name = requestParam.value();
checkState(emptyToNull(name) != null,
"RequestParam.value() was empty on parameter %s", parameterIndex);
// 设置该位置的索引--名称 对应关系
context.setParameterName(name);
// 将该占位符的信息添加在list老的集合中之后重新设置给template
Collection<String> query = context.setTemplateParameter(name,
data.template().queries().get(name));
data.template().query(name, query);
return true;
}
这里的设置占位符使用的是注解的RequestParam.value值加上{},即{equestParam.value};将该值添加在query集合中。
解析请求头
解析的是RequestHeader注解
RequestHeaderParameterProcessor#processArgument
和处理请求参数的逻辑一致,不再分析了。
@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;
}
解析表单参数
解析的注解是: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();
// 得到注解的value;
String varName = '{' + name + '}';
// 这里的值:url没有,请求参数中没有,请求头中没有,则会加入在表达参数中。
if (!data.template().url().contains(varName)
&& !searchMapValues(data.template().queries(), varName)
&& !searchMapValues(data.template().headers(), varName)) {
data.formParams().add(name);
}
return true;
}
每个参数的注解有不同的处理器,目前只有@requestParam 处理请求参数;@requestHead处理请求头;@pathVariable处理path中的参数。 @SpringQueryMap处理请求参数,前三个都会记录别名和索引,之后分别解析。
解析请求参数集合
解析的注解是SpringQueryMap
QueryMapParameterProcessor#processArgument
@Override
public boolean processArgument(AnnotatedParameterContext context,
Annotation annotation, Method method) {
int paramIndex = context.getParameterIndex();
MethodMetadata metadata = context.getMethodMetadata();
// 如果使用该注解标注,map类型 和使用RequestParam map类型等效。
if (metadata.queryMapIndex() == null) {
metadata.queryMapIndex(paramIndex);
metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded());
}
return true;
}
小结:
RequestParam注解设置的是拼接在url上的参数
ReqestHead注解设置的是请求头参数
PathVariable注解设置的是post方法,请求体中的参数。
发现了吧,怎么没有解析请求体的逻辑呢?其实没有,只是记录的body的索引。它只是解析以上的四个注解。不是的的话一律不解析的。返回的是isHttpAnnotation,如果是false,类型不是Request.Options.class,那么会进行判断,设置请求体的索引。
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]));
}
调用
ReflectiveFeign.ParseHandlersByName#apply
public Map<String, MethodHandler> apply(Target key) {
// 解析出所有的方法元信息
List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
// 遍历元信息,根据元信息中属性的不同,使用不同的RequestTemplate创建器
for (MethodMetadata md : metadata) {
BuildTemplateByResolvingArgs buildTemplate;
// 如果表达参数不为空,并且请求体是空
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
} else if (md.bodyIndex() != null) {
// 如果请求体的索引不为null
buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
} else {
// 最后:什么也没有,只解析url,请求参数,请求头
buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);
}
result.put(md.configKey(),
factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
}
return result;
}
}