通过Feign调用报错“Method Not Allowed 405“来探究它的执行原理

kelly-sikkema-PXl_S152jNM-unsplash.jpg

前几天在使用feign进行远程调用时,出现了"Method Not Allowed 405","Request method 'POST' not supported" 的错误,肉眼检查了一下代码,感觉没有任何问题,我先把代码简单贴出来看一下.
feign的客户端:

 

java

复制代码

@FeignClient(name = "serverFeignClient", url = "http://localhost:8080") public interface ServerClientFeign { @GetMapping("/server/hello") String test(QueryRequest queryRequest); } @Data class QueryRequest { private String name; private String date; private boolean valid; }

服务提供者:http://localhost:8080

 

java

复制代码

@RestController public class ServerController { @GetMapping("/server/hello") public String server(QueryParam queryParam) { return "hello success: " + queryParam; } } //`QueryParam` 属性值基本上和QueryRequest一样。

当发起请求时,就会报错:Request method 'POST' not supported,不支持POST请求?我这也没有POST呀,有点摸不到头脑,当时也没有仔细探究,暂时根据报错提示,我就把请求都改成POST了,就没有问题了,今天我们来看下为什么会出现这个问题。

feign是什么就不多做介绍了,它就是声明式的webservice客户端,集成了其他HTTP 客户端框架来发送请求,默认使用的是JDK提供的HttpURLConnection来发送请求,其他我们熟知的还有:okhttphttpClient,这些后面都会看到。趁着这次报错的机会,正好来看下feign的执行过程。

1.探究feign的执行原理

在springboot的生态中,使用一个功能一般可以从 xxxAutoConfiguration 自动配置类或者 @Enablexxx 注解入手;使用feign的时候,在导入依赖后,要通过 @EnableFeignClients 注解来开启feign的功能。我们就从这个注解入手。

 

java

复制代码

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { //..... }

导入了 FeignClientsRegistrar类:

 

java

复制代码

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { //重点看重写ImportBeanDefinitionRegistrar接口的方法 @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { //..... } }

它实现了 ImportBeanDefinitionRegistrar接口,熟悉springboot执行流程的应该都对这个接口有印象, 简单说就是给容器注册对象的。

我们对这个过程简单梳理一下:

1.1 通过@EnableFeignClients注解扫描指定包下的标注了@FeignClient注解的接口

image.png

1.2 注册Bean

在注册bean的时候有一个很重要的FeignClientFactoryBean对象, 它是一个FactoryBean, spring在整合其他框架的时候,经常会看到FactoryBean对象,比如mybatis。

image.png

既然是FactoryBean,那么就看它的getObject()方法就行。

image.png

顺便说一下在执行Feign.Builder builder = feign(context)方法时,会创建几个比较重要的对象,这里说明一下,后面会用到。

  1. 创建 SpringMvcContract 对象(在FeignClientsConfiguration 配置类)
  2. 创建 Feign.Builder 对象(在FeignClientsConfiguration 配置类)
  3. 创建 DefaultTargeter 对象(在FeignAutoConfiguration配置类)

ok, 接下来我们就继续往下看DefaultTargeter对象的target方法做了什么?

image.png

调用的是Feign.Builder 对象的target方法,那就继续跟进去看下:

image.png

返回了一个ReflectiveFeign对象。内部创建了一个InvocationHandlerFactory 对象,看名字就知道他是做什么的了吧。

我们再继续看下ReflectiveFeign对象的newInstance(target)方法

image.png

这个方法我们只关注标红的这2块,第1处和我们遇到的”Method Not Allowed 405“ 有关,第二处不用看也知道了,就是返回一个代理对象,使用的是JDK的动态代理。

image.png

我们就来看下,这2个方法都做了什么:

image.png

image.png

上面是类(接口)层面的限制,接下来看下方法层面的限制:

image.png

接下来就是参数层面的限制,这个也和我们遇到的问题有关:

image.png

checkState(data.bodyIndex() == null) 说明没有任何注解的参数只能有一个!

像这种就不行了,因为只能有一个。

 

java

复制代码

@FeignClient(name = "oneRClient", url = "http://localhost:8080") public interface OneRFeignClient { @GetMapping(value = {"/server/hello"}) String test(String name, String date, boolean valid); }

ok, 我们再来看下为什么processAnnotationOnParameter方法返回的是false ?

image.png

> 不难发现,这里面都是用来处理含有注解参数的,比如@RequestParam@PathVariable 等等,除了这些特定注解以外的请求参数,比如@RequestBody没有任何注解的参数,统统都会放到boyIndex中进行标记。

其实到这里就差不多了,其他的内容因为和题目中遇到的问题无关,这里也就不做过多赘述了,接下来我们调用feign的接口,看看他的运行逻辑:

1.3 请求feign

前面我们有提到,他会通过 FeignInvocationHandler 来创建代理对象,当发起请求的时候直接看它的invoke(...) 方法即可。

image.png

继续往下看:

image.png

我们先看buildTemplateFromArgs.create()方法的重点内容:

image.png

image.png

我们再继续看executeAndDecode(template, options)方法的内容:

image.png

拦截器的执行时机在这里。

调用execute()方法

image.png

再继续看 convertAndSend()方法:

image.png

image.png

ok, 这里就完整的演示了,为什么我的feign调用会出现 "status":405,"error":"Method Not Allowed" 的问题了。

1.4 如何解决?

最简单的解决办法就是在方法参数上加一个 @SpringQueryMap注解,其他的不用做任何改动。因为这个注解将会自定义对象转成Map,然后以名值对的形式拼接到url上。

2.集成HttpClient

 

xml

复制代码

<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> <version>11.8</version> </dependency>

开启:

 

yaml

复制代码

feign: httpclient: enabled: true

在启动阶段没有任何的差异,只是配置的客户端不一样而已,既然如此,那我们就直接请求,看Http Client的客户端是如何处理的即可。

依然是从FeignInvocationHandlerinvoke()方法作为分析的入口:

image.png

image.png

重点看body的处理:

image.png

不难发现,他没有将GET改为POST的行为,但是它是将参数以JSON的形式放到了请求体中,然后发送请求。但从这里看是没有问题的,但是接收方如何接收参数呢? 正常来说应该是这样的:

image.png

这样的话,虽然请求可以成功,但是无法获获取参数,所有值都是null, 现在请求方将参数以JSON的形式放到了请求体里面了,接收方要怎么获取呢? 用@RequestBody注解就可以从请求体中获取参数。

 

java

复制代码

@GetMapping("/server/hello") public String server(@RequestBody QueryParam queryParam) { return "请求参数:" + queryParam + ", 当前时间:" + LocalDateTime.now(); }

这样写虽然可以获取参数值,但是这种写法有点“另类”了。

所以, HttpClient相比默认的Client$Default, 可以发送请求,但是无法获取参数值。这一点要注意。

3.集成OkHttp

 

xml

复制代码

<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> <version>11.8</version> </dependency>

开启:

 

yaml

复制代码

feign: okhttp: enabled: true

启动阶段没有任何差异,只是配置了OkHttp的客户端,所以我们从发起请求开始看,以FeignInvocationHandler#invoke()作为入口:

image.png

我们还是看对body的处理:

image.png

在看下requestBuilder.method(input.httpMethod().name(), body)方法的逻辑:

image.png

HttpMethod.permitsRequestBody(method)方法的逻辑很简单:

 

java

复制代码

public static boolean permitsRequestBody(String method) { return !(method.equals("GET") || method.equals("HEAD")); }

到这里就很明显了,okHttp将会报错异常:

image.png

4.总结

还是以上面的代码为例: feign client:

 

java

复制代码

@FeignClient(name = "oneRClient", url = "http://localhost:8080") public interface OneRFeignClient { @GetMapping(value = {"/server/hello"}) String test(QueryRequest queryRequest); }

服务端:

 

java

复制代码

@RestController public class ServerController { @GetMapping("/server/hello") public String server(QueryParam queryParam) { return "请求参数2:" + queryParam + ", 当前时间:" + LocalDateTime.now(); } }

image.png

GET请求下:对于参数是Map,自定义类这种的,使用 @SpringQueryMap 可以将数据以键值对的形式拼接到url上。如果参数是单个String这种的,就不要用@SpringQueryMap 了,虽然它也会以键值对的形式拼接到url上,但是key固定是value, 值是内存地址,所以单个String这种的,乖乖用 @RequestParam就好了。

 

java

复制代码

@FeignClient(name = "oneRClient", url = "http://localhost:8080") public interface OneRFeignClient { @GetMapping(value = {"/server/hello"}) String test(@SpringQueryMap QueryRequest queryRequest); }

好了,关于在feign的使用过程中出现的错误就分析到这里吧。祝大家新年快乐,万事顺意。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值