文章目录
大家接触Feign都是通过Spring Cloud,平时使用的均是Spring MVC的注解。但是Feign是完全不依赖SpringMvc的注解的,是可以直接使用的哟。
1 @RequestLine
1.1 介绍
// 只能用在方法上
@java.lang.annotation.Target(METHOD)
@Retention(RUNTIME)
public @interface RequestLine {
//定义请求方式,请求路径,还有请求参数
String value();
// 是否编码/符号,默认是会编码的,也就是转义的意思
boolean decodeSlash() default true;
// 默认支持URL传多值,是通过key来传输的。形如:key=value1&key=value2&key=value3
// CollectionFormat不同的取值对应不同的分隔符,一般不建议改
CollectionFormat collectionFormat() default CollectionFormat.EXPLODED;
}
- 只能用在方法上
- 通过value参数定义请求方式,请求路径,还有请求参数
1.2 演示
1.2.1 准备工作:Feign日志打印
Feign内置了日志打印的
- feign.Logger.JavaLogger:使用的java.util.logging.Logger输出,但是日志级别的FINE级别,默认不会输出到控制台
- feign.Logger.ErrorLogger:错误输出。使用的System.err.printf()输出
- feign.Logger.NoOpLogger:什么都不输出,它是Feign的默认使用的Logger实现,也就是不会给控制台输出
定义一个Feign客户端构建器:
package study.wyy.feign.java.spi;
import feign.Feign;
import feign.Logger;
import feign.Retryer;
/**
* @author by wyaoyao
* @Description
* @Date 2020/12/16 4:21 下午
*/
public class FeignClientBuilder<T> {
public static String HOST = "http://localhost:8001";
public static <T> T build(Class<T> clazz){
return Feign.builder()
// 输出日志到控制台
.logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL)
// 关闭重试
.retryer(Retryer.NEVER_RETRY)
// 404进行编码,404的时候就不会抛出异常了
.decode404()
.target(clazz,HOST);
}
}
1.2.2 服务提供
在00-service-provider
模块提供下面两个rest接口
package study.wyy.feign.provider.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* @author by wyaoyao
* @Description
* @Date 2020/12/16 5:17 下午
*/
@Slf4j
@RequestMapping("/feign/provider")
@RestController
public class RequestLineController {
/****
* 测试基本使用:和入门案例一样
*/
@GetMapping("/requestLine1")
public String RequestLine1(@RequestParam String name){
return "RequestLine " + name;
}
/****
* 测试参数为map
*/
@GetMapping("/requestLine2")
public String RequestLine2(@RequestParam Map<String,Object> param) throws JsonProcessingException {
log.info("requestLine2 param: {}",param);
// 转成字符串返回
return new ObjectMapper().writer().writeValueAsString(param);
}
/****
* 测试传递多个参数
*/
@GetMapping("/requestLine3")
public String RequestLine3(@RequestParam String name,@RequestParam int num){
return "RequestLine " + name + " " + num;
}
}
1.2.3 Feign客户端
- 声明对应的接口
package study.wyy.feign.java.consumer;
import feign.Param;
import feign.QueryMap;
import feign.RequestLine;
import java.util.Map;
/**
* @author: wyaoyao
* @date: 2020-12-17 14:04
* @description: requestLine注解演示
*/
public interface RequestLineClient {
/****
* 测试基本使用:和入门案例一样
*/
@RequestLine("GET /feign/provider/requestLine1?name={name}")
String consumerRequestLine1(@Param("name") String name);
/****
* 测试参数为map
*/
@RequestLine("GET /feign/provider/requestLine2")
String consumerRequestLine2(@QueryMap Map<String,Object> param);
/****
* 测试基本使用:和入门案例一样
*/
@RequestLine("GET /feign/provider/requestLine3?name={name}&num={num}")
String consumerRequestLine3(@Param("name") String name,@Param("num") int num);
}
- 测试
package study.wyy.feign.java.test;
import org.junit.Before;
import org.junit.Test;
import study.wyy.feign.java.consumer.RequestLineClient;
import study.wyy.feign.java.spi.FeignClientBuilder;
import java.util.HashMap;
import java.util.Map;
/**
* @author: wyaoyao
* @date: 2020-12-17 14:00
* @description: @RequestLine 测试
*/
public class RequestLineTest {
private RequestLineClient client;
@Before
public void before(){
client = FeignClientBuilder.build(RequestLineClient.class);
}
@Test
public void testConsumerRequestLine1() {
String result = client.consumerRequestLine1("Feign");
System.out.println(result);
}
@Test
public void testConsumerRequestLine2() {
Map<String, Object> param = new HashMap<>();
param.put("name","Feign");
param.put("num",2020);
String s = client.consumerRequestLine2(param);
System.out.println(s);
}
@Test
public void testConsumerRequestLine3() {
String s = client.consumerRequestLine3("Feign",2020);
System.out.println(s);
}
}
输出:
[RequestLineClient#consumerRequestLine1] ---> GET http://localhost:8001/feign/provider/requestLine1?name=Feign HTTP/1.1
[RequestLineClient#consumerRequestLine1] ---> END HTTP (0-byte body)
[RequestLineClient#consumerRequestLine1] <--- HTTP/1.1 200 (104ms)
[RequestLineClient#consumerRequestLine1] connection: keep-alive
[RequestLineClient#consumerRequestLine1] content-length: 17
[RequestLineClient#consumerRequestLine1] content-type: text/plain;charset=UTF-8
[RequestLineClient#consumerRequestLine1] date: Thu, 17 Dec 2020 07:05:20 GMT
[RequestLineClient#consumerRequestLine1] keep-alive: timeout=60
[RequestLineClient#consumerRequestLine1]
[RequestLineClient#consumerRequestLine1] RequestLine Feign
[RequestLineClient#consumerRequestLine1] <--- END HTTP (17-byte body)
RequestLine Feign
[RequestLineClient#consumerRequestLine2] ---> GET http://localhost:8001/feign/provider/requestLine2?num=2020&name=Feign HTTP/1.1
[RequestLineClient#consumerRequestLine2] ---> END HTTP (0-byte body)
[RequestLineClient#consumerRequestLine2] <--- HTTP/1.1 200 (20ms)
[RequestLineClient#consumerRequestLine2] connection: keep-alive
[RequestLineClient#consumerRequestLine2] content-length: 29
[RequestLineClient#consumerRequestLine2] content-type: text/plain;charset=UTF-8
[RequestLineClient#consumerRequestLine2] date: Thu, 17 Dec 2020 07:05:20 GMT
[RequestLineClient#consumerRequestLine2] keep-alive: timeout=60
[RequestLineClient#consumerRequestLine2]
[RequestLineClient#consumerRequestLine2] {"num":"2020","name":"Feign"}
[RequestLineClient#consumerRequestLine2] <--- END HTTP (29-byte body)
{"num":"2020","name":"Feign"}
[RequestLineClient#consumerRequestLine3] ---> GET http://localhost:8001/feign/provider/requestLine3?name=Feign&num=2020 HTTP/1.1
[RequestLineClient#consumerRequestLine3] ---> END HTTP (0-byte body)
[RequestLineClient#consumerRequestLine3] <--- HTTP/1.1 200 (3ms)
[RequestLineClient#consumerRequestLine3] connection: keep-alive
[RequestLineClient#consumerRequestLine3] content-length: 22
[RequestLineClient#consumerRequestLine3] content-type: text/plain;charset=UTF-8
[RequestLineClient#consumerRequestLine3] date: Thu, 17 Dec 2020 07:05:20 GMT
[RequestLineClient#consumerRequestLine3] keep-alive: timeout=60
[RequestLineClient#consumerRequestLine3]
[RequestLineClient#consumerRequestLine3] RequestLine Feign 2020
[RequestLineClient#consumerRequestLine3] <--- END HTTP (22-byte body)
RequestLine Feign 2020
1.3 使用注意点
- 请求方法必须大写:GET,POST。。
因为底层是通过下面的枚举feign.Request.HttpMethod
映射
public enum HttpMethod {
GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
}
RequestLine
注解的首个单词必须是HTTP方法,且必须顶格写(前面不允许有空格),但后面是需要有空格
2 @Param
2.1 介绍
通过名称定义模板变量,其值将用于填入上面的模版:@Headers/@RequestLine/@Body均可使用模版表达式。
比如说刚刚的:
/****
* RequestLine 就是定义一个模板,定义了一下内容:
* 请求方式
* 请求路径
* 请求参数,@Param就会把形参中的具体填充到模板中的name参数
*/
@RequestLine("GET /feign/provider/requestLine1?name={name}")
String consumerRequestLine1(@Param("name") String name);
@Retention(RUNTIME)
// 只能用在参数上
@java.lang.annotation.Target(PARAMETER)
public @interface Param {
/**
* 名称(key),和模版会进行匹配然后填充 必填项
*/
String value();
/**
* 如何把值填充上去,默认是调用其toString方法直接填上去
*/
Class<? extends Expander> expander() default ToStringExpander.class;
/**
* 是否转义,默认不转义,直接放上去
*/
boolean encoded() default false;
interface Expander {
/**
* Expands the value into a string. Does not accept or return null.
*/
String expand(Object value);
}
final class ToStringExpander implements Expander {
@Override
public String expand(Object value) {
return value.toString();
}
}
}
2.2 使用演示
2.2.1 基本使用演示
- 定义一个rest接口
package study.wyy.feign.provider.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author by wyaoyao
* @Description
* @Date 2020/12/16 5:17 下午
*/
@Slf4j
@RequestMapping("/feign/provider")
@RestController
public class ParamController {
@GetMapping("/param1")
public String param1(@RequestParam String name){
return "@Param " + name;
}
}
- 定义Feign接口
package study.wyy.feign.java.consumer;
import feign.Param;
import feign.RequestLine;
/**
* @author: wyaoyao
* @date: 2020-12-17 15:45
* @description:
*/
public interface ParamClient {
/**
* 测试1: 使用Param注解
* @param
* @return
*/
@RequestLine("GET /feign/provider/param1?name={name}")
String consumerParam1(@Param("name") String str);
/**
* 测试1: 不使用Param注解,形参和模板中的参数key对应
* @param
* @return
*/
@RequestLine("GET /feign/provider/param1?name={name}")
String consumerParam2(String name);
/**
* 测试1: 不使用Param注解,但是形参和模板中的参数key不对应
* @param
* @return
*/
@RequestLine("GET /feign/provider/param1?name={name}")
String consumerParam3(String str);
}
- 测试
package study.wyy.feign.java.test;
import org.junit.Before;
import org.junit.Test;
import study.wyy.feign.java.consumer.ParamClient;
import study.wyy.feign.java.spi.FeignClientBuilder;
import java.util.HashMap;
import java.util.Map;
/**
* @author: wyaoyao
* @date: 2020-12-17 14:00
* @description: @RequestLine 测试
*/
public class ParamTest {
private ParamClient client;
@Before
public void before(){
client = FeignClientBuilder.build(ParamClient.class);
}
@Test
public void testConsumerParam1() {
String result = client.consumerParam1("Feign");
System.out.println(result);
}
@Test
public void testConsumerParam2() {
String s = client.consumerParam2("Feign");
System.out.println(s);
}
@Test
public void testConsumerParam3() {
String s = client.consumerParam3("Feign");
System.out.println(s);
}
}
- 测试结果
只有第一个可以测试通过,第二个第三个都是同一个错误:
[ParamClient#consumerParam3] ---> GET http://localhost:8001/feign/provider/param1 HTTP/1.1 # 参数根本没有拼接
[ParamClient#consumerParam3] Content-Length: 5
[ParamClient#consumerParam3]
[ParamClient#consumerParam3] Feign
[ParamClient#consumerParam3] ---> END HTTP (5-byte body)
[ParamClient#consumerParam3] <--- HTTP/1.1 405 (18ms)
[ParamClient#consumerParam3] allow: GET
[ParamClient#consumerParam3] connection: keep-alive
[ParamClient#consumerParam3] content-type: application/json
[ParamClient#consumerParam3] date: Thu, 17 Dec 2020 07:54:20 GMT
[ParamClient#consumerParam3] keep-alive: timeout=60
[ParamClient#consumerParam3] transfer-encoding: chunked
[ParamClient#consumerParam3]
[ParamClient#consumerParam3] {"timestamp":"2020-12-17T07:54:20.980+0000","status":405,"error":"Method Not Allowed","message":"Request method 'POST' not supported","path":"/feign/provider/param1"}
[ParamClient#consumerParam3] <--- END HTTP (166-byte body)
feign.FeignException$MethodNotAllowed: status 405 reading ParamClient#consumerParam3(String)
上面日志的输出可以看出
http://localhost:8001/feign/provider/param1 HTTP/1.1 # 参数根本没有拼接
所以@Param
是不能省略的,即使形参的参数名字和模板中的一致也不可以省略
2.2.2 数组和集合
- 增加两个rest接口
@GetMapping("/param2")
public String param2(@RequestParam String[] names){
return "@Param " + names;
}
@GetMapping("/param3")
public String param3(@RequestParam List<String> names){
return "@Param " + names;
}
- Feign接口
@RequestLine("GET /feign/provider/param2?names={names}")
public String testArr(@Param("names") String[] names);
@RequestLine("GET /feign/provider/param3?names={names}")
public String testCollection(@Param("names") List<String> names);
- 测试
@Test
public void testArr(){
System.out.println(client.testArr(new String[]{"java","python"}));
}
@Test
public void testCollection(){
List<String> list = new ArrayList<>();
list.add("java");
list.add("python");
System.out.println(client.testCollection(list));
}
- 测试结果
[ParamClient#testArr] ---> GET http://localhost:8001/feign/provider/param2?names=%5BLjava.lang.String&names=@47f37ef1 HTTP/1.1
[ParamClient#testArr] ---> END HTTP (0-byte body)
[ParamClient#testArr] <--- HTTP/1.1 200 (19ms)
[ParamClient#testArr] connection: keep-alive
[ParamClient#testArr] content-length: 35
[ParamClient#testArr] content-type: text/plain;charset=UTF-8
[ParamClient#testArr] date: Thu, 17 Dec 2020 08:17:17 GMT
[ParamClient#testArr] keep-alive: timeout=60
[ParamClient#testArr]
[ParamClient#testArr] @Param [Ljava.lang.String;@36386bb1
[ParamClient#testArr] <--- END HTTP (35-byte body)
@Param [Ljava.lang.String;@36386bb1
[ParamClient#testCollection] ---> GET http://localhost:8001/feign/provider/param3?names=java&names=python HTTP/1.1
[ParamClient#testCollection] ---> END HTTP (0-byte body)
[ParamClient#testCollection] <--- HTTP/1.1 200 (19ms)
[ParamClient#testCollection] connection: keep-alive
[ParamClient#testCollection] content-length: 21
[ParamClient#testCollection] content-type: text/plain;charset=UTF-8
[ParamClient#testCollection] date: Thu, 17 Dec 2020 08:19:41 GMT
[ParamClient#testCollection] keep-alive: timeout=60
[ParamClient#testCollection]
[ParamClient#testCollection] @Param [java, python]
[ParamClient#testCollection] <--- END HTTP (21-byte body)
@Param [java, python]
注意这两次方法请求格式
GET http://localhost:8001/feign/provider/param2?names=%5BLjava.lang.String&names=@47f37ef1
GET http://localhost:8001/feign/provider/param3?names=java&names=python
可以看到,如果是Collection类型是能够很好的被解析成多值的,但是数组不行,因此多用集合少用数组哦(数组直接调用toString()方法了)。
3 Headers注解
3.1 介绍
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Headers {
String[] value();
}
能标注在类上和方法上。用于传请求头,使用起来比较简单,形如这样即可:
@Headers({"Accept:*/*", "Accept-Language:zh-cn"})
唯一注意的一点:k-v使用的是:链接,而不是=。
3 QueryMap注解
@Retention(RUNTIME)
@java.lang.annotation.Target(PARAMETER)
public @interface QueryMap {
boolean encoded() default false;
}
只能标注在方法参数上。用于传递多个查询值,拼接在URL后面,上面已经给出示例了。
仅需注意一点:只能标注在Map类型的参数前面,否则报错。
4 Body注解
4.1 介绍
前面都是get请求,那要是post请求呢,该如何向请求体中塞参数呢?
这就是@Body
干的事了
@Target(METHOD)
@Retention(RUNTIME)
public @interface Body {
String value();
}
@Body("{body}")
,这样就可以通过方法参数的@Param(“body”) String body传值喽。注意:这个值最终是以http body体的形式发送的(并非URL参数哦),body体的内容并不要求必须是json,一般请配合请求头使用。
4.2 使用演示
- 定义个model
package study.wyy.feign.model;
import lombok.Data;
import lombok.ToString;
/**
* @author: wyaoyao
* @date: 2020-12-17 13:17
* @description:
*/
@Data
@ToString
public class User {
private String name;
/**8
* 年龄
*/
private int age;
}
- 服务提供者定义一个Rest接口
package study.wyy.feign.provider.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import study.wyy.feign.model.User;
/**
* @author: wyaoyao
* @date: 2020-12-17 16:49
* @description:
*/
@Slf4j
@RequestMapping("/feign/provider")
@RestController
public class BodyController {
@PostMapping("body1")
public String body1(@RequestBody User user) {
if (null != user) {
return "Hello " + user.getName();
}
return "no one";
}
}
- 定义Feign的客户端接口
package study.wyy.feign.java.consumer;
import feign.Body;
import feign.Headers;
import feign.Param;
import feign.RequestLine;
import study.wyy.feign.model.User;
/**
* @author: wyaoyao
* @date: 2020-12-17 16:54
* @description:
*/
public interface BodyClient {
@Body("{person}")
@RequestLine("POST /feign/provider/body1")
String consumer1(@Param("person") User person);
}
- 测试
package study.wyy.feign.java.test;
import org.junit.Before;
import org.junit.Test;
import study.wyy.feign.java.consumer.BodyClient;
import study.wyy.feign.java.spi.FeignClientBuilder;
import study.wyy.feign.model.User;
/**
*
* @author: wyaoyao
* @date: 2020-12-17 14:00
* @description: @RequestLine 测试
*/
public class BodyTest {
private BodyClient client;
@Before
public void before(){
client = FeignClientBuilder.build(BodyClient.class);
}
@Test
public void test1() {
User user = new User();
user.setName("Feign");
user.setAge(12);
String result = client.consumer1(user);
System.out.println(result);
}
}
- 测试结果: 抛出Unsupported Media Type异常
[BodyClient#consumer1] ---> POST http://localhost:8001/feign/provider/body1 HTTP/1.1
[BodyClient#consumer1] Content-Length: 24
[BodyClient#consumer1]
[BodyClient#consumer1] User(name=Feign, age=12)
[BodyClient#consumer1] ---> END HTTP (24-byte body)
[BodyClient#consumer1] <--- HTTP/1.1 415 (121ms)
[BodyClient#consumer1] connection: keep-alive
[BodyClient#consumer1] content-type: application/json
[BodyClient#consumer1] date: Thu, 17 Dec 2020 12:20:53 GMT
[BodyClient#consumer1] keep-alive: timeout=60
[BodyClient#consumer1] transfer-encoding: chunked
[BodyClient#consumer1]
[BodyClient#consumer1] {"timestamp":"2020-12-17T12:20:53.165+0000","status":415,"error":"Unsupported Media Type","message":"Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported","path":"/feign/provider/body1"}
[BodyClient#consumer1] <--- END HTTP (210-byte body)
feign.FeignException$UnsupportedMediaType: status 415 reading BodyClient#consumer1(User)
因为我们服务提供者使用了@@RequestBody
注解,所以请求要求"content-type
要为application/json
。所以呢要如何解决,前面可以通过@Headers
来指定请求头,所以:进行如下修改:
@Body("{person}")
@RequestLine("POST /feign/provider/body1")
@Headers({"content-type:application/json"})
String consumer1(@Param("person") User person);
- 再次测试:这次是400
[BodyClient#consumer1] Content-Length: 24
[BodyClient#consumer1] content-type: application/json
[BodyClient#consumer1]
[BodyClient#consumer1] User(name=Feign, age=12) # 原因就在这,Feign的默认序列化方式是调用ToString方法,toString方法返回的并不是一个Json格式
[BodyClient#consumer1] ---> END HTTP (24-byte body)
[BodyClient#consumer1] <--- HTTP/1.1 400 (40ms)
[BodyClient#consumer1] connection: close
[BodyClient#consumer1] content-type: application/json
[BodyClient#consumer1] date: Thu, 17 Dec 2020 12:42:06 GMT
[BodyClient#consumer1] transfer-encoding: chunked
[BodyClient#consumer1]
[BodyClient#consumer1] {"timestamp":"2020-12-17T12:42:06.633+0000","status":400,"error":"Bad Request","message":"JSON parse error: Unrecognized token 'User': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false'); nested exception is com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'User': was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')\n at [Source: (PushbackInputStream); line: 1, column: 6]","path":"/feign/provider/body1"}
[BodyClient#consumer1] <--- END HTTP (491-byte body)
feign.FeignException$BadRequest: status 400 reading BodyClient#consumer1(User)
User(name=Feign, age=12) :原因就在这,Feign的默认序列化方式是调用ToString方法,toString方法返回的并不是一个Json格式
- 验证一下使用json串是否可以
@Body("{\"name\" : \"wyaoyao\"}") # 这里先直接写死一个json串
@Headers({"Accept:*/*", "Accept-Language: zh-cn","content-type:application/json"})
@RequestLine("POST /feign/provider/body1")
String consumer2();
@Test
public void test2() {
String result = client.consumer2();
System.out.println(result);
}
测试通过,那么如何解决这个问题,目前先提一个简单的方法,那就是重写toString方法,让其返回一个json串
@Override
public String toString() {
String s = JSON.toJSONString(this);
return s;
}
再次测试就可以通过
[BodyClient#consumer1] ---> POST http://localhost:8001/feign/provider/body1 HTTP/1.1
[BodyClient#consumer1] Content-Length: 25
[BodyClient#consumer1] content-type: application/json
[BodyClient#consumer1]
[BodyClient#consumer1] {"age":12,"name":"Feign"} # 这里的格式就是json格式啦
[BodyClient#consumer1] ---> END HTTP (25-byte body)
[BodyClient#consumer1] <--- HTTP/1.1 200 (17ms)
[BodyClient#consumer1] connection: keep-alive
[BodyClient#consumer1] content-length: 11
[BodyClient#consumer1] content-type: text/plain;charset=UTF-8
[BodyClient#consumer1] date: Thu, 17 Dec 2020 12:54:15 GMT
[BodyClient#consumer1] keep-alive: timeout=60
[BodyClient#consumer1]
[BodyClient#consumer1] Hello Feign
[BodyClient#consumer1] <--- END HTTP (11-byte body)
Hello Feign
Feign默认情况下只能支持文本消息,但后来feign提供了feign-form这个扩展模块,所以也就能够支持二进制、文件上传喽。
需要说明的是:feign-form并不属于官方直接子模块,是后续新增的所以它的大版本号不跟主版本号走,GAV也有所不同: