报错405

这两天遇到了一个FeignClient和GetMapping组合报错405请求方法错误,在写代码的时候,遇到一个情况,在使用FeignClient调用服务,因为是获得数据的接口,并且参数有很多,所以就直接使用的实体类,但是却总是报错,报的还是很奇怪的405,请求方法错误,被调用服务抛错具体内容如下:

[org.springframework.web.HttpRequestMethodNotSupportedException: Request method ‘POST’ not supported]

通过全局异常捕捉时,发现在服务端显示的是调用源POST调用,可是我明明使用FeignClient的GetMapping发起的。(因为想着在SpringMVC可以直接接收实体对象,并且@RequestParam并不能直接作用于一个实体对象上,所以就直接如下写了FeignClient) 调用方代码如下:

@FeignClient(value = "service-hi", fallback = HelloServiceImpl.class)
public interface HelloService {
    @GetMapping("/{organizationId}/hi")
    List<String> sayHiFromClientOne(@PathVariable("organizationId") Long tenantId, BillDimensionDtlDTO billDimensionDtlDTO);
}

被调用服务接口如下,

@RestController("/{organizationId}")
public class HelloController {
    @Value("${server.port}")
    String port;

    @RequestMapping("/hi")
    public ResponseEntity<List<String>> home(@PathVariable("organizationId") Long tenantId, BillDimensionDtlDTO billDimensionDtlDTO) {
        System.out.println(billDimensionDtlDTO);
        List<String> strings = new ArrayList<>();
        strings.add("a");
        strings.add("b");
        strings.add(tenantId.toString());
        return ResponseEntity.ok(strings);
    }
}

事实上,通过分析造成如此的原因,我也整理了一下FeignClient的调用流程。这里先给出结论,为什么FeignClient发起的GetMapping会报405,是因为FeignClient最后是用HttpURLConnectiion发起的网络连接,在发起的过程中,Connection会判断其自身的body是否为空,如果不为空,则将 GET Method 转换为 POST Method。这也容易理解,因为body数据只能放在RquestBody内以流的形式传输,而param数据则在Http协议中直接放在URL上进行传输和获取,所以如果有body数据,理应转为POST请求,这也每种数据都能正确的传输到网络接口。但是,我们的服务端生产者是Get请求的接口,这样直接就会导致请求类型不一致报405,而为什么会造成转换,是因为body数据不为空,但是为什么body会有值,这要从初始化FeignClient和FeignClient的规则说起。在启动过程中,就将FeignClient注解的类进行动态代理,且初始化了代理类。

  1. 如果有@RequestParam(“xx”)注解,则会将参数作为key,放入RequestTemplate.Factory中,由urlIndex来指明数组索引,并且在解析的时候,根据每个key从arg数组获取到具体值,拼接在url后面;
  2. 如果没有任何注解,或者用@RequestBody贴在参数前面,则初始化RequestTemplate.Factory的时候,bodyIndex会维护参数索引,并且bodyType这个参数会携带参数类型

所以我们现在的场景是没有加任何注解,FeignClient会把数据放入body中,那么怎么解决呢,可以直接加@RequestParam注解吗,如改写成下面这样?答案是否定的,因为@RequestParam只能注解单个基本数据类型,上面的写法将会让url的dto的值是dto.toString()。我们可以将dto的数据全部拿出来,一个一个写在参数列表上,并都贴上@RequestParam注解,但是这样写法无异于让参数列表变的很长。为此,如果我们要传递这种封装了多个数据的实体数据,又不想一个一个拿出来写在参数列表,我们在FeignClient中就要打破RESTful规范,使用POST来发起请求调用,同时服务端也使用POST来接收。(按照上面的GET会转POST的理论,如果我们FeignClient调用端写的是GetMapping,参数不贴注解,只要服务端的生产者是POST请求加@RequestBody接收,那么也能正确接收并响应数据,经过实验发现确实如此)

405报错总结

对于get请求,路径上的参数必须使用@RequestParam注解或者@PathVariable 注解,否则将会报错405;对于Post请求,加上@RequestBody 就好。