Spring Cloud 入门——5.5 Feign 声明式调用——使用问题

24 篇文章 0 订阅
22 篇文章 3 订阅

代码信息

本篇文章涉及代码版本

组件版本
Spring Boot2.0.8.RELEASE
Spring CloudFinchley.SR1

整合feign时遇见的坑

本来计划中是没有这篇内容的,但是实际中这一个月来,整理了之前学习springcloud的知识和内容。因为之前有经验还算很快,但是在整合Feign的时候因为是一个不算太麻烦的功能所以就没去找以前的笔记,后来实际整合的时候各种坑大大超过了计划时间,所以决定把这些东西都整理出来。


错误:Request method ‘POST’ not supported

一个比较坑的问题

问题复现

当我们有一些下面这样的请求需要去远程应用拿数据的时候。

    @RequestMapping(value = "user",method = RequestMethod.GET)
    String getUserInfo(User user);


    @RequestMapping(value = "getNumber",method = RequestMethod.GET)
    String getNumber(Integer number);


    @RequestMapping(value = "getStr",method = RequestMethod.GET)
    String getStr(String str);

这个时候发起请求的时候,会出现这个异常

{"timestamp":"2019-07-13T08:52:13.330+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/user"}] with root cause

feign.FeignException: status 405 reading UserService#getUserInfo(User); content:
{"timestamp":"2019-07-13T08:52:13.330+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/user"}
异常原因

虽然我们使用的是GET方法,而服务提供方使用也是GET方法但是服务却告知我们缺失POST方法。这是因为只要参数是复杂对象,即使指定了是GET方法,feign依然会以POST方法进行发送请求

解决办法

此时当数据是复杂对象的时候需要使用POST发起请求。


错误:PathVariable annotation was empty on param 0

问题复现

假如有个服务应用提供了这个请求接口

    @RequestMapping(value = "/simple/{id}",method = RequestMethod.GET)
    public String getId(@PathVariable(required = false) Long id) {
        log.info("getId:{}",id);
        return String.valueOf(id);
    }

拿到相关接口规范后,尝试这么编写Feign的接口

    @RequestMapping(value = "/simple/{id}",method = RequestMethod.GET)
    String getId(@PathVariable(required = false) Long id);

    @GetMapping(value = "/simple/{id}")
    String getIdV2(@PathVariable(required = false) Long id);

此时却发现项目无法启动了并且抛出了下面的错误

Caused by: java.lang.IllegalStateException: PathVariable annotation was empty on param 0.
	at feign.Util.checkState(Util.java:128) ~[feign-core-9.5.1.jar:na]
	at org.springframework.cloud.openfeign.annotation.PathVariableParameterProcessor.processArgument(PathVariableParameterProcessor.java:51) ~[spring-cloud-openfeign-core-2.0.1.RELEASE.jar:2.0.1.RELEASE]
	at org.springframework.cloud.openfeign.support.SpringMvcContract.processAnnotationsOnParameter(SpringMvcContract.java:238) ~[spring-cloud-openfeign-core-2.0.1.RELEASE.jar:2.0.1.RELEASE]
	at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:110) ~[feign-core-9.5.1.jar:na]
	at org.springframework.cloud.openfeign.support.SpringMvcContract.parseAndValidateMetadata(SpringMvcContract.java:133) ~[spring-cloud-openfeign-core-2.0.1.RELEASE.jar:2.0.1.RELEASE]
	at feign.Contract$BaseContract.parseAndValidatateMetadata(Contract.java:66) ~[feign-core-9.5.1.jar:na]
	at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:146) ~[feign-core-9.5.1.jar:na]
	at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:53) ~[feign-core-9.5.1.jar:na]
	at feign.Feign$Builder.target(Feign.java:218) ~[feign-core-9.5.1.jar:na]
	at org.springframework.cloud.openfeign.HystrixTargeter.target(HystrixTargeter.java:39) ~[spring-cloud-openfeign-core-2.0.1.RELEASE.jar:2.0.1.RELEASE]
	at org.springframework.cloud.openfeign.FeignClientFactoryBean.loadBalance(FeignClientFactoryBean.java:223) ~[spring-cloud-openfeign-core-2.0.1.RELEASE.jar:2.0.1.RELEASE]
	at org.springframework.cloud.openfeign.FeignClientFactoryBean.getObject(FeignClientFactoryBean.java:244) ~[spring-cloud-openfeign-core-2.0.1.RELEASE.jar:2.0.1.RELEASE]
	at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:171) ~[spring-beans-5.0.12.RELEASE.jar:5.0.12.RELEASE]
	... 30 common frames omitted

问题原因及解决办法

FeignClient@PathVariablevalue值必须设置


错误:not annotated with HTTP method type (ex. GET, POST)

非常无语的问题,浪费了大把时间

问题复现

最开始我们尝试使用Feign的自定义配置的时候尝试使用过这样的配置

@Configuration
public class Configuration1 {

  @Bean
  public Contract feignContract() {
    //这将SpringMvc Contract 替换为feign.Contract.Default
    return new feign.Contract.Default();
  }

}

上面内容的作用就是将Feign对MVC格式的支持替换成了Feign自己的规则,你可以使用下面的方式使用Feign的语法来发起请求。

@FeignClient(value = "base-producer-cluster",configuration = Configuration1.class)
public interface ClientService {

    /**
     * 测试的服务获取
     * @return
     */
    @RequestLine("GET getService")
    String getService();

    /**
     * 用来测试超时的请求
     * @param time
     * @return
     */
    @RequestLine("POST testParams")
    String testParams(Long time);
}

但是这个时候你想在另外一个FeignClient中使用springmvc的语法进行发起的时候…………

@FeignClient(value = "base-producer-cluster",configuration = Configuration2.class)
public interface ClientV2Service {

    /**
     * 测试的服务获取
     * @return
     */
    @RequestMapping(value = "getService",method = RequestMethod.POST)
    String getService();

}

启动项目你会发现这样的错误

	at dailearn.feign.FeignConfigApplication.main(FeignConfigApplication.java:20) [classes/:na]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dailearn.feign.producer.ClientV2Service': 
FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: Method getService not annotated with HTTP method type (ex. GET, POST)
	at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBean
	......
	... 19 common frames omitted
Caused by: java.lang.IllegalStateException: Method getService not annotated with HTTP method type (ex. GET, POST)
	at feign.Util.checkState(Util.java:128) ~[feign-core-9.5.1.jar:na]
	at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:99) ~[feign-core-9.5.1.jar:na]

截止到目前假如你和我当时操作一样并且错误一样,我想我们可以遇到了一样的问题。

问题原因以及解决方案

首先我只能在这里告知我发现问题的原因,和一个根本不算解决方案的结果

解决方案

要想解决这个问题需要有两个地方需要注意:

  1. FeignClient注解中value、serviceId、name这三个值是一个意思,它默认取第一个
  2. FeignClient中的value(serviceId、name)值相同的FeignClient会使用一种配置,也就是说你针对某个应用创建了一套配置,那么所有对这个应用的请求都会套用这个配置。

所以想找到一个比较彻底的解决方案的同学,可以离开了,我这里只能给你们这些帮助了。当然你们要能找到针对一个应用套用不同配置的方法,也希望你们找到答案后能告知我。下面是我当时排查原因的一个步骤,对为什么产生这个问题原因的同学可以看一看。

为什么会出现这个问题?

首先、是异常堆栈

首先我们先看下异常堆栈,我们忽略掉哪些无用的spring异常可以看到下面内容。

Caused by: java.lang.IllegalStateException: Method getService not annotated with HTTP method type (ex. GET, POST)
	at feign.Util.checkState(Util.java:128) ~[feign-core-9.5.1.jar:na]
	at feign.Contract$BaseContract.parseAndValidateMetadata(Contract.java:99) ~[feign-core-9.5.1.jar:na]
	at org.springframework.cloud.openfeign.support.SpringMvcContract.parseAndValidateMetadata(SpringMvcContract.java:133) ~[spring-cloud-openfeign-core-2.0.1.RELEASE.jar:2.0.1.RELEASE]
	at feign.Contract$BaseContract.parseAndValidatateMetadata(Contract.java:66) ~[feign-core-9.5.1.jar:na]
	at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:146) ~[feign-core-9.5.1.jar:na]
	at feign.ReflectiveFeign.newInstance(ReflectiveFeign.java:53) ~[feign-core-9.5.1.jar:na]
	at feign.Feign$Builder.target(Feign.java:218) ~[feign-core-9.5.1.jar:na]

Contract到底发生了什么

首先我们在这里设置断点查看到底出了什么问题at feign.ReflectiveFeign$ParseHandlersByName.apply(ReflectiveFeign.java:146)

现在我们到下面内容
在这里插入图片描述

但是实际上base-producer-cluster使用了一个Configuration2的配置,而这个配置中使用的是FeignClientsConfiguration的配置。这个配置明明使用的是springmvc的契约配置,但实际中缺使用的是Default

@FeignClient(value = "base-producer-cluster",configuration = Configuration2.class)

我们看到Contract出现了错误,而我们自定义配置中自定义的就是Contract。目前feign中主要有两个实现类SpringMvcContractDefault默认的实现类,分别支持springmvc和feign格式。

设置的时机

既然知道哪里的逻辑出现了预期之外的内容,那么这Contract实现类都是什么时候被设置进来的呢?
首先我们在异常堆栈中看到这个内容at feign.Feign$Builder.target(Feign.java:218) ~[feign-core-9.5.1.jar:na]。本能告诉我这里面应该有设置参数的逻辑。

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }
    
    public Feign build() {
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                                               logLevel, decode404);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder,
                                  errorDecoder, synchronousMethodHandlerFactory);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
    }
    

我们发现Fegin使用Build来创建自己的Fegin。上面这一段代码是属于Feign的子类Builder
在这里插入图片描述

而此时Contract的内容已经不符合我们要求了,这个时候我们根据方法栈来反推逻辑

在这里插入图片描述

  1. 首先哦们看到了build的方法调用,一般对象的赋值很可能在这里面。但是此时contract已经被赋值,那么我们需要继续往前看。

我们继续向前看到了FeignClientFactoryBean的这个方法

	protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
			HardCodedTarget<T> target) {
		Client client = getOptional(context, Client.class);
		if (client != null) {
			builder.client(client);
			Targeter targeter = get(context, Targeter.class);
			return targeter.target(this, builder, context, target);
		}

		throw new IllegalStateException(
				"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
	}

我们发现了Feign.Builder的这个参数,而它正式创建Feign的关键,那么我们要找到它最终的创建点,最后在同类的getObject方法里面看到了Builder创建的方法。

	public Object getObject() throws Exception {
		FeignContext context = applicationContext.getBean(FeignContext.class);
		// Builder被创建的方法
		Feign.Builder builder = feign(context);

		if (!StringUtils.hasText(this.url)) {
			String url;
			if (!this.name.startsWith("http")) {
				url = "http://" + this.name;
			}
			else {
				url = this.name;
			}
			url += cleanPath();
			return loadBalance(builder, context, new HardCodedTarget<>(this.type,
					this.name, 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 targeter.target(this, builder, context, new HardCodedTarget<>(
				this.type, this.name, url));
	}

在这里我们看到了Builder被赋值的逻辑,而Feign通过从FeignContext中获取指定类型的实现类来进行值的赋值,而FeignContext的来源是从容器中获得FeignContext类的Bean获取的FeignContext context = applicationContext.getBean(FeignContext.class);

至于applicationContext怎么来的,可以看下这个类实现的接口class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware。熟悉spring的同学应该知道容器初始化的时候可以这样拿到容器对象。

	protected Feign.Builder feign(FeignContext context) {
		FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
		Logger logger = loggerFactory.create(this.type);

		// @formatter:off
		Feign.Builder builder = get(context, Feign.Builder.class)
				// required values
				.logger(logger)
				.encoder(get(context, Encoder.class))
				.decoder(get(context, Decoder.class))
				.contract(get(context, Contract.class));
		// @formatter:on

		configureFeign(context, builder);

		return builder;
	}

现在我们看下取值的逻辑

	protected <T> T get(FeignContext context, Class<T> type) {
		T instance = context.getInstance(this.name, type);
		if (instance == null) {
			throw new IllegalStateException("No bean found of type " + type + " for "
					+ this.name);
		}
		return instance;
	}

可以看到,Feign通过两个参数获得对应配置对象,this.nametype。而type是传递的参数那么我们看下this.name

现在我们看下FeignClient注解,发现value就是其name

public @interface FeignClient {

	/**
	 * The name of the service with optional protocol prefix. Synonym for {@link #name()
	 * name}. A name must be specified for all clients, whether or not a url is provided.
	 * Can be specified as property key, eg: ${propertyKey}.
	 */
	@AliasFor("name")
	String value() default "";

}

根据堆栈信息中可以看到this.name的值也是如此。
在这里插入图片描述

也就说一个name只能存在一种配置。并且@FeignClient注解的注释显示,value、serviceId、name这三个代表的意思是一样的。所以这也意味着,对于同一个服务调用的目标应用,你只能维护一种配置。


本篇文章并未贴出所有代码,涉及的源码下载地址:https://gitee.com/daifylearn/cloud-learn

ps.上述的所有项目都是可以成功运行的。但是在后期为了实现每个应用端口尽量不冲突会有些许调整,而后续某次作死调整结构和名称可能会导致部分项目无法运行o(╯□╰)o,如果发现请留言我进行修改。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值