1. 什么forest
Forest 是一个开源的 Java HTTP 客户端框架,是一个高层的、极简的轻量级HTTP调用API框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。
2. 为什么使用 Forest
使用 Forest 就像使用类似 Dubbo 那样的 RPC 框架一样,只需要定义接口,调用接口即可,不必关心具体发送 HTTP 请求的细节。同时将 HTTP 请求信息与业务代码解耦,方便您统一管理大量 HTTP 的 URL、Header 等信息。而请求的调用方完全不必在意 HTTP 的具体内容,即使该 HTTP 请求信息发生变更,大多数情况也不需要修改调用发送请求的代码。
3. Forest 的工作原理
Forest 会将您定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API(httpclient 等 API)执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。 请求发送方调用这个接口时,实际上就是在调用这个干脏活累活的实现类。
4. Forest 的架构
- Forest 配置: 负责管理 HTTP 发送请求所需的配置。
- Forest 注解: 用于定义 HTTP 发送请求的所有相关信息,一般定义在 interface 上和其方法上。
- 动态代理: 用户定义好的 HTTP 请求的
interface
将通过动态代理产生实际执行发送请求过程的代理类。 - 模板表达式: 模板表达式可以嵌入在几乎所有的 HTTP 请求参数定义中,它能够将用户通过参数或全局变量传入的数据动态绑定到 HTTP 请求信息中。
- 数据转换: 此模块将字符串数据和
JSON
或XML
形式数据进行互转。目前 JSON 转换器支持Jackson
、Fastjson
、Gson
三种,XML 支持JAXB
一种。 - 拦截器: 用户可以自定义拦截器,拦截指定的一个或一批请求的开始、成功返回数据、失败、完成等生命周期中的各个环节,以插入自定义的逻辑进行处理。
- 过滤器: 用于动态过滤和处理传入 HTTP 请求的相关数据。
- SSL: Forest 支持单向和双向验证的 HTTPS 请求,此模块用于处理 SSL 相关协议的内容。
5. Forest 的使用
1) springboot集成forest
对于 Springboot 项目来说, 只需要添加 forest-spring-boot-starter 依赖即可
<dependency>
<groupId>com.dtflys.forest</groupId>
<artifactId>forest-spring-boot-starter</artifactId>
<version>1.5.28</version>
</dependency>
JSON框架依赖:springboot项目中默认依赖了jacJson框架,如果需要依赖fastJSON,则需要添加一下依赖 Fastjson依赖:版本 >= 1.2.48
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
2) 配置优先级/作用域
3) forest的使用有两种方式:声明式和编程式
一、 声明式
所有的 HTTP 请求信息都要绑定到某一个接口的方法上,不需要编写具体的代码去发送请求。请求发送方通过调用事先定义好 HTTP 请求信息的接口方法,自动去执行 HTTP 发送请求的过程,其具体发送请求信息就是该方法对应绑定的 HTTP 请求信息。
简单请求
创建一个interface
,并用@Request
注解修饰接口方法。
public interface MyClient {
@Request("http://localhost:8080/hello")
String simpleRequest();
}
通过@Request
注解,将上面的MyClient
接口中的simpleRequest()
方法绑定了一个 HTTP 请求, 其 URL 为http://localhost:8080/hello
,并默认使用GET
方式,且将请求响应的数据以String
的方式返回给调用者。
请求方法
HTTP 请求方法 | 请求注解 | 描述 |
---|---|---|
GET | @Get 、@GetRequest | 获取资源 |
POST | @Post 、@PostRequest | 传输实体文本 |
PUT | @Put 、@PutRequest | 上传资源 |
HEAD | @HeadRequest | 获取报文首部 |
DELETE | @Delete 、@DeleteRequest | 删除资源 |
OPTIONS | @Options 、@OptionsRequest | 询问支持的方法 |
TRACE | @Trace 、@TraceRequest | 追踪路径 |
PATCH | @Patch 、@PatchRequest | 更新资源的某一部分 |
不定方法 | @Request | 可动态传入HTTP方法 |
请求地址
通过 @Var
注解修饰的参数从外部动态传入URL
:
/**
* 整个完整的URL都通过参数传入
* {0}代表引用第一个参数
*/
@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);
根地址
@Address 注解
// 通过 @Address 注解绑定根地址
// host 绑定到第一个参数, port 绑定到第二个参数
@Post("/data")
@Address(host = "{0}", port = "{1}")
ForestRequest<String> sendHostPort(String host, int port);
动态根地址
Forest 提供了地址来源
接口,即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();
URL 参数
字符串模板传参
/**
* 直接在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 注解或全局配置声明变量
*/
@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);
数组参数
/*
* 接受列表参数为URL查询参数
*/
@Get("http://localhost:8080/abc")
String send1(@Query("id") List idList);
/*
* 接受数组参数为URL查询参数
*/
@Get("http://localhost:8080/abc")
String send2(@Query("id") int[] idList);
/*
* 在 @Query 注解的参数名后跟上 [] 即可
*/
@Get("http://localhost:8080/abc")
String send(@Query("id[]") int[] idList);
/*
* 内置变量 _index 代表数组的下标
*/
@Get("http://localhost:8080/abc")
String send(@Query("id[${_index}]") int[] idList);
产生的最终URL为
http://localhost:8080/abc?id=1&id=2&id=3&id=4
http://localhost:8080/abc?id=1&id=2&id=3&id=4
http://localhost:8080/abc?id[]=1&id[]=2&id[]=3&id[]=4
http://localhost:8080/abc?id[0]=1&id[1]=2&id[2]=3&id[3]=4
请求体
在POST
和PUT
等请求方法中,通常使用 HTTP 请求体进行传输数据。在 Forest 中有多种方式设置请求体数据。
@Body 注解
使用@Body
注解修饰参数的方式,将传入参数的数据绑定到 HTTP 请求体中
/**
* 默认body格式为 application/x-www-form-urlencoded,即以表单形式序列化数据
*/
@Post("http://localhost:8080/user")
String sendPost(@Body("username") String username, @Body("password") String password);
@JSONBody注解修饰对象
发送JSON非常简单,只要用@JSONBody
注解修饰相关参数就可以了
/**
* 被@JSONBody注解修饰的参数会根据其类型被自定解析为JSON字符串
* 使用@JSONBody注解时可以省略 contentType = "application/json"属性设置
*/
@Post("http://localhost:8080/hello/user")
String helloUser(@JSONBody User user);
成功/失败条件
默认成功/失败条件
Forest 提供了默认的请求成功/失败条件,其逻辑如下:
- 判断是否在发送和等待响应的过程中出现异常,如: 网络连接错误、超时等
- 在取得响应结果后,判断其响应状态码是否在正常范围内 (
100
~399
)
以上两条判断条件如有一条不满足,则就判定为请求失败,否则为成功。
自定义成功/失败条件
第一步,先要定义 SuccessWhen 接口的实现类
// 自定义成功/失败条件实现类
// 需要实现 SuccessWhen 接口
public class MySuccessCondition implements SuccessWhen {
/**
* 请求成功条件
* @param req Forest请求对象
* @param res Forest响应对象
* @return 是否成功,true: 请求成功,false: 请求失败
*/
@Override
public boolean successWhen(ForestRequest req, ForestResponse res) {
// req 为Forest请求对象,即 ForestRequest 类实例
// res 为Forest响应对象,即 ForestResponse 类实例
// 返回值为 ture 则表示请求成功,false 表示请求失败
return res.noException() && // 请求过程没有异常
res.statusOk() && // 并且状态码在 100 ~ 399 范围内
res.statusIsNot(203); // 但不能是 203
// 当然在这里也可以写其它条件,比如 通过 res.getResult() 或 res.getContent() 获取业务数据
// 再更具业务数据判断是否成功
}
}
第二步,挂上 @Success
注解
public interface MyClient {
/**
* 挂上了 @Success 注解
* <p>该方法的请求是否成功
* <p>以自定义成功条件类 MySuccessCondition 的判断方法为准
*
* @return 请求响应结果
*/
@Get("/")
@Success(condition = MySuccessCondition.class)
String sendData();
}
@Success
注解也可以挂在interface
接口类上,代表其下的所有请求方法都设置为此自定义条件
重试机制
使用@Retry
注解
public interface MyClient {
// maxRetryCount 为最大重试次数,默认为 0 次
// maxRetryInterval 为最大重试时间间隔, 单位为毫秒,默认为 0 毫秒
@Get("/")
@Retry(maxRetryCount = "3", maxRetryInterval = "10")
String sendData();
}
@Retry
注解也可以挂在interface
接口类上
自定义重试器
// 自定义重试器
// 继承 BackOffRetryer 类
public class MyRetryer extends BackOffRetryer {
public MyRetryer(ForestRequest request) {
super(request);
}
/**
* 重写 nextInterval 方法
* 该方法用于指定每次重试的时间间隔
* @param currentCount 当前重试次数
* @return 时间间隔 (时间单位为毫秒)
*/
@Override
protected long nextInterval(int currentCount) {
// 每次重试时间间隔恒定为 1s (1000ms)
return 1000;
}
}
public interface MyClient {
// maxRetryCount 为最大重试次数,默认为 0 次
// maxRetryInterval 为最大重试时间间隔, 单位为毫秒,默认为 0 毫秒
// @Retryer 注解绑定自定义的 MyRetryer 重试器类
// 将按照自定义的重试策略处理重试间隔时间
@Get("/")
@Retry(maxRetryCount = "3", maxRetryInterval = "10")
@Retryer(MyRetryer.class)
String sendData();
}
重试条件
Forest 请求的重试条件有两种设置模式:
- 将 请求成功/失败条件 作为重试条件
- 设置 RetryWhen 重试条件
重试流程
- 请求失败后的重试流程
请求失败 ✔
└── 是否未达到请求最大重试次数 (maxRetryCount) ✔
├── 执行 OnRetry 回调函数
├── 执行拦截器中的 onRetry 方法
├── 发送重试请求
└── 回到第一步,重试请求是否成功
- 请求成功后的重试流程
请求成功 ✔
└── 是否满足重试条件 (自定义 RetryWhen 接口类条件) ✔
└── 是否未达到请求最大重试次数 (maxRetryCount) ✔
├── 执行 OnRetry 回调函数
├── 执行拦截器中的 onRetry 方法
├── 发送重试请求
└── 回到第一步,重试请求是否成功,并且是否满足重试条件
使用 RetryWhen
接口实现重试条件
// 自定义重试条件类
// 需要实现 RetryWhen 接口
public class MyRetryCondition implements RetryWhen {
/**
* 请求重试条件
* @param req Forest请求对象
* @param res Forest响应对象
* @return true 重试,false 不重试
*/
@Override
public boolean retryWhen(ForestRequest req, ForestResponse res) {
// 响应状态码为 203 就重试
return res.statusIs(203);
}
}
public interface MyClient {
// maxRetryCount 为最大重试次数
// maxRetryInterval 为最大重试时间间隔, 单位为毫秒
@Get("/")
@Retry(maxRetryCount = "3", maxRetryInterval = "10", condition = MyRetryCondition.class)
String sendData();
}
异步请求
在Forest使用异步请求,可以通过设置@Request
注解的async
属性为true
实现,不设置或设置为false
即为同步请求
/**
* async 属性为 true 即为异步请求,为 false 则为同步请求
* 不设置该属性时,默认为 false
*/
@Get(
url = "http://localhost:8080/hello/user?username=${0}",
async = true
)
String asyncGet(String username);
使用回调函数
@Get(
url = "http://localhost:8080/hello/user?username=${0}",
async = true,
headers = {"Accept:text/plain"}
)
void asyncGet(String username, OnSuccess<String> onSuccess,OnError onError);
一般情况下,异步请求都通过OnSuccess<T>
回调函数来接受响应返回的数据,而不是通过接口方法的返回值,所以这里的返回值类型一般会定义为void
二、 编程式
发送请求
// Get请求
// 并以 String 类型接受数据
String str = Forest.get("/").executeAsString();
// Post请求
// 并以自定义的 MyResult 类型接受
MyResult myResult = Forest.post("/").execute(MyResult.class);
// 通过 TypeReference 引用类传递泛型参数
// 就可以将响应数据以带复杂泛型参数的类型接受了
Result<List<User>> userList = Forest.post("/")
.execute(new TypeReference<Result<List<User>>>() {});
// 异步 Post 请求
// 通过 onSuccess 回调函数处理请求成功后的结果
// 而 onError 回调函数则在请求失败后被触发
Forest.post("/")
.async()
.onSuccess(((data, req, res) -> {
// data 为响应成功后返回的反序列化过的数据
// req 为Forest请求对象,即 ForestRequest 类实例
// res 为Forest响应对象,即 ForestResponse 类实例
}))
.onError(((ex, req, res) -> {
// ex 为请求过程可能抛出的异常对象
// req 为Forest请求对象,即 ForestRequest 类实例
// res 为Forest响应对象,即 ForestResponse 类实例
}))
.execute();
// 定义各种参数
// 并以 Map 类型接受
Map<String, Object> map = Forest.post("/")
.backend("okhttp3") // 设置后端为 okhttp3
.host("127.0.0.1") // 设置地址的host为 127.0.0.1
.port(8080) // 设置地址的端口为 8080
.contentTypeJson() // 设置 Content-Type 头为 application/json
.addBody("a", 1) // 添加 Body 项(键值对): a, 1
.addBody("b", 2) // 添加 Body 项(键值对: b, 2
.maxRetryCount(3) // 设置请求最大重试次数为 3
// 设置 onSuccess 回调函数
.onSuccess((data, req, res) -> { log.info("success!"); })
// 设置 onError 回调函数
.onError((ex, req, res) -> { log.info("error!"); })
// 设置请求成功判断条件回调函数
.successWhen((req, res) -> res.noException() && res.statusOk())
// 执行并返回Map数据类型对象
.executeAsMap();