前几天在使用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
来发送请求,其他我们熟知的还有:okhttp
和httpClient
,这些后面都会看到。趁着这次报错的机会,正好来看下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)
方法时,会创建几个比较重要的对象,这里说明一下,后面会用到。
- 创建
SpringMvcContract
对象(在FeignClientsConfiguration
配置类) - 创建
Feign.Builder
对象(在FeignClientsConfiguration
配置类) - 创建
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的客户端是如何处理的即可。
依然是从FeignInvocationHandler
的invoke()
方法作为分析的入口:
重点看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的使用过程中出现的错误就分析到这里吧。祝大家新年快乐,万事顺意。