[享学Feign] 二、原生Feign的注解介绍及使用示例

人无远虑,必有近忧。

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/feign-learning

前言

通过第一篇文章了解了Feign的最基础知识,作为一个有态度的程序员,我们势必要搞清楚它整个执行的脉络,了解它的执行过程和原理才算结束,这是最后最后最后进行个性化定制的基础,一切都为了“玩”嘛。

本文将讲解它原生支持的注解,毕竟Feign并不强依赖于Spring MVC,在Java环境也是可以作为HC来使用的,了解起来不难,建议你掌握。

说明:本文依旧仅仅只导入核心包feign-core包,因为所有的注解支持均在它的核心包内。


正文

本文内容依旧站在使用的角度看Feign,并且会结合具体的使用示例来辅以说明,相信不会让人觉得枯燥。

由于大多数人了解Feign、使用Feign都是出于Spring Cloud,平时使用的均是Spring MVC的注解。所以未免对Feign的原生注解了解并不多,本文就来帮你扫盲,让你在实际使用过程中能更加的得心应手。
源生注解并不多,此处逐个介绍:


@RequestLine

它只能标注在Method方法上。为请求定义HttpMethodUriTemplate(标注在方法上的就是一个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连接符用的是:而不是=,请务必注意。另外:对空格不敏感
  • 但凡只要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)
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()方法了)。


@Headers

@Target({METHOD, TYPE})
@Retention(RUNTIME)
public @interface Headers {
  String[] value();
}

能标注在类上和方法上。用于传请求头,使用起来比较简单,形如这样即可:

@Headers({"Accept:*/*", "Accept-Language:zh-cn"})

唯一注意的一点:k-v使用的是:链接,而不是=


@QueryMap

@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 {
}

同上,只是用在Header上而已


@Body

它只能标注在方法上

@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规范)。
分隔线

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可左边扫码加入我的 Java高工、架构师 系列群大家庭学习和交流。
往期精选

发布了288 篇原创文章 · 获赞 451 · 访问量 38万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/104241601