二、原生 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方法上。为请求定义HttpMethodUriTemplate(标注在方法上的就是一个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规范)。

posted @ 2023-08-20 18:53  YangDanMua  阅读(116)  评论(0编辑  收藏  举报