Forest轻量级框架的声明式使用

发布时间 2023-05-26 16:56:03作者: 不会上猪的树

1.Forest框架声明式接口的基础使用


1.1构建接口

​ 在 Forest 中,所有的 HTTP 请求信息都要绑定到某一个接口的方法上,不需要编写具体的代码去发送请求。请求发送方通过调用事先定义好 HTTP 请求信息的接口方法,自动去执行 HTTP 发送请求的过程,其具体发送请求信息就是该方法对应绑定的 HTTP 请求信息

1.12简单请求

public interface MyClient {

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

}

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

1.13带有@Query的请求

public interface MyClient {

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

实际的url地址为:http://localhost:8080/hello/user?uname=username

如果调用方法:

@Resource
MyClient myClient;

myClient.sendRequest("foo");

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

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

1.2请求方法

常用的GET和POST请求

@GET或者@GetRequest

@Get("http://localhost:8080/hello")
String simpleGet1();

@GetRequest("http://localhost:8080/hello")
String simpleGet2();

@POST或者PostRequest

@Post("http://localhost:8080/hello")
String simplePost1();

@PostRequest("http://localhost:8080/hello")
String simplePost2();

其他的请求方式暂时未作了解,可以去了解http请求方式


1.2.1动态请求方式@Request

@Request

/**
 * 通过在 @Request 注解的 type 属性中定义字符串模板
 * 在字符串模板中引用方法的参数
 */
@Request(
    url = "http://localhost:8080/hello",
    type = "{type}"
)
String simpleRequest(@Var("type") String type);

关于@Var注解后续了解


1.3请求地址

HTTP请求可以没有请求头、请求体,但一定会有请求地址,即URL,以及很多请求的参数都是直接绑定在URLQuery部分上

@Var 注解修饰的参数从外部动态传入URL

/**
 * 整个完整的URL都通过参数传入
 * {0}代表引用第一个参数,{1}代表引用的第二个参数,这里自身写的方法,没有第二个参数,也就意味着{0}对应的就是String myURL的值
 */
@Get("{0}")
String send1(String myURL);

/**
 * 整个完整的URL都通过 @Var 注解修饰的参数动态传入
 */
@Get("{myURL}")
String send2(@Var("myURL") String myURL);

/**
 * 通过参数转入的值作为URL的一部分
 */
@Get("http://{myURL}/abc")
String send3(@Var("myURL") String myURL);

/**
 * 参数转入的值可以作为URL的任意一部分
 */
@Get("http://localhost:8080/test/{myURL}?a=1&b=2")
String send4(@Var("myURL") String myURL);

1.31@Address 注解

Forest 从1.5.3版本开始提供了 @Address 注解,帮助您将URL的地址部分提取出来,方便管理

// 通过 @Address 注解绑定根地址
// host 绑定到第一个参数, port 绑定到第二个参数
@Post("/data")
@Address(host = "{0}", port = "{1}")
ForestRequest<String> sendHostPort(String host, int port);

// 若调用 sendHostPort("192.168.0.2", 8080);
// 则最终产生URL:
// http://192.168.0.2:8080/data

也可也将@Address注解放到接口上,这样将会直接定义整个接口的根url,可以自行了解

如果想通过动态的获取根url的地址,可以用一个类去实现AddressSource接口

// 实现 AddressSource 接口
public class MyAddressSource implements AddressSource {

    @Override
    public ForestAddress getAddress(ForestRequest request) {
        // 定义 3 个 IP 地址
        String[] ipArray = new String[] {
                "192.168.0.1",
                "192.168.0.2",
                "192.168.0.3",
        };
        // 随机选出其中一个
        Random random = new Random();
        int i = random.nextInt(3);
        String ip = ipArray[i];
        // 返回 Forest 地址对象
        return new ForestAddress(ip, 80);
    }
}

绑定自定义的AddressSource接口实现类

// 也是通过 @Address 注解来绑定动态地址来源
// 每次调用该方法,都可能是不同的根地址
@Post("/data")
@Address(source = MyAddressSource.class)
ForestRequest<String> sendData();

1.4URL参数

URL参数,也称为 URL 查询字符串,即跟在 URL 地址中?后面的那串字符串,可以用=表示一对键值对,多个键值对用&隔开,其可以作为 HTTP 请求的参数

  • Forest给URLQuery部分传参也有多种方式,其中最简洁直白的就数字符串拼接了。

1.41@Var

/**
 * 直接在url字符串的问号后面部分直接写上 参数名=参数值 的形式
 * 等号后面的参数值部分可以用 {参数序号} 这种字符串模板的形式替代
 * 在发送请求时会动态拼接成一个完整的URL
 * 使用这种形式不需要为参数定义额外的注解
 * 
 * 注:参数序号是从 0 开始记的方法参数的序号
 * 0 代表第一个参数,1 代表第二个参数,以此类推
 */
@Get("http://localhost:8080/abc?a={0}&b={1}&id=0")
String send1(String a, String b);

/**
 * 直接在url字符串的问号后面部分直接写上 参数名=参数值 的形式
 * 等号后面的参数值部分可以用 {变量名} 这种字符串模板的形式替代
 * 在发送请求时会动态拼接成一个完整的URL
 * 使用这种方式需要通过 @Var 注解或全局配置声明变量
 * @Var注解一般用来做url后已知参数的名称和对应的value值,这个value值就是参数对应的值
 */
@Get("http://localhost:8080/abc?a={a}&b={b}&id=0")
String send2(@Var("a") String a, @Var("b") String b);


/**
 * 如果一个一个变量包含多个Query参数,比如: "a=1&b=2&c=3"
 * 为变量 parameters 的字符串值
 * 就用 ${变量名} 这种字符串模板格式
 * 使用这种方式需要通过 @Var 注解或全局配置声明变量
 * 这种不推荐使用,官方文档有详细说明,就不在此赘述了
 */
@Get("http://localhost:8080/abc?${parameters}")
String send3(@Var("parameters") String parameters);

在已知参数名称的情况下显然用@Var而不是@Query,@Var可以用于请求参数,请求头,请求体中,请求路径也是可以的

1.42@Query:


/**
 * 使用 @Query 注解,可以直接将该注解修饰的参数动态绑定到请求url中
 * 注解的 value 值即代表它在url的Query部分的参数名
 */
@Get("http://localhost:8080/abc?id=0")
String send(@Query("a") String a, @Query("b") String b);


url:http://localhost:8080/abc?id=0&a="a"&b="b"

若是要传的URL参数多


/**
 * 使用 @Query 注解,可以修饰 Map 类型的参数
 * 很自然的,Map 的 Key 将作为 URL 的参数名, Value 将作为 URL 的参数值
 * 这时候 @Query 注解不定义名称
 */
@Get("http://localhost:8080/abc?id=0")
String send1(@Query Map<String, Object> map);


/**
 * @Query 注解也可以修饰自定义类型的对象参数
 * 依据对象类的 Getter 和 Setter 的规则取出属性
 * 其属性名为 URL 参数名,属性值为 URL 参数值
 * 这时候 @Query 注解不定义名称
 */
@Get("http://localhost:8080/abc?id=0")
String send2(@Query UserInfo user);


友情提示

@Query 注解修饰的参数一定会出现在 URL 中。

send1 传入的key-value对应的是{name="zhangsan",password="123456"}
则对应的url是:http://localhost:8080/abc?id=0&name="zhangsan"&password="123456"

数组传参就不过多赘述了,也是类似字符串拼接一样放在参数的后面

1.43@JSONQuery

@JSONQuery做参数传递

@Get("http://localhost:8080/abc")
String send(@JSONQuery("id") List idList);

若调用 send(Arrays.asList(1, 2, 3, 4))

则产生的最终URL为

http://localhost:8080/abc?id=[1, 2, 3, 4]

1.5请求头

在这里先了解一下HTTP消息结构为好

1.51客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

img

列举一下常见的文本格式:

  • 常见的媒体格式类型如下:

    • text/html : HTML格式
    • text/plain :纯文本格式
    • text/xml : XML格式
    • image/gif :gif图片格式
    • image/jpeg :jpg图片格式
    • image/png:png图片格式

    以application开头的媒体格式类型:

    • application/xhtml+xml :XHTML格式
    • application/xml: XML数据格式
    • application/atom+xml :Atom XML聚合格式
    • application/json: JSON数据格式
    • application/pdf:pdf格式
    • application/msword : Word文档格式
    • application/octet-stream : 二进制流数据(如常见的文件下载)
    • application/x-www-form-urlencoded :
      中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)

    另外一种常见的媒体格式是上传文件之时使用的:

    • multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式

1.52headers属性

public interface MyClient {

    @Request(
            url = "http://localhost:8080/hello/user",
            headers = {
                "Accept-Charset: utf-8",
                "Content-Type: text/plain"
            }
    )
    String multipleHeaders();
}

该接口调用后所实际产生的 HTTP 请求如下:

GET http://localhost:8080/hello/user
HEADER:
    Accept-Charset: utf-8
    Content-Type: text/plain

也可以用@Var注解绑定请求头中的内容,例如:

public interface MyClient {

    @Request(
            url = "http://localhost:8080/hello/user",
            headers = {
                "Accept-Charset: ${encoding}",
                "Content-Type: text/plain"
            }
    )
    String bindingHeader(@Var("encoding") String encoding);
}

实际的HTTP请求响应为:

GET http://localhost:8080/hello/user
HEADER:
    Accept-Charset: gbk
    Content-Type: text/plain

1.53@Header注解

@Header 注解来帮助您把方法的参数直接绑定到请求体中。


/**
 * 使用 @Header 注解将参数绑定到请求头上
 * @Header 注解的 value 指为请求头的名称,参数值为请求头的值
 * @Header("Accept") String accept将字符串类型参数绑定到请求头 Accept 上
 * @Header("accessToken") String accessToken将字符串类型参数绑定到请求头 accessToken 上
 */
@Post("http://localhost:8080/hello/user?username=foo")
void postUser(@Header("Accept") String accept, @Header("accessToken") String accessToken);

实际的HTTP请求响应为:

GET http://localhost:8080/hello/user?username=foo
HEADER:
    Accept: accept的具体内容
    accessToken: accessToken的具体内容

如果有很多很多的请求头,就用Map

/**
 * 使用 @Header 注解可以修饰 Map 类型的参数
 * Map 的 Key 指为请求头的名称,Value 为请求头的值
 * 通过此方式,可以将 Map 中所有的键值对批量地绑定到请求头中
 */
@Post("http://localhost:8080/hello/user?username=foo")
void headHelloUser(@Header Map<String, Object> headerMap);


/**
 * 使用 @Header 注解可以修饰自定义类型的对象参数
 * 依据对象类的 Getter 和 Setter 的规则取出属性
 * 其属性名为 URL 请求头的名称,属性值为请求头的值
 * 以此方式,将一个对象中的所有属性批量地绑定到请求头中
 */
@Post("http://localhost:8080/hello/user?username=foo")
void headHelloUser(@Header MyHeaderInfo headersInfo);

注意

  • (1) 需要单个单个定义请求头的时候,@Header注解的value值一定要有,比如 @Header("Content-Type") String contentType
  • (2) 需要绑定对象的时候,@Header注解的value值一定要空着,比如 @Header MyHeaders headers@Header Map headerMap

1.6请求体

1.61@Body

您可以使用@Body注解修饰参数的方式,将传入参数的数据绑定到 HTTP 请求体中。

@Body注解修饰的参数一定会绑定到请求体中,不用担心它会出现在其他地方

/**
 * 默认body格式为 application/x-www-form-urlencoded,即以表单形式序列化数据
 */
@Post("http://localhost:8080/user")
String sendPost(@Body("username") String username,  @Body("password") String password);

1.61表单格式

上面使用 @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"
)
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("http://localhost:8080/hello/user")
String send(@Body User user);

其实也就是将对象的属性和属性值作为键值队用来作为请求体的名和值

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

1.63@JSONBody

发送JSON非常简单,只要用@JSONBody注解修饰相关参数就可以了,该注解自1.5.0-RC1版本起可以使用。 使用@JSONBody注解的同时就可以省略 contentType = "application/json"属性设置,可以和@JSONQuery做对比和认知,就能发现设计的很人性化

/**
 * 被@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 注解必须填上 value 属性或 name 属性的值,作为JSON的字段名称
 */
@Post("http://localhost:8080/hello/user")
String helloUser(@JSONBody("username") String username, @JSONBody("password") String password);
POST http://localhost:8080/hello/user
HEADER:
    Content-Type: application/json
BODY:
    {"username": "foo", "password": "bar"}

当然,用来修饰集合也是一样的,不做过多阐述

其实用这个注解也就是在定义请求体里的格式的同时也定义了请求头的Content-Type: application/json

完全也可也用@Body去实现

@Post(
    url = "http://localhost:8080/hello/user",
    contentType = "application/json"
)
String send(@Body User user);

自己加上contentType就得,还清晰明了

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

这里还有@XMLBody注解用来修饰请求体,修饰对象为类时,需要添加@XmlRootElement(name = "xxxx")在类的上面,详情观看官方文档

1.62 data 属性

您也可以通过@Request、以及@Get@Post等请求注解的data属性把数据添加到请求体。需要注意的是只有当typePOSTPUTPATCH这类 HTTP Method 时,data属性中的值才会绑定到请求体中,而GET请求在有些情况会绑定到url的参数中。

type data属性数据绑定位置 支持的contentTypeContent-Type请求头
GET url参数部分 只有application/x-www-form-urlencoded
POST 请求体 任何contentType
PUT 请求体 任何contentType
PATCH 请求体 任何contentType
HEAD url参数部分 只有application/x-www-form-urlencoded
OPTIONS url参数部分 只有application/x-www-form-urlencoded
DELETE url参数部分 只有application/x-www-form-urlencoded
TRACE url参数部分 只有application/x-www-form-urlencoded

data属性中进行数据绑定:

public interface MyClient {

    /**
     * 这里 data 属性中设置的字符串内容会绑定到请求体中
     * 其中 ${0} 和 ${1} 为参数序号绑定,会将序号对应的参数绑定到字符串中对应的位置
     * ${0} 会替换为 username 的值,${1} 会替换为 password 的值
     */
    @Request(
            url = "http://localhost:8080/hello/user",
            type = "post",
            data = "username=${0}&password=${1}",
            headers = {"Accept:text/plain"}
    )
    String dataPost(String username, String password);
}
POST http://localhost:8080/hello/user
HEADER:
    Accept: text/plain
BODY:
    username=xxx&password=xxx

也可以自定义改变请求头中的文本格式,来改变对应请求体中的内容格式

**@BodyType也可以用来指定请求体的格式,他遵守第一原则,即使@Request或者@Post请求中注明了Content格式,依旧根据@BodyType的来**

同时他可以去指定编码格式Encoder,可以用谷歌阿里巴巴的等编码注解自行注明,详情看官方文档


1.7Forest后端的两种实现方式

okhttp3

httpclient

可以在配置中设置

# 设置全局后端框架
# 目前版本有两种选择:okhttp3 和 httpclient
# 不填的默认请求为 okhttp3

forest:
    backend: httpclient # 设置全局后端为 httpclient

# 设置全局后端框架
# 目前版本有两种选择:okhttp3 和 httpclient
# 不填的默认请求为 okhttp3

forest:
    backend: okhttp3 # 设置全局后端为 okhttp3

1.71后端 Client 缓存

为提高 Forest 请求的执行性能,默认情况下,每个请求所对应的后端客户端对象都会被缓存

请求前,会先去缓存中寻找所需的后端 Client 对象实例,如若没有,则新创建一个并放入该请求所对应的缓存中

接口的缓存开关设定如下:

// 关闭后端 Client 缓存
@Get("/")
@BackendClient(cache = false)
ForestRequest<String> sendData();

1.8接口注解

1.81@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);

}

1.9数据响应

1.91 反序列化

第一步:定义dataType属性

dataType`属性指定了该请求响应返回的数据类型,目前可选的数据类型有三种: `text`, `json`, `xml

Forest会根据您指定的dataType属性选择不同的反序列化方式。其中dataType的默认值为text,如果您不指定其他数据类型,那么Forest就不会做任何形式的序列化,并以文本字符串的形式返回给你数据。

/**
 * dataType为text或不填时,请求响应的数据将以文本字符串的形式返回回来
 */
@Request(
    url = "http://localhost:8080/text/data",
    dataType = "text"
)
String getData();

若您指定为jsonxml,那就告诉了Forest该请求的响应数据类型为JSON或XML形式的数据,就会以相应的形式进行反序列化。

/**
 * dataType为json或xml时,Forest会进行相应的反序列化
 */
@Request(
    url = "http://localhost:8080/text/data",
    dataType = "json"
)
Map getData();

第二步:指定反序列化的目标类型

反序列化需要一个目标类型,而该类型其实就是方法的返回值类型,如返回值为String就会反序列成String字符串,返回值为Map就会反序列化成一个HashMap对象,您也可以指定为自定义的Class类型。

public class User {
    private String username;
    private String score;
    
    // Setter和Getter ...
}
{"username":  "Foo", "score":  "82"}

那请求接口就应该定义成这样:

/**
 * dataType属性指明了返回的数据类型为JSON
 */
@Get(
    url = "http://localhost:8080/user?id=${0}",
    dataType = "json"
)
User getUser(Integer id)

1.4.0版本开始,dataType 属性默认为 auto(自动判断数据类型),但最好养成良好的个人习惯

1.92返回响应对象

直接用普通的对象类型作为请求方法的返回类型,可以将响应数据方便的反序列化,以满足大部分的需求。但还有很多时候不光需要获取响应内容,也需要得到响应头等信息,这时候就需要 ForestResponse 出场了。

/**
 * ForestResponse 可以作为请求方法的返回类型
 * ForestResponse 为带泛型的类,其泛型参数中填的类作为其响应反序列化的目标类型
 */
@Post("http://localhost:8080/user")
ForestResponse<String> postUser(@JSONBody User user);

对应的API

// 以ForestResponse类型变量接受响应数据
ForestResponse<String> response = client.postUser(user);

// 用isError方法去判断请求是否失败
if (response.isError()) {
    ... ...
}

// 用isSuccess方法去判断请求是否成功
if (response.isSuccess()) {
    ... ...
}

// 以字符串方式读取请求响应内容
String text = response.readAsString();

// getContent方法可以获取请求响应内容文本
// 和readAsString方法不同的地方在于,getContent方法不会读取二进制形式数据内容,
// 而readAsString方法会将二进制数据转换成字符串读取
String content = response.getContent();

// 获取反序列化成对象类型的请求响应内容
// 因为返回类型为ForetReponse<String>, 其泛型参数为String
// 所以这里也用String类型获取结果        
String result = response.getResult();

// 以字节数组的形式获取请求响应内容
byte[] byteArray = response.getByteArray();

// 以输入流的形式获取请求响应内容
InputStream in = response.getInputStream();

获取响应头

ForestResponse<String> response = client.textXXX();

// 根据响应头名称获取单个请求响应头
ForestHeader header = response.getHeader("Content-Type");
// 响应头名称
String headerName = header.getName();
// 响应头值
String headerValue = header.getValue();

// 根据响应头名称获取请求响应头列表
List<ForestHeader> heaers = response.getHeaders("Content-Type");

// 根据响应头名称获取请求响应头值
String val = response.getHeaderValue("Content-Type");

// 根据响应头名称获取请求响应头值列表
List<String> vals = response.getHeaderValues("Content-Type");

2.0数据转换

2.1序列化

几乎所有数据格式的转换都包含序列化和反序列化,Forest的数据转换同样如此

序列化是指,将原始的 Java 类型数据对象转化为 HTTP 请求想要发送的数据格式(如:JSONXMLProtobuf 等)

Forest中对数据进行序列化可以通过指定contentType属性或Content-Type头指定内容格式

@Post(
        url = "http://localhost:8080/hello/user",
        contentType = "application/json"    // 指定contentType为application/json
)
String postJson(@Body MyUser user);   // 自动将user对象序列化为JSON格式

同时反序列化其实也是会自动的

[至于Forest的集成配置请观看Forest官方文档详解:] (https://forest.dtflyx.com/pages/1.5.31/install_guide/ ”Forest“)