二、原生 API 的注解使用示例
RequestLine
@java.lang.annotation.Target(METHOD)
@Retention(RUNTIME)
public @interface RequestLine {
// 请求方式 + uri
String value();
// 斜线是否 base64 编码
boolean decodeSlash() default true;
// 默认支持URL传多值,是通过key来传输的。形如:key=value1&key=value2&key=value3
// CollectionFormat不同的取值对应不同的分隔符,一般不建议改
CollectionFormat collectionFormat() default CollectionFormat.EXPLODED;
}
它只能标注在Method方法
上。为请求定义HttpMethod
和UriTemplate
(标注在方法上的就是一个HttpMethod,且写好了URI(可是绝对路径,也可是相对的,一般写后部分即可))。表达式、用大括号括起来的值{expression}最终会用对应的@Param注解填进去(根据key匹配)。
使用示例
Feign内置的Logger实现:
- feign.Logger.JavaLogger:使用的java.util.logging.Logger输出,但是日志级别的FINE级别,默认不会输出到控制台
- feign.Logger.ErrorLogger:错误输出。使用的System.err.printf()输出
- feign.Logger.NoOpLogger:什么都不输出,它是Feign的默认使用的Logger实现,也就是不会给控制台输出
abstract class FeignClientFactory {
static <T> T create(Class<T> clazz, int port) {
return Feign.builder()
// 日志
.logger(new Logger.ErrorLogger())
.logLevel(Logger.Level.FULL)
// 不重试
.retryer(Retryer.NEVER_RETRY)
// 把404也解码 -> 这样就不会以异常形式抛出
.decode404()
.target(clazz, "http://localhost:" + port);
}
}
Feign 接口
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);
}
这是在 Feign 的源码的测试的包内写的,模仿 feign 的单元测试的写法
import feign.*;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Rule;
import org.junit.Test;
import java.util.Map;
public class Demo {
@Rule
public final MockWebServer server = new MockWebServer();
// 1、正常使用、正常书写
@Test
public void testRequestLine() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
client.testRequestLine("YourBatman");
}
日志
Aug 20, 2023 5:06:31 PM okhttp3.mockwebserver.MockWebServer$3 execute
INFO: MockWebServer[56615] starting to accept connections
[RequestLineClient#testRequestLine] ---> GET http://localhost:56615/feign/demo1?name=YourBatman HTTP/1.1
[RequestLineClient#testRequestLine] Accept: */*
[RequestLineClient#testRequestLine] Accept-Language: zh-cn
[RequestLineClient#testRequestLine] ---> END HTTP (0-byte body)
Aug 20, 2023 5:06:31 PM okhttp3.mockwebserver.MockWebServer$4 processOneRequest
INFO: MockWebServer[56615] received request: GET /feign/demo1?name=YourBatman HTTP/1.1 and responded: HTTP/1.1 200 OK
[RequestLineClient#testRequestLine] <--- HTTP/1.1 200 OK (26ms)
[RequestLineClient#testRequestLine] content-length: 13
[RequestLineClient#testRequestLine]
[RequestLineClient#testRequestLine] response data
[RequestLineClient#testRequestLine] <--- END HTTP (13-byte body)
Aug 20, 2023 5:06:31 PM okhttp3.mockwebserver.MockWebServer$3 acceptConnections
INFO: MockWebServer[56615] done accepting connections: socket closed
其他测试方法
public class Demo {
@Rule
public final MockWebServer server = new MockWebServer();
// 1、正常使用、正常书写
@Test
public void testRequestLine() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
client.testRequestLine("YourBatman");
}
// 2、GET后不止一个空格,有多个空格
@Test
public void testRequestLine2() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
client.testRequestLine2("YourBatman");
}
// 3、使用Map一次性传递多个查询参数,使用注解为@QueryMap
@Test
public void testRequestLine3() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
// 使用Map一次传多个请求参数
Map<String, Object> map = new HashMap<>();
map.put("name", "YourBatman3");
map.put("age", Arrays.asList(16, 18, 20));
client.testRequestLine3(map);
}
// 4、方法参数上不使用任何注解
@Test
public void testRequestLine4() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
client.testRequestLine4("YourBatman");
}
// 5、一个方法参数使用注解但是不使用
// class java.util.LinkedHashMap is not a type supported by this encoder.
@Test
public void testRequestLine5() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
// client.testRequestLine5("YourBatman");
}
// 6、方法上标注有@Body注解,然后把方法参数传递给它
@Test
public void testRequestLine6() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
client.testRequestLine6("YourBatman");
}
// 7、方法两个参数,均不使用注解标注
// 启动直接报错(FeignClientFactory.create时报错):Method has too many Body parameters:
// @RequestLine("GET /feign/demo1")
@Test
public void testRequestLine7(){
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
// client.testRequestLine7("YourBatman", 18);
}
// 8、启动直接报错:Body parameters cannot be used with form parameters.
// @RequestLine("GET /feign/demo1")
// @Body("{name}")
public void testRequestLine8(){
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
// client.testRequestLine8("YourBatman", 18);
}
// 9、如果你既想要body参数,又想要查询参数,请这么写
@Test
public void testRequestLine9() {
server.enqueue(new MockResponse().setBody("response data"));
RequestLineClient client = FeignClientFactory.create(RequestLineClient.class, server.getPort());
client.testRequestLine9("YourBatman", 18);
}
}
从日志中也能看出一点小细节
- @RequestLine注解的首个单词必须是HTTP方法,且必须顶格写(前面不允许有空格),但后面是需要有空格的且可以是多个空格
- @Headers它的key连接符用的是:而不是=,请务必注意。另外:对空格不敏感
- 但凡只要body体不为空,最终就会以POST请求的形式发出,这是由默认实现:JDK底层实现HttpURLConnection决定的,如果你换成OkHttp将不会是这样
- 方法参数若没标注@Param注解,最终会被放进请求Body体里
- 方法参数中URI、feign.Request.Options类型除外,他俩不用注解
- 若你@Body既想用模版,@RequestLine里也想用模版,那么请务必保证每个方法参数都有@Param注解
@Param
只能标注在方法参数Parameter上。 通过名称定义模板变量,其值将用于填入上面的模版:@Headers/@RequestLine/@Body均可使用模版表达式
@Retention(RUNTIME)
@java.lang.annotation.Target({PARAMETER, FIELD, METHOD})
public @interface Param {
/**
* 名称(key),和模版会进行匹配然后填充 必填项
*/
String value();
/**
* 如何把值填充上去,默认是调用其toString方法直接填上去
*/
Class<? extends Expander> expander() default ToStringExpander.class;
/**
* 是否转义,默认不转义,直接放上去
*/
boolean encoded() default false;
/**
* 转 String, 类似于 toString 的东西
*/
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();
}
}
}
如果是Collection类型是能够很好的被解析成多值的,但是数组不行,因此多用集合少用数组哦(数组直接调用toString()方法了)
@Headers
@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Headers {
String[] value();
}
能标注在类上和方法上。用于传请求头,使用起来比较简单,形如这样即可:
@Headers({"Accept:*/*", "Accept-Language:zh-cn"})
唯一注意的一点:k-v使用的是冒号连接,而不是等号。
@QueryMap
@SuppressWarnings("deprecation")
@Retention(RUNTIME)
@java.lang.annotation.Target(PARAMETER)
public @interface QueryMap {
boolean encoded() default false;
}
只能标注在方法参数上。用于传递多个查询值,拼接在URL后面。
仅需注意一点:只能标注在Map类型的参数前面,否则报错。
@HeaderMap
@Retention(RUNTIME)
@java.lang.annotation.Target(PARAMETER)
public @interface HeaderMap {
}
类似 QueryMap,不过 QueryMap 是解析 Map 为表单参数,HeaderMap 解析 Map 为请求头
@Body
@Target(METHOD)
@Retention(RUNTIME)
public @interface Body {
String value();
}
比如:@Body("{body}"),这样就可以通过方法参数的@Param("body") String body传值。注意:这个值最终是以http body体的形式发送的(并非URL参数),body体的内容并不要求必须是json,一般请配合请求头使用。
@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);
}
可以看到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规范)。