Feign-Get请求自动转成Post问题分析


1 现象

Feign使用中有一个小小的细节之处,在明明我们使用Get配置的时候,我们会发现Feign会将Get请求转成Post调用。

直接上示例:
接口提供者:

  	@GetMapping(value = "/provider")
    public String test(@RequestParam(value = "name", defaultValue = "zcswl7961") String name) {

        List<String> services = discoveryClient.getServices();
        /**
         * test code
         */
        for (String serviceId : services) {
            List<ServiceInstance> instances = discoveryClient.getInstances(serviceId);

            for (ServiceInstance serviceInstance : instances) {
                URI uri = serviceInstance.getUri();
            }
        }

        return "this port is:" + port + " name is:"+name;
    }


    /**
     * test feign Get Post
     */
    @PostMapping(value = "provider")
    public String postTest(@RequestBody String name) {

        System.out.println(name);

        return "this method is Post port is:" + port + "     name is:" +name;


    }

该 服务模块命名为service-hi,我们在服务中定义了两个接口,接口路径相同,都是/provider 只是一个为Get 一个为Post请求,

Feign接口定义:

@FeignClient(value = "service-hi",fallback = ScheduleServiceHiHystric.class)
public interface ScheduleServiceHi {

    /**
     * 接口调用
     * @param name 名称
     * @return 返回结果
     */
    @RequestMapping(value = "/provider",method = RequestMethod.GET)
    String sayProvider(String name);
}

Feign接口中,我们定义了对应Get请求的接口示例,

Feign接口调用:

@RestController
@AllArgsConstructor
public class HiController {

    private final ScheduleServiceHi scheduleServiceHi;

    @GetMapping(value = "/hi")
    public String sayHi(@RequestParam String name) {
        return scheduleServiceHi.sayProvider( name );
    }
}

我们在服务模块:service-feign(端口:8766)中 定义了一个/hi接口,使用了Feign接口调用,通过调用测试/hi接口之后,我们会发现,feign调用了Post接口的请求数据

curl :
127.0.0.1:8766/hi?name=zhoucg
Response:
this method is Post port is:8763 name is:zhoucg
2 解决

对于这个现象,我们需要声明参数请求的注解形式
例如,我们可以在Feign的接口定义的参数中加上@RequestParam(“name”)方式

@FeignClient(value = "service-hi",fallback = ScheduleServiceHiHystric.class)
public interface ScheduleServiceHi {

    /**
     * 接口调用
     * @param name 名称
     * @return 返回结果
     */
    @RequestMapping(value = "/provider",method = RequestMethod.GET)
    String sayProvider(@RequestParam("name") String name);

}

另一种方法是更换Apache的HttpClient
在Feign的配置项中加入

feign:
  httpclient:
    enabled: true

同时使用下面两个依赖:

		<dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.9</version>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
            <version>10.2.3</version>
        </dependency>
3 源码分析

这个问题的主要的原因就是Feign默认使用的连接工具实现类,发现只要你有对应的body体对象,就会强制把GET请求转换成POST请求

这句话说的很笼统,下面,就是带着源码细节,我们慢慢的分析,具体的Feign源码的实现细节

针对第一个解决方式:
我们只是简单的在Feign的接口中的调用参数中增加了一个@RequestParam(“name”) 注解就解决了问题,这主要是因为,Feign源码在解析含有@FeignClient注解的接口的时候,在创建代理对象的时候,代理对象在去解析含有@RequestParam注解的参数的时候,会将该参数增强到url上,而不是作为body传递

我们在使用Feign的时候,模块启动类会存在 @EnableFeignClients注解
该注解会通过@Import注解向Spring注入 FeignClientsRegistrar

FeignClientsRegister是一个ImportBeanDefinitionRegistrar 类型的类 ,系统会默认调用registerBeanDefinitions(AnnotationMetadata metadata) 方法

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		registerDefaultConfiguration(metadata, registry);
		registerFeignClients(metadata, registry);
	}

该方法中,首先是会根据@EnableFeignClients 配置的Configuration配置环境实体类去向Spring容器中注入BeanDefinition,
BeanDefinition中的name为 default. + @EnableFeignClients注解的类的全名
BeanDefinition中的class为:FeignClientSpecification
在这里插入图片描述
然后会去解析指定包下的@FeignClient注解,并创建对应接口的代理类,注册到spring 容器中,

默认情况下,解析的包会从@EnableFeignClient注解的basePackages配置和basePackageClasses配置属性中查询,如果没有配置,会去解析当前包以及子包下含有的@FeignClient注解
在这里插入图片描述
然后在遍历每一个包路径,并且解析包中含有@FeignClient注解的BeanDefinition类,进行处理,处理步骤

  • 1,首先是校验该类是接口定义
  • 2,获取@FeignClient注解的配置属性信息,存放到attributes中
  • 3,解析name,根据@FeignClient配置的contextId -> value -> name ->
    serviceId优先级依次获取
  • 4,注册步骤3中的name名称的BeanDefinition,class类型为:FeignClientSpecification
  • 5,注册FeignClient,解析@FeignClient中的配置属性,BeanDefinition的别名为:contextId +"FeignClient"
  • class类型为:FeignClientFactoryBean
    是一个ObjectFactory类型的类

在这里插入图片描述

在解析@FeignClient之后,会通过ObjectFacotry的getObject()方法获取到其代理对象

	<T> T getTarget() {
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(this.url)) {
			if (!this.name.startsWith("http")) {
				this.url = "http://" + this.name;
			}
			else {
				this.url = this.name;
			}
			this.url += cleanPath();
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
		}
		if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
			this.url = "http://" + this.url;
		}
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof LoadBalancerFeignClient) {
				// not load balancing because we have a url,
				// but ribbon is on the classpath, so unwrap
				client = ((LoadBalancerFeignClient) client).getDelegate();
			}
			builder.client(client);
		}
		Targeter targeter = get(context, Targeter.class);
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(this.type, this.name, url));
	}

我们重点看一下loadBalance方法中的最终会调用ReflectiveFeign#newInstance(Target target) 方法进行获取
在这里插入图片描述
首先是通过ParseHandlersByName类apply去获取解析目标接口,并返回方法元数据对象
在这里插入图片描述
1,避免检测从Object类继承的方法,剔除检测@FeignClient注解的接口中的静态方法,和抽象方法,
2,进入parseAndValidateMetadata方法,解析对应存在方法中的feign的注解参数

	protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
      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);
        }
        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;
    }

1,该方法中,首先是获取对应feign配置方法中的returnType和configKey
2,解析配置的@RequestMapping注解以及对应的Param注解,最终我们会发现,会将含有@RequestParam注解存储到 indexToNameurl进行封装

	protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
      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);
        }
        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;
    }

这也是为什么增加了@RequestParam时,Feign会将其追加到url中,而不是作为body传递

针对第二个解决方法:

第二个解决方式是我们直接替换Feign中的HttpClient调用jar包,
Feign中,默认的Http连接工具是在feign-core.jar包中的Client方法

	HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
      final HttpURLConnection connection =
          (HttpURLConnection) new URL(request.url()).openConnection();
      if (connection instanceof HttpsURLConnection) {
        HttpsURLConnection sslCon = (HttpsURLConnection) connection;
        if (sslContextFactory != null) {
          sslCon.setSSLSocketFactory(sslContextFactory);
        }
        if (hostnameVerifier != null) {
          sslCon.setHostnameVerifier(hostnameVerifier);
        }
      }
      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
      connection.setAllowUserInteraction(false);
      connection.setInstanceFollowRedirects(options.isFollowRedirects());
      connection.setRequestMethod(request.httpMethod().name());

      Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
      boolean gzipEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
      boolean deflateEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);

      boolean hasAcceptHeader = false;
      Integer contentLength = null;
      for (String field : request.headers().keySet()) {
        if (field.equalsIgnoreCase("Accept")) {
          hasAcceptHeader = true;
        }
        for (String value : request.headers().get(field)) {
          if (field.equals(CONTENT_LENGTH)) {
            if (!gzipEncodedRequest && !deflateEncodedRequest) {
              contentLength = Integer.valueOf(value);
              connection.addRequestProperty(field, value);
            }
          } else {
            connection.addRequestProperty(field, value);
          }
        }
      }
      // Some servers choke on the default accept string.
      if (!hasAcceptHeader) {
        connection.addRequestProperty("Accept", "*/*");
      }

      if (request.requestBody().asBytes() != null) {
        if (contentLength != null) {
          connection.setFixedLengthStreamingMode(contentLength);
        } else {
          connection.setChunkedStreamingMode(8196);
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        if (gzipEncodedRequest) {
          out = new GZIPOutputStream(out);
        } else if (deflateEncodedRequest) {
          out = new DeflaterOutputStream(out);
        }
        try {
          out.write(request.requestBody().asBytes());
        } finally {
          try {
            out.close();
          } catch (IOException suppressed) { // NOPMD
          }
        }
      }
      return connection;
    }

Feign原生的连接工具使用了jdk中的rt.jar包的HttpURLConnection 类 进行实现,

其中,对应HttpURLConnection 的连接对象,Feign默认的实现是设置了doOutput为true

        connection.setDoOutput(true);

这个设置也正是解释了为什么Feign只要发现你存在body体对象就会将Get请求转成Post

关于这个HttpURLConnection的配置的解释,我们可以参考strackoverflow这个链接:https://stackoverflow.com/questions/8587913/what-exactly-does-urlconnection-setdooutput-affect

因此,我们可以替换原始的feign中的httpclient的实现,来解决这个问题。

在Netflix的官方github上,同样是针对这个问题提出了一个issue,链接:
https://github.com/spring-cloud/spring-cloud-netflix/issues/1253

其中,有一位打个也同样给出了一个解决方案:通过实现RequestInterceptor 来自定义Feign配置的解析
代码如下:

public class YryzRequestInterceptor implements RequestInterceptor {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void apply(RequestTemplate template) {
        // feign 不支持 GET 方法传 POJO, json body转query
        if (template.method().equals("GET") && template.body() != null) {
            try {
                JsonNode jsonNode = objectMapper.readTree(template.body());
                template.body(null);

                Map<String, Collection<String>> queries = new HashMap<>();
                buildQuery(jsonNode, "", queries);
                template.queries(queries);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
        if (!jsonNode.isContainerNode()) {   // 叶子节点
            if (jsonNode.isNull()) {
                return;
            }
            Collection<String> values = queries.get(path);
            if (null == values) {
                values = new ArrayList<>();
                queries.put(path, values);
            }
            values.add(jsonNode.asText());
            return;
        }
        if (jsonNode.isArray()) {   // 数组节点
            Iterator<JsonNode> it = jsonNode.elements();
            while (it.hasNext()) {
                buildQuery(it.next(), path, queries);
            }
        } else {
            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
            while (it.hasNext()) {
                Map.Entry<String, JsonNode> entry = it.next();
                if (StringUtils.hasText(path)) {
                    buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                } else {  // 根节点
                    buildQuery(entry.getValue(), entry.getKey(), queries);
                }
            }
        }
    }
}

关于RequestInterceptor的解析,Feign的源码是在SynchronousMethodHandler类中的targetRequest(RequestTemplate template) 方法实现

  Request targetRequest(RequestTemplate template) {
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    return target.apply(template);
  }

可自行分析

  • 22
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值