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

相关推荐
鼠鼠我捏,要死了捏2 小时前
深入解析Java NIO多路复用原理与性能优化实践指南
java·性能优化·nio
ningqw2 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友2 小时前
vi编辑器命令常用操作整理(持续更新)
后端
superlls2 小时前
(Redis)主从哨兵模式与集群模式
java·开发语言·redis
胡gh2 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫3 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong3 小时前
技术人如何对客做好沟通(上篇)
后端
叫我阿柒啊4 小时前
Java全栈工程师面试实战:从基础到微服务的深度解析
java·redis·微服务·node.js·vue3·全栈开发·电商平台
颜如玉4 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源
Moment4 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源