OpenFeign是如何识别RequestMapping注解的?

简介: 该文章详细介绍了OpenFeign中的日志组件Logger的原理与应用场景,包括日志打印的位置、默认的日志组件、日志级别以及如何定制日志组件和级别。

前言

我们平时写的OpenFeign Client接口,和SpringMvc Controller接口语法的定义一模一样。使得Spring Mvc用户使用OpenFeign框架非常丝滑的过渡。

image.png

比如下方这种:

@FeignClient(name = "fox-server", url = "http://127.0.0.1")
public interface FeignClientApi {
   
   

        @RequestMapping(path = "/get")
        String getName(@RequestParam(name = "name") String name);
}

一开始接触OpenFeign,我就非常好奇,我们写的OpenFeign Client里的方法语法为什么和SpringMvc Controller接口方法一样。

问题

带着上面的疑问,探究一下OpenFeign是如何做到使用SpringMvc的注解的。本文主要是了解下OpenFeign是如何实现对Spring Mvc注解支持的。我们可以知道下面两个问题。

  • 如果没有SpringMvc,OpenFeign能独立运行吗?

  • OpenFeign如何做到支持SpringMvc注解的?

没有SpringMvc,OpenFeign能独立运行吗?

答案是可以的,OpenFeign是一个声明式的rpc通信客户端,默认实现了自己的注解方式。

理解上面这个问题,不得不提OpenFeign的约定接口Contract

Contract 是OpenFeign定义的一个接口,一个协议,一个约定,按约定解析成OpenFeign可以识别的元信息。

接口只有一个parseAndValidatateMetadata方法,

参数Class<?> targetType就是我们写的Feign Client接口的Class。

返回是List<MethodMetadata>方法元数据列表。


public interface Contract {
   
   

/**
* 解析类上方法的元数据,转换成http请求数据
*/

List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);

OpenFeign提供了基础的协议实现BaseContract,他定义了核心的主逻辑

image.png

abstract class BaseContract implements Contract {
   
   

    @Override
    public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
   
   
      //结果集,每个方法都会存一份元数据
      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());
    }
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
   
   
      //方法返回和方法名作为key
      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]);
      }
      //解析Feign Client接口上的注解
      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);
        }
        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]));
        }
      }

      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;
    }

BaseContract定义了解析各个方法的主流程,具体实现留给子类,使用了模版方法设计模式。

abstract class BaseContract implements Contract {
   
   

    /**
     * 解析类上的注解
     */
    protected abstract void processAnnotationOnClass(MethodMetadata data, Class<?> clz);
    /**
     * 解析方法上的注解
     */
    protected abstract void processAnnotationOnMethod(MethodMetadata data,
                                                      Annotation annotation,
                                                      Method method);
    /**
     * 解析参数上的注解
     */
    protected abstract boolean processAnnotationsOnParameter(MethodMetadata data,
                                                             Annotation[] annotations,
                                                             int paramIndex);

  }

默认的协议feign.Contract.Default实现

在默认协议实现中,实现了基础协议定义的抽象方法processAnnotationOnClass,processAnnotationOnMethod,processAnnotationsOnParameter,分别用来处理feign.Headers, feign.RequestLine,feign.Bodyfeign.Param等注解。

这些注解都是feign包中的,也就是OpenFeign也提供了SpringMvc类似的注解

image.png

image.png

image.png 如果不使用SpringMvc,我们可以这样定义FeignClient。

@FeignClient(name = "fox-server", url = "http://127.0.0.1")
public interface FeignClientTestApi {
   
   

        @RequestLine(value = "/get/test")
        String get(@Param String name);
}

支持SpringMvc的协议实现SpringMvcContract

Spring Cloud 为了在SpringMvc项目中比较容易使用OpenFeign,对SpringMvc进行了支持。 实现了SpringMvcContract

processAnnotationOnClass实现中,对类上的RequestMapping注解进行了解析

protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
   
   
        if (clz.getInterfaces().length == 0) {
   
   
            RequestMapping classAnnotation = (RequestMapping)AnnotatedElementUtils.findMergedAnnotation(clz, RequestMapping.class);
            if (classAnnotation != null && classAnnotation.value().length > 0) {
   
   
                String pathValue = Util.emptyToNull(classAnnotation.value()[0]);
                pathValue = this.resolve(pathValue);
                if (!pathValue.startsWith("/")) {
   
   
                    pathValue = "/" + pathValue;
                }

                data.template().uri(pathValue);
            }
        }

    }

对方法上的RequestMapping进行解析

protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
   
   
        if (RequestMapping.class.isInstance(methodAnnotation) || methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
   
   
            RequestMapping methodMapping = (RequestMapping)AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
            RequestMethod[] methods = methodMapping.method();
            if (methods.length == 0) {
   
   
                methods = new RequestMethod[]{
   
   RequestMethod.GET};
            }

            this.checkOne(method, methods, "method");
            data.template().method(HttpMethod.valueOf(methods[0].name()));
            this.checkAtMostOne(method, methodMapping.value(), "value");
            if (methodMapping.value().length > 0) {
   
   
                String pathValue = Util.emptyToNull(methodMapping.value()[0]);
                if (pathValue != null) {
   
   
                    pathValue = this.resolve(pathValue);
                    if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
   
   
                        pathValue = "/" + pathValue;
                    }

                    data.template().uri(pathValue, true);
                }
            }

            this.parseProduces(data, method, methodMapping);
            this.parseConsumes(data, method, methodMapping);
            this.parseHeaders(data, method, methodMapping);
            data.indexToExpander(new LinkedHashMap());
        }
    }

对参数上的注解解析

protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
   
   
        boolean isHttpAnnotation = false;
        AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(data, paramIndex);
        Method method = (Method)this.processedMethods.get(data.configKey());
        Annotation[] var7 = annotations;
        int var8 = annotations.length;

        for(int var9 = 0; var9 < var8; ++var9) {
   
   
            Annotation parameterAnnotation = var7[var9];
            AnnotatedParameterProcessor processor = (AnnotatedParameterProcessor)this.annotatedArgumentProcessors.get(parameterAnnotation.annotationType());
            if (processor != null) {
   
   
                Annotation processParameterAnnotation = this.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;
    }

不管是OpenFeign自己的feign.Contract.Default实现,还是支持SpringMvc的SpringMvcContract实现,它们最终都将FeignClient上的方法解析成feign.MethodMetadata,里面包含了参数和响应的参数,类型等信息。

image.png

总结

回到文章开头提的疑问,现在可以知道了 如果没有SpringMvc,OpenFeign能独立运行吗?

  • 答案是可以的,默认支持自己的注解,比如feign.RequestLine,feign.Body

OpenFeign如何做到支持SpringMvc注解的?

通过实现Contract接口的SpringMvcContract, 里面会对SpringMvc注解的解析。

相关文章
|
消息中间件 监控 搜索推荐
OpenFeign日志组件Logger原理与应用
该文章详细解释了如何在OpenFeign中配置并使用请求和响应的GZIP压缩功能。
|
Kubernetes 关系型数据库 MySQL
seata启动问题之指针异常如何解决
Seata是一款开源的分布式事务解决方案,旨在提供高效且无缝的分布式事务服务;在集成和使用Seata过程中,开发者可能会遇到不同的异常问题,本合集针对Seata常见异常进行系统整理,为开发者提供详细的问题分析和解决方案,助力高效解决分布式事务中的难题。
604 103
|
存储 监控 数据库
什么是聚集索引和非聚集索引?
【8月更文挑战第3天】
7166 6
|
负载均衡 监控 Java
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
23548 7
SpringCloud常见面试题(一):SpringCloud 5大组件,服务注册和发现,nacos与eureka区别,服务雪崩、服务熔断、服务降级,微服务监控
|
存储 监控 Java
OpenFeign请求拦截器组件RequestInterceptor原理与使用场景
该文章讲述了OpenFeign中的请求拦截器组件RequestInterceptor的原理及其常见使用场景。
OpenFeign请求拦截器组件RequestInterceptor原理与使用场景
|
负载均衡 前端开发 Java
OpenFeign阶段性总结-几个核心能力
该文章是对OpenFeign核心功能的总结,涵盖了OpenFeign的执行流程、参数解析、负载均衡能力、请求拦截器、重试机制以及其强大的可扩展性。
OpenFeign阶段性总结-几个核心能力
|
存储 设计模式 缓存
OpenFeign集成Ribbon负载均衡-过滤和选择服务核心实现
该文章主要介绍了如何在OpenFeign中集成Ribbon以实现负载均衡,并详细分析了Ribbon中服务选择和服务过滤的核心实现过程。文章还涉及了Ribbon中负载均衡器(ILoadBalancer)和负载均衡策略(IRule)的初始化方式。
OpenFeign集成Ribbon负载均衡-过滤和选择服务核心实现
|
JSON 前端开发 中间件
React读取properties配置文件转化为json对象并使用在url地址中
本文介绍了如何在React项目中读取properties配置文件,将其内容转化为JSON对象,并在请求URL地址时使用这些配置。文章详细说明了异步读取文件、处理字符串转换为JSON对象的过程,并提供了一个封装函数,用于在发起请求前动态生成配置化的URL地址。
278 1
|
消息中间件 监控 Kafka
查询Kafka集群中消费组(group)信息和对应topic的消费情况
查询Kafka集群中消费组(group)信息和对应topic的消费情况
5682 0
|
设计模式 Java 开发者
如何在Java项目中实现领域驱动设计(DDD)
如何在Java项目中实现领域驱动设计(DDD)