Http客户端框架Forest项目解读

1,035 阅读6分钟

项目介绍

Forest是一个高层的、极简的声明式HTTP调用API框架。相比于直接使用Httpclient您不再用写一大堆重复的代码了,而是像调用本地方法一样去发送HTTP请求。

Maven依赖

<dependency>
    <groupId>com.dtflys.forest</groupId>
    <artifactId>forest-spring-boot-starter</artifactId>
    <version>1.5.19</version>
</dependency>

快速上手

简单请求

public interface MyClient {

    @Request("http://localhost:8080/hello")
    String simpleRequest();

}

通过@Request注解,将上面的MyClient接口中的simpleRequest()方法绑定了一个 HTTP 请求, 其 URL 为http://localhost:8080/hello ,并默认使用GET方式,且将请求响应的数据以String的方式返回给调用者。

稍微复杂点的请求,需要在请求头设置信息

public interface MyClient {

    @Request(
            url = "http://localhost:8080/hello/user",
            headers = "Accept: text/plain"
    )
    String sendRequest(@Query("uname") String username);
}

上面的sendRequest方法绑定的 HTTP 请求,定义了 URL 信息,以及把Accept:text/plain加到了请求头中, 方法的参数String username绑定了注解@Query("uname"),它的作用是将调用者传入入参 username 时,自动将username的值加入到 HTTP 的请求参数uname中。

这段实际产生的HTTP请求如下:

GET http://localhost:8080/hello/user?uname=foo
HEADER:
    Accept: text/plain

请求方法,假设发起post请求,有3种写法:

public interface MyClient {

    /**
     * 使用 @Post 注解,可以去掉 type = "POST" 这行属性
     */
    @Post("http://localhost:8080/hello")
    String simplePost1();


    /**
     * 通过 @Request 注解的 type 参数指定 HTTP 请求的方式。
     */
    @Request(
            url = "http://localhost:8080/hello",
            type = "POST"
    )
    String simplePost2();

    /**
     * 使用 @PostRequest 注解,和上面效果等价
     */
    @PostRequest("http://localhost:8080/hello")
    String simplePost3();

}

可以用@GetRequest@PostRequest等注解代替@Request注解,这样就可以省去写type属性的麻烦了。

请求体

POSTPUT等请求方法中,通常使用 HTTP 请求体进行传输数据。在 Forest 中有多种方式设置请求体数据。

表单格式

上面使用 @Body 注解的例子用的是普通的表单格式,也就是contentType属性为application/x-www-form-urlencoded的格式,即contentType不做配置时的默认值。

表单格式的请求体以字符串 key1=value1&key2=value2&...&key{n}=value{n} 的形式进行传输数据,其中value都是已经过 URL Encode 编码过的字符串。


/**
 * contentType属性设置为 application/x-www-form-urlencoded 即为表单格式,
 * 当然不设置的时候默认值也为 application/x-www-form-urlencoded, 也同样是表单格式。
 * 在 @Body 注解的 value 属性中设置的名称为表单项的 key 名,
 * 而注解所修饰的参数值即为表单项的值,它可以为任何类型,不过最终都会转换为字符串进行传输。
 */
@Post(
    url = "http://localhost:8080/user",
    contentType = "application/x-www-form-urlencoded",
    headers = {"Accept:text/plain"}
)
String sendPost(@Body("key1") String value1,  @Body("key2") Integer value2, @Body("key3") Long value3);

调用后产生的结果可能如下:

POST http://localhost:8080/hello/user
HEADER:
    Content-Type: application/x-www-form-urlencoded
BODY:
    key1=xxx&key2=1000&key3=9999

@Body注解修饰的参数为一个对象,并注解的value属性不设置任何名称的时候,会将注解所修饰参数值对象视为一整个表单,其对象中的所有属性将按 属性名1=属性值1&属性名2=属性值2&...&属性名{n}=属性值{n} 的形式通过请求体进行传输数据。

/**
 * contentType 属性不设置默认为 application/x-www-form-urlencoded
 * 要以对象作为表达传输项时,其 @Body 注解的 value 名称不能设置
 */
@Post(
    url = "http://localhost:8080/hello/user",
    headers = {"Accept:text/plain"}
)
String send(@Body User user);

调用产生的结果如下:

POST http://localhost:8080/hello/user
HEADER:
    Content-Type: application/x-www-form-urlencoded
BODY:
    username=foo&password=bar

JSON格式

@JSONBody注解修饰对象

@JSONBody = @Body+contentType的格式,除了@JSONBody注解,使用@Body注解也可以,只要将contentType属性或Content-Type请求头指定为application/json便可。

发送JSON非常简单,只要用@JSONBody注解修饰相关参数就可以了,该注解自1.5.0-RC1版本起可以使用。 使用@JSONBody注解的同时就可以省略 contentType = "application/json"属性设置

/**
 * 被@JSONBody注解修饰的参数会根据其类型被自定解析为JSON字符串
 * 使用@JSONBody注解时可以省略 contentType = "application/json"属性设置
 */
@Post("http://localhost:8080/hello/user")
String helloUser(@JSONBody User user);

调用后产生的结果如下:

POST http://localhost:8080/hello/user
HEADER:
    Content-Type: application/json
BODY:
    {"username": "foo", "password": "bar"}

切记使用@JSONBody绑定对象入参的时候,JSONBody的value一定要空着,比如,@JSONBody User user写法:

/**
 * 被@JSONBody注解修饰的Map类型参数会被自定解析为JSON字符串
 */
@Post(url = "http://localhost:8080/hello/user")
String helloUser1(@JSONBody User user);

当@JSONBody修饰Map的时候:

/**
 * 被@JSONBody注解修饰的Map类型参数会被自定解析为JSON字符串
 */
@Post(url = "http://localhost:8080/hello/user")
String helloUser2(@JSONBody Map<String, Object> userMap);

//若调用代码是这样的:
Map<String, Object> map = new HashMap<>();
map.put("username", "foo");
map.put("password", "bar");
client.helloUser(map);

//会产生的结果:
POST http://localhost:8080/hello/user
HEADER:
    Content-Type: application/json
BODY:
    {"username": "foo", "password": "bar"}

详细forest请求体说明可以参考官方文档:forest.dtflyx.com/docs/basic/…

注解BaseRequest

@BaseRequest注解定义在接口类上,在@BaseRequest上定义的属性会被分配到该接口中每一个方法上,但方法上定义的请求属性会覆盖@BaseRequest上重复定义的内容。 因此可以认为@BaseRequest上定义的属性内容是所在接口中所有请求的默认属性。

/**
 * @BaseRequest 为配置接口层级请求信息的注解
 * 其属性会成为该接口下所有请求的默认属性
 * 但可以被方法上定义的属性所覆盖
 */
@BaseRequest(
    baseURL = "http://localhost:8080",     // 默认域名
    headers = {
        "Accept:text/plain"                // 默认请求头
    },
    sslProtocol = "TLS"                    // 默认单向SSL协议
)
public interface MyClient {
  
    // 方法的URL不必再写域名部分
    @Get("/hello/user")
    String send1(@Query("username") String username);

    // 若方法的URL是完整包含http://开头的,那么会以方法的URL中域名为准,不会被接口层级中的baseURL属性覆盖,这个确实是非常方便了~
    @Get("http://www.xxx.com/hello/user")
    String send2(@Query("username") String username);
  
    @Get(
        url = "/hello/user",
        headers = {
            "Accept:application/json"      // 覆盖接口层级配置的请求头信息
        }
    )     
    String send3(@Query("username") String username);

}

forest异步请求

在Forest使用异步请求,可以通过设置@Request注解的async属性为true实现,不设置或设置为false即为同步请求

/**
 * async 属性为 true 即为异步请求,为 false 则为同步请求
 * 不设置该属性时,默认为 false
 */
@Post(
        url = "http://localhost:8080/user/updateUserTagById",
        async = true
)
boolean asyncUpdate(String userId);