本篇文章介绍的是如何将方法中数据映射为请求数据,例如哪些是请求参数,哪些是请求体,哪些是请求头。。。
该接口的作用就是解析类中的方法。每个方法解析为MethodMetadata。
public interface Contract { /** * Contract 提供接口,feign的原生实现是BaseContract,整合spring使用的是SpringMvcContract */ // TODO: break this and correct spelling at some point List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType); }
该接口只有一个方法,传入的参数是开发者编写的接口的元信息;作用是将每个方法解析为一个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#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; }
流程梳理:
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; } }