Feign多种类型的POST

Feign NB

Posted by hcy on December 18, 2019

Feign多种类型的POST

三种携带请求体的方式

前文Feign高级功能说了,Feign有三种方式实现请求体,分别是

  • 使用@Body注解
  • 使用一个不加注解的参数
  • 使用若干个添加@Param但未在@ReqeustLine、@Heads...等地方使用的参数

POST的三种常见ContentType

而http的post有三种类型,分别是:

  • application/json
  • x-www.form-urlencoded
  • form-data

下面按照这三种ContentType分别讨论一下。

1.发送application/json格式的请求体

发送此类post请求,需要添加头部@Heads("Content-Type:application/json")

使用@Body发送

注意使用@Body的方式构建请求体,别忘了注解中的值左右两遍的大括号需要转义。

1
2
3
4
5
6
7
   interface TestJson {

        @RequestLine(value = "POST /test")
        @Body("%7B\"user_name\":\"{userName}\"%7D")
        @Headers("Content-Type:application/json")
        Response getItemCoupon(@Param("userName") String userName);
    }

使用不加注解的参数

1
2
3
4
5
    interface TestJson2 {
        @RequestLine(value = "POST /test")
        @Headers("Content-Type:application/json")
        Response getItemCoupon(TestParam body);
    }
1
2
3
4
5
        TestJson2 target = Feign.builder()
                .encoder(new JacksonEncoder())
                .target(TestJson2.class, "http://localhost:8081");

        target.getItemCoupon(new TestParam("username", "password"));

接口中有一个没有注解的参数TestParam,build时需要配置JacksonEncoder来处理它,将TestParam序列化成JSON字符串作为请求体。

使用没有使用的@Param 参数

1
2
3
4
5
    interface TestJson3 {
        @RequestLine(value = "POST /test")
        @Headers("Content-Type:application/json")
        Response getItemCoupon(@Param("userName") String userName, @Param("password") String passWord);
    }
1
2
3
4
5
 TestJson2 target = Feign.builder()
                .encoder(new JacksonEncoder())
                .target(TestJson2.class, "http://localhost:8081");

        target.getItemCoupon("username","password");

请看接口中有两个参数,但没有被使用,那么这两个参数就会组成一个Map,然后被配置的JacksonEncoder转成json字符串作为请求体。

结果报文

image-20191218180418698

2.发送x-www-form-urlencoded格式的post请求

使用@Body发送

1
2
3
4
5
6
    interface TestWwwFormUrlencoded {
        @RequestLine(value = "POST /test")
        @Headers("Content-Type:application/x-www-form-urlencoded")
        @Body("name={name}&age={age}")
        Response getItemCoupon(@Param("name") String name, @Param("age") Integer age);
    }
1
2
3
4
        TestWwwFormUrlencoded target = Feign.builder()
                .target(TestWwwFormUrlencoded.class, "http://localhost:8081");
        Response xiao = target.getItemCoupon(UriUtils.encode("小明"), 18);
	

这样的用法要注意两点,1.需要自己手工处理url编码,2.需要自己拼接参数,且数量有限。

虽然能写任意字符串拼接到body中,但是很麻烦不推荐这种用法。

使用不加注解的参数

1
2
3
4
5
    interface TestWwwFormUrlencoded2 {
        @RequestLine(value = "POST /test")
        @Headers("Content-Type:application/x-www-form-urlencoded")
        Response getItemCoupon(String param);
    }

如果这样的话,传参需要自己拼接参数,还要自己url转码,很麻烦不推荐。

使用map来接收参数,并配合自定义encoder处理

1
2
3
4
5
    interface TestWwwFormUrlencoded3 {
        @RequestLine(value = "POST /test")
        @Headers("Content-Type:application/x-www-form-urlencoded")
        Response getItemCoupon(Map<String, Object> params);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
    public static class WwwEncode implements Encoder {

        @Override
        public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
            Map<String, Object> map = (Map<String, Object>) object;
            List<String> list = new ArrayList<>();
            map.forEach((k, v) -> {
                list.add(UriUtils.encode(k) + "=" + UriUtils.encode(String.valueOf(v)));
            });
            String body = String.join("&", list);
            template.body(Request.Body.encoded(body.getBytes(), StandardCharsets.UTF_8));
        }
    }

这样比第一种方法方便点,但掉用时传递的参数是map,在不确定参数数目是很好用,但不利于识别,也不推荐。

使用没有使用的@Param 参数

1
2
3
4
5
    interface TestWwwFormUrlencoded4 {
        @RequestLine(value = "POST /test")
        @Headers("Content-Type:application/x-www-form-urlencoded")
        Response getItemCoupon(@Param("name") String userName, @Param("age") String passWord);
    }
1
2
3
4
5
6
7
8
9
10
11
12
    public static class WwwEncode implements Encoder {
        @Override
        public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
            Map<String, Object> map = (Map<String, Object>) object;
            List<String> list = new ArrayList<>();
            map.forEach((k, v) -> {
                list.add(UriUtils.encode(k) + "=" + UriUtils.encode(String.valueOf(v)));
            });
            String body = String.join("&", list);
            template.body(Request.Body.encoded(body.getBytes(), StandardCharsets.UTF_8));
        }
    }

Feign会把参数放到一个map里,传入encoder中。

同样需要encoder,,这种方法在确定参数数量的情况下比较好用,并且有参数名的提示,比上面那种方法用着舒服点,但要求固定的参数数量,多数情况下都是这样的。

并且可以同时使用@QueryMap注解,将多余的注解拼在url上,虽然不太好,但也是可以的。

结果报文

image-20191218180217041

3. 发送form-data格式的请求

这种格式主要用于上传文件,但Feign上传文件还是挺麻烦的,这里不讲上传文件的方法。如果服务端必须要求是form-data格式的请求,Feign也能实现,下面讲讲feign使用form-data发送post请求。

form-data介绍

因为上面讲到的x-www-form-urlencoded形式的请求需要把字符进行url编码,这样体积一下就大了很多,而且不能用法发文件,所以form-data格式就被发明出来了。它使用一个约定好的符号进行分隔参数。报文如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /test HTTP/1.1
Host: localhost:8081
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Host: localhost:8081
Content-Length: 266

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="name"

abc
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="age"

18
------WebKitFormBoundary7MA4YWxkTrZu0gW--

上面的报文可以看出请求体部分是由boundary开始,由参数值结束。

image-20191218181740787

boundary在请求头中约定好,且用于参数分隔的boundary要比请求头中定义的多了两个--

最后收尾的boundary的尾部再多了两个–-

所以真正的请求体格式是

1
2
3
4
5
6
7
8
9
10
11
 bound = 111

--111
Content-Disposition: form-data; name="name"

abc
--111
Content-Disposition: form-data; name="age"

18
--111--		//boundary比head中定义的多两个`--`,收尾的又在尾部多两个`--`

使用@Body发送

  • 处理不了,在Body注解中拼接请求体太麻烦了,所以不采用这种方式。

使用不加注解的参数

1
2
3
4
    interface TestFormData {
        @RequestLine(value = "POST /test")
        Response getItemCoupon(Map<String, Object> map);
    }

定义自定义encoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    public static class FormDataencodedPost implements Encoder {
        @Override
        public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
            String boundary = "--------------" + System.currentTimeMillis();
            Map<String, Object> map = (Map<String, Object>) object;
            StringBuilder sb = new StringBuilder();
            map.forEach((k, v) -> {
                sb.append("--").append(boundary).append("\r\n");
                sb.append("Content-Disposition: form-data; name=\"").append(k).append("\"").append("\r\n");
                sb.append("\r\n");
                sb.append(v).append("\r\n");
            });
            sb.append("--").append(boundary).append("--\r\n");
            template.body(Request.Body.encoded(sb.toString().getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8));

            template.removeHeader("Content-Type");
            template.header("Content-Type", "multipart/form-data; boundary=" + boundary);
        }
    }
1
2
3
4
5
6
7
8
9
        TestFormData target = Feign.builder()
                .encoder(new FormDataencodedPost())
                .target(TestFormData.class, "http://localhost:8081");

        HashMap<String, Object> params = new HashMap<>();
        params.put("name", "老王");
        params.put("age", 12);

        target.getItemCoupon(params);
  • 参数是map,所以此处encoder中拿到的参数就是map,然后遍历map,将参数和值拼接成上面讲的form-data所需的形式。
  • boundary使用的是-----+时间戳的形式,也可以用其他随机生成的方式,for循环里面拼接boundary时,在每个boundary前面添加两个-,然后注意\r\n别遗漏了。
  • 参数循环拼接完成后在最后面在添加一个收尾的boundary,此boundary的前后各添加两个-
  • 还要注意的一点时,先清空Content-Type再添加Content-Type,讲boundary设进去。

使用没有使用的@Param 参数

​ 使用这个其实也是讲参数封装成map进行encoder和上面是一样的这里就不写了。

结果报文

image-20191218183647159

form-data的拼接方式比较复杂,如果发送完后台接收不到数据,请仔细核对报文格式是否正确。可以打开WireShark抓包,再用其他工具进行正常的post,仔细比对两者报文的差异。

总结

以上就是使用Feign发送三种类型的POST请求的方法,业务上需要的场景一般就都能满足了,案例中用到的encoder写的比较简单,真实使用时请注意参数校验,处理好异常情况,或写更通用的encoder


转载请注明出处:https://www.huangchaoyu.com/2019/12/18/openFeign-多种类型的POST/