代码信息
本篇文章涉及代码版本
组件 | 版本 |
---|---|
Spring Boot | 2.0.8.RELEASE |
Spring Cloud | Finchley.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
中@PathVariable
的value
值必须设置
错误: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]
截止到目前假如你和我当时操作一样并且错误一样,我想我们可以遇到了一样的问题。
问题原因以及解决方案
首先我只能在这里告知我发现问题的原因,和一个根本不算解决方案的结果
解决方案
要想解决这个问题需要有两个地方需要注意:
- FeignClient注解中value、serviceId、name这三个值是一个意思,它默认取第一个
- 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中主要有两个实现类
SpringMvcContract
和Default
默认的实现类,分别支持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的内容已经不符合我们要求了,这个时候我们根据方法栈来反推逻辑
- 首先哦们看到了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.name
和type
。而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,如果发现请留言我进行修改。