人无远虑,必有近忧。
代码下载地址:https://github.com/f641385712/feign-learning
通过第一篇文章了解了Feign的最基础知识,作为一个有态度的程序员,我们势必要搞清楚它整个执行的脉络,了解它的执行过程和原理才算结束,这是最后最后最后进行个性化定制的基础,一切都为了“玩”嘛。
本文将讲解它原生支持的注解,毕竟Feign并不强依赖于Spring MVC
,在Java环境也是可以作为HC来使用的,了解起来不难,建议你掌握。
说明:本文依旧仅仅只导入核心包feign-core
包,因为所有的注解支持均在它的核心包内。
本文内容依旧站在使用的角度看Feign,并且会结合具体的使用示例来辅以说明,相信不会让人觉得枯燥。
由于大多数人了解Feign、使用Feign都是出于Spring Cloud
,平时使用的均是Spring MVC
的注解。所以未免对Feign的原生注解了解并不多,本文就来帮你扫盲,让你在实际使用过程中能更加的得心应手。
源生注解并不多,此处逐个介绍:
它只能标注在Method方法上。为请求定义HttpMethod
和UriTemplate
(标注在方法上的就是一个HttpMethod
,且写好了URI(可是绝对路径,也可是相对的,一般写后部分即可))。表达式、用大括号括起来的值{expression}
最终会用对应的@Param
注解填进去(根据key匹配)
@java.lang.annotation.Target(METHOD)
@Retention(RUNTIME)
public @interface RequestLine {
// 书写请求方法 + URI
String value();
// 是否编码/符号,默认是会编码的,也就是转义的意思
boolean decodeSlash() default true;
// 默认支持URL传多值,是通过key来传输的。形如:key=value1&key=value2&key=value3
// CollectionFormat不同的取值对应不同的分隔符,一般不建议改
CollectionFormat collectionFormat() default CollectionFormat.EXPLODED;
}
在介绍使用示例之前,为了更好的看到效果,要求把Feign的日志打印出来,而Feign内置的Logger实现:
feign.Logger.JavaLogger
:使用的java.util.logging.Logger
输出,但是日志级别的FINE级别,默认不会输出到控制台feign.Logger.ErrorLogger
:错误输出。使用的System.err.printf()
输出feign.Logger.NoOpLogger
:什么都不输出,它是Feign的默认使用的Logger实现,也就是不会给控制台输出鉴于此,为了在控制台看到效果,因此本例(下同)所有的Logger实现均采用ErrorLogger
~,并且不开启重试,统一由如下工厂创建出Client实例:
public abstract class FeignClientFactory {
static <T> T create(Class<T> clazz) {
return Feign.builder()
.logger(new Logger.ErrorLogger()).logLevel(Logger.Level.FULL) // 输出日志到控制台
.retryer(Retryer.NEVER_RETRY) // 关闭重试
.decode404() // 把404也解码 -> 这样就不会以一场形式抛出,中断程序喽,方便我测试嘛
.target(clazz, "http://localhost:8080");
}
}
测试代码:
public interface RequestLineClient {
// 1、正常使用、正常书写
@Headers({"Accept:*/*", "Accept-Language: zh-cn"})
@RequestLine("GET /feign/demo1?name={name}")
String testRequestLine(@Param("name") String name);
// 2、GET后不止一个空格,有多个空格
@RequestLine("GET /feign/demo1?name={name}")
String testRequestLine2(@Param("name") String name);
// 3、使用Map一次性传递多个查询参数,使用注解为@QueryMap
@RequestLine("GET /feign/demo1")
String testRequestLine3(@QueryMap Map<String, Object> params);
// 4、方法参数上不使用任何注解
@RequestLine("GET /feign/demo1")
String testRequestLine4(String name);
// 5、方法上标注有@Body注解,然后把方法参数传递给它
@RequestLine("GET /feign/demo1")
@Body("{name}")
String testRequestLine5(@Param("name") String name);
// 6、方法两个参数,均不使用注解标注
// 启动直接报错:Method has too many Body parameters:
// @RequestLine("GET /feign/demo1")
// String testRequestLine6(String name,Integer age);
// 7、启动直接报错:Body parameters cannot be used with form parameters.
// @RequestLine("GET /feign/demo1")
// @Body("{name}")
// String testRequestLine7(@Param("name") String name, Integer age);
// 8、如果你既想要body参数,又想要查询参数,请这么写
@RequestLine("GET /feign/demo1?name={name}")
@Body("{age}")
String testRequestLine8(@Param("name") String name, @Param("age") Integer age);
}
说明:模版表达式必须用
{}
包起来才算一个表达式(变量),里面的值就是name(key),才会被@Param
匹配然后替换掉~
测试程序:
@Test
public void fun1() {
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class);
client.testRequestLine("YourBatman");
System.err.println(" ------------------ ");
client.testRequestLine2("YourBatman2");
System.err.println(" ------------------ ");
// 使用Map一次传多个请求参数
Map<String, Object> map = new HashMap<>();
map.put("name", "YourBatman3");
map.put("age", Arrays.asList(16, 18, 20));
client.testRequestLine3(map);
System.err.println(" ------------------ ");
try {
client.testRequestLine4("YourBatman4");
} catch (Exception e) {
}
System.err.println(" ------------------ ");
try {
client.testRequestLine5("YourBatman4");
} catch (Exception e) {
}
System.err.println(" ------------------ ");
try {
client.testRequestLine8("YourBatman4", 18);
} catch (Exception e) {
}
}
运行,控制台打印详细请求日志如下:
[RequestLineClient#testRequestLine] ---> GET http://localhost:8080/feign/demo1?name=YourBatman HTTP/1.1
[RequestLineClient#testRequestLine] Accept: */*
[RequestLineClient#testRequestLine] Accept-Language: zh-cn
[RequestLineClient#testRequestLine] ---> END HTTP (0-byte body)
[RequestLineClient#testRequestLine] <--- HTTP/1.1 200 (357ms)
[RequestLineClient#testRequestLine] content-length: 18
[RequestLineClient#testRequestLine] content-type: text/plain;charset=ISO-8859-1
[RequestLineClient#testRequestLine] date: Tue, 11 Feb 2020 09:53:09 GMT
[RequestLineClient#testRequestLine] vary: Access-Control-Request-Headers
[RequestLineClient#testRequestLine] vary: Access-Control-Request-Method
[RequestLineClient#testRequestLine] vary: Origin
[RequestLineClient#testRequestLine]
[RequestLineClient#testRequestLine] success:YourBatman
[RequestLineClient#testRequestLine] <--- END HTTP (18-byte body)
------------------
[RequestLineClient#testRequestLine2] ---> GET http://localhost:8080/feign/demo1?name=YourBatman2 HTTP/1.1
[RequestLineClient#testRequestLine2] ---> END HTTP (0-byte body)
[RequestLineClient#testRequestLine2] <--- HTTP/1.1 200 (20ms)
[RequestLineClient#testRequestLine2] content-length: 19
[RequestLineClient#testRequestLine2] content-type: text/plain;charset=ISO-8859-1
[RequestLineClient#testRequestLine2] date: Tue, 11 Feb 2020 09:53:09 GMT
[RequestLineClient#testRequestLine2] vary: Access-Control-Request-Headers
[RequestLineClient#testRequestLine2] vary: Access-Control-Request-Method
[RequestLineClient#testRequestLine2] vary: Origin
[RequestLineClient#testRequestLine2]
[RequestLineClient#testRequestLine2] success:YourBatman2
[RequestLineClient#testRequestLine2] <--- END HTTP (19-byte body)
------------------
[RequestLineClient#testRequestLine3] ---> GET http://localhost:8080/feign/demo1?name=YourBatman3&age=16&age=18&age=20 HTTP/1.1
[RequestLineClient#testRequestLine3] ---> END HTTP (0-byte body)
[RequestLineClient#testRequestLine3] <--- HTTP/1.1 200 (6ms)
[RequestLineClient#testRequestLine3] content-length: 19
[RequestLineClient#testRequestLine3] content-type: text/plain;charset=ISO-8859-1
[RequestLineClient#testRequestLine3] date: Tue, 11 Feb 2020 09:53:09 GMT
[RequestLineClient#testRequestLine3] vary: Access-Control-Request-Headers
[RequestLineClient#testRequestLine3] vary: Access-Control-Request-Method
[RequestLineClient#testRequestLine3] vary: Origin
[RequestLineClient#testRequestLine3]
[RequestLineClient#testRequestLine3] success:YourBatman3
[RequestLineClient#testRequestLine3] <--- END HTTP (19-byte body)
------------------
[RequestLineClient#testRequestLine4] ---> GET http://localhost:8080/feign/demo1 HTTP/1.1
[RequestLineClient#testRequestLine4] Content-Length: 11
[RequestLineClient#testRequestLine4]
[RequestLineClient#testRequestLine4] YourBatman4
[RequestLineClient#testRequestLine4] ---> END HTTP (11-byte body)
[RequestLineClient#testRequestLine4] <--- HTTP/1.1 400 (26ms)
[RequestLineClient#testRequestLine4] connection: close
[RequestLineClient#testRequestLine4] content-length: 47
[RequestLineClient#testRequestLine4] content-type: text/plain;charset=ISO-8859-1
[RequestLineClient#testRequestLine4] date: Tue, 11 Feb 2020 09:53:09 GMT
[RequestLineClient#testRequestLine4]
[RequestLineClient#testRequestLine4] hello error:Request method 'POST' not supported
[RequestLineClient#testRequestLine4] <--- END HTTP (47-byte body)
------------------
[RequestLineClient#testRequestLine5] ---> GET http://localhost:8080/feign/demo1 HTTP/1.1
[RequestLineClient#testRequestLine5] Content-Length: 11
[RequestLineClient#testRequestLine5]
[RequestLineClient#testRequestLine5] YourBatman4
[RequestLineClient#testRequestLine5] ---> END HTTP (11-byte body)
[RequestLineClient#testRequestLine5] <--- HTTP/1.1 400 (77ms)
[RequestLineClient#testRequestLine5] connection: close
[RequestLineClient#testRequestLine5] content-length: 47
[RequestLineClient#testRequestLine5] content-type: text/plain;charset=ISO-8859-1
[RequestLineClient#testRequestLine5] date: Tue, 11 Feb 2020 09:53:09 GMT
[RequestLineClient#testRequestLine5]
[RequestLineClient#testRequestLine5] hello error:Request method 'POST' not supported
[RequestLineClient#testRequestLine5] <--- END HTTP (47-byte body)
------------------
[RequestLineClient#testRequestLine8] ---> GET http://localhost:8080/feign/demo1?name=YourBatman4 HTTP/1.1
[RequestLineClient#testRequestLine8] Content-Length: 2
[RequestLineClient#testRequestLine8]
[RequestLineClient#testRequestLine8] 18
[RequestLineClient#testRequestLine8] ---> END HTTP (2-byte body)
[RequestLineClient#testRequestLine8] <--- HTTP/1.1 400 (7ms)
[RequestLineClient#testRequestLine8] connection: close
[RequestLineClient#testRequestLine8] content-length: 47
[RequestLineClient#testRequestLine8] content-type: text/plain;charset=ISO-8859-1
[RequestLineClient#testRequestLine8] date: Tue, 11 Feb 2020 09:53:09 GMT
[RequestLineClient#testRequestLine8]
[RequestLineClient#testRequestLine8] hello error:Request method 'POST' not supported
[RequestLineClient#testRequestLine8] <--- END HTTP (47-byte body)
从日志中也能看出一点小细节:
@RequestLine
注解的首个单词必须是HTTP方法,且必须顶格写(前面不允许有空格),但后面是需要有空格的且可以是多个空格@Headers
它的key连接符用的是:
而不是=
,请务必注意。另外:对空格不敏感HttpURLConnection
决定的,如果你换成OkHttp将不会是这样@Param
注解,最终会被放进请求Body体里 feign.Request.Options
类型除外,他俩不用注解我摘抄出来一条日志进行详细解释,因为是首次所以比较详细,下面就会一带而过了。
只能标注在方法参数Parameter上。 通过名称定义模板变量,其值将用于填入上面的模版:@Headers/@RequestLine/@Body
均可使用模版表达式。
@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 {
String expand(Object value);
}
final class ToStringExpander implements Expander {
@Override
public String expand(Object value) {
return value.toString();
}
}
}
说明:测试此注解的作用,仅需要看看日志的发送即可,并不要求能ping通哦~。
public interface ParamClient {
// 1、参数为数组类型
@RequestLine("GET /feign/demo2?name={name}")
String testParam(@Param("name") String[] names);
// 2、参数为List类型
@RequestLine("GET /feign/demo2?name={name}")
String testParam2(@Param("name") Collection<String> names);
// 3、参数值包含特殊字符:? / 这种
@RequestLine("GET /feign/demo2?name={name}")
String testParam3(@Param("name") String name);
}
@Test
public void fun2() {
ParamClient client = FeignClientFactory.create(ParamClient.class);
client.testParam(new String[]{"YourBatman", "fsx"});
System.err.println(" ------------------ ");
client.testParam2(Arrays.asList("1", "2", "3"));
System.err.println(" ------------------ ");
client.testParam3("/?YourBatman/");
System.err.println(" ------------------ ");
}
运行程序打印情况:
GET http://localhost:8080/feign/demo2?name=%5BLjava.lang.String&name=@19bb089b HTTP/1.1
...
GET http://localhost:8080/feign/demo2?name=1&name=2&name=3 HTTP/1.1
...
GET http://localhost:8080/feign/demo2?name=%2F%3FYourBatman%2F HTTP/1.1
可以看到,如果是Collection
类型是能够很好的被解析成多值的,但是数组不行,因此多用集合少用数组哦(数组直接调用toString()方法了)。
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Headers {
String[] value();
}
能标注在类上和方法上。用于传请求头,使用起来比较简单,形如这样即可:
@Headers({"Accept:*/*", "Accept-Language:zh-cn"})
唯一注意的一点:k-v使用的是:
链接,而不是=
。
@Retention(RUNTIME)
@java.lang.annotation.Target(PARAMETER)
public @interface QueryMap {
boolean encoded() default false;
}
只能标注在方法参数上。用于传递多个查询值,拼接在URL后面,上面已经给出示例了,本处略。 仅需注意一点:只能标注在Map类型的参数前面,否则报错。
@Retention(RUNTIME)
@java.lang.annotation.Target(PARAMETER)
public @interface HeaderMap {
}
同上,只是用在Header上而已
它只能标注在方法上。
@Target(METHOD)
@Retention(RUNTIME)
public @interface Body {
String value();
}
比如:@Body("{body}")
,这样就可以通过方法参数的@Param("body") String body
传值喽。注意:这个值最终是以http body体的形式发送的(并非URL参数哦),body体的内容并不要求必须是json,一般请配合请求头使用。
准备一个POJO:
@Getter
@Setter
@ToString
public class Person {
private String name = "YourBatman";
private Integer age = 18;
}
public interface BodyClient {
// 1、@Body里可以是写死的字符串
@Body("{\"name\" : \"YourBatman\"}")
@RequestLine("POST /feign/demo3")
String testBody();
// 2、@Body可以使用模版{} 取值
@Body("{body}")
@RequestLine("POST /feign/demo3")
String testBody2(@Param("body") String name);
// 3、@Body里取值来自于一个JavaBean
@Body("{person}")
@RequestLine("POST /feign/demo3")
String testBody3(@Param("person") Person person);
}
测试程序:
@Test
public void fun3() {
BodyClient client = FeignClientFactory.create(BodyClient.class);
client.testBody();
System.err.println(" ------------------ ");
client.testBody2("my name is YourBatman");
System.err.println(" ------------------ ");
client.testBody3(new Person());
}
运行,控制台打印如下(只摘抄主要信息):
[BodyClient#testBody] ---> POST http://localhost:8080/feign/demo3 HTTP/1.1
[BodyClient#testBody] Content-Length: 23
[BodyClient#testBody]
[BodyClient#testBody] {"name" : "YourBatman"}
[BodyClient#testBody] ---> END HTTP (23-byte body)
...
------------------
[BodyClient#testBody2] ---> POST http://localhost:8080/feign/demo3 HTTP/1.1
[BodyClient#testBody2] Content-Length: 21
[BodyClient#testBody2]
[BodyClient#testBody2] my name is YourBatman
[BodyClient#testBody2] ---> END HTTP (21-byte body)
------------------
[BodyClient#testBody3] ---> POST http://localhost:8080/feign/demo3 HTTP/1.1
[BodyClient#testBody3] Content-Length: 31
[BodyClient#testBody3]
[BodyClient#testBody3] Person(name=YourBatman, age=18)
[BodyClient#testBody3] ---> END HTTP (31-byte body)
...
可以看到body里是可以是任意格式的数据的,包括POJO(只不过默认是调用它的toString方法而已~)。
说明:Feign默认情况下只能支持文本消息,但后来feign提供了
feign-form
这个扩展模块,所以也就能够支持二进制、文件上传喽。 需要说明的是:feign-form并不属于官方直接子模块,是后续新增的所以它的大版本号不跟主版本号走,GAV也有所不同:
// feign-form和feign-form-spring共用一个父工程,版本号保持一致
// feign-form-spring依赖于feign-form工程
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<!-- <artifactId>feign-form-spring</artifactId> -->
<version>3.8.0</version>
</dependency>
Spring Cloud的Feign除了导入了core包,也导入了feign-form-spring(包含feign-form),所以默认也是支持到了二进制数据传递的
关于POJO那个Person对象为何最终调用的是toString()而非序列化成了一个JSON,这和RequestTemplate
的构建有关。以及为何在Spring Cloud下是能成为JSON的,这些原因后文会分解。。。
关于原生Feign的原生注解就讲解到这了,还是蛮有意思的。总体来说这些原生注解使用起来并不难,它的语法规范遵循的是RFC6570
规范,这是区别于Spring MVC的(它是Ant规范)。
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有