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

前几天在使用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注解的接口

1.2 注册Bean

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

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

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

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

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

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

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

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

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

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

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

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

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 ?

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

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

1.3 请求feign

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

继续往下看:

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

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

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

调用execute()方法

再继续看 convertAndSend()方法:

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()方法作为分析的入口:

重点看body的处理:

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

这样的话,虽然请求可以成功,但是无法获获取参数,所有值都是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()作为入口:

我们还是看对body的处理:

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

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

java 复制代码
public static boolean permitsRequestBody(String method) {  
   return !(method.equals("GET") || method.equals("HEAD"));  
}

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

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

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的使用过程中出现的错误就分析到这里吧。祝大家新年快乐,万事顺意。

相关推荐
郑祎亦8 分钟前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
不是二师兄的八戒9 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生20 分钟前
Easyexcel(2-文件读取)
java·excel
本当迷ya21 分钟前
💖2025年不会Stream流被同事排挤了┭┮﹏┭┮(强烈建议实操)
后端·程序员
带多刺的玫瑰37 分钟前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导61 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study1 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言
Chris _data1 小时前
二叉树oj题解析
java·数据结构
牙牙7051 小时前
Centos7安装Jenkins脚本一键部署
java·servlet·jenkins
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端