v1.5.21
版本发布了!
此次版本发布主要解决了URL在某些情况下的解析错误问题,以及对字符串模板进行手动URLEncode的支持
v1.5.20
版本发布了,中间因为有其它事情所以停滞了一段时间,还好有其他小伙伴的支持,为此间的版本升级,尤其是JDK 17的兼容做了很多工作,特此感谢。此次版本升级,以支持新版本JDK为主,包括从8到17的所有版本基本都可以兼容(主要测了较为主流的11和17,8不必说,原本就支持)
v1.5.19
版本发布了,此次更新主要修复了一些BUG
fix: 在原生Spring环境中,拦截器无法注入Spring上下文 (#I4UE9T:在原生Spring环境中,拦截器无法注入Spring上下文)
fix: httpclient后端的https连接池并发时数据会串 (#I4TYJ1:httpclient后端的https连接池并发时数据会串)
fix: 响应头两个 Set-Cookie 的时候只能取到最后一个 (#I4TATV: 响应头两个 Set-Cookie 的时候只能取到最后一个)
v1.5.17
版本发布了,本次更新主要修复以往的BUG,以及新增了动态正向代理来源信息的功能
feat: 动态正向代理来源信息 (#I4SYM1:动态正向代理来源信息)
fix: 异常:The file of SSL KeyStore is empty (#I4SYGB:异常:The file of SSL KeyStore is empty)
fix: maxRetryInterval配置失效 (#I4SV2P:maxRetryInterval配置失效)
fix: 主项目没有依赖lang3会报错的问题 (#I4M9DE:主项目没有依赖lang3会报错的问题)
fix: 组合注解未生效 (#I4N2HC:组合注解未生效)
refactor: 去掉 NameUtils 工具类中重复的if分支
1.5.15
版本发布了,本次添加了自定义SSL主机名验证器,以及修复了Response
字符编码和gzip
解压等问题
Forest已参加2021年度OSC中国开源项目评选活动,如果您喜欢Forest或对Forest感兴趣,请投上您宝贵的一票,感谢!
点击投票👉 投票
hostnameVerifier
定义一个实现 HostnameVerifier 接口的类
/**
* 自定义SSL主机名/域名验证器
*/
public class MyHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String s, SSLSession sslSession) {
if ("gitee.com".equals(s)) {
return true;
}
return false;
}
}
将自定义的SSL主机名验证器配置到 Forest 的KeyStore
中
application.yml
文件中配置forest:
ssl-key-stores:
- id: keystore1
hostname-verifier: your.site.MyHostnameVerifier
在接口中绑定
@Post(url = "/something", keyStore = "keystore1")
String postSomething(@Body body);
@SSLHostnameVerifier
注解除了在全局配置文件中的keyStore
中配置外, 也可以通过相关注解直接在接口上绑定
public class MyHostnameVerifier implements HostnameVerifier {
@Override
public boolean verify(String hostname, SSLSession session) {
// 只通过域名为 gitee.com 的请求
if ("gitee.com".equals(hostname)) {
return true;
}
return false;
}
}
通过@SSLHostnameVerifier
注解绑定到接口
@Post(url = "/something")
@SSLHostnameVerifier(TrustAnyHostnameVerifier.class)
String postSomething(@Body body);
@SSLSocketFactoryBuilder
注解同理 SSLSocketFactory 也一样可以自定义,同时由@SSLSocketFactoryBuilder
注解来绑定
public class MySSLSocketFactoryBuilder implements SSLSocketFactoryBuilder {
@Override
public SSLSocketFactory getSSLSocketFactory(ForestRequest request, String protocol) throws Exception {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null,
new TrustManager[] { new TrustAllManager() },
new SecureRandom());
System.out.println("do MySSLSocketFactoryBuilder");
return sslContext.getSocketFactory();
}
}
绑定到接口
@Post(url = "/something")
@SSLSocketFactoryBuilder(MySSLSocketFactoryBuilder.class)
String postSomething(@Body body);
本次更新有 @AlexShi 小伙伴参与贡献,万分感谢!
Forest已参加2021年度OSC中国开源项目评选活动,如果您喜欢Forest或对Forest感兴趣,请投上您宝贵的一票,感谢!
点击投票👉 投票
v1.5.14
版本发布!此次版本发布主要解决了异步线程池大小设置问题,以及URL中@
字符解析问题
v1.5.13
版本发布了,此次版本更新主要解决了url解析中的一些问题和指定请求Body类型和Encoder的问题。
ForestRequest
类的setBodyType
方法和bodyType
方法的参数类型改为ForestDataType
v1.5.12
版本发布了,此次更新主要重构了后端,解决了一些URL解析的相关问题
v1.5.11 版本发布了,此版本解决了若干BUG
v1.5.10 版本发布,此版本解决了上个版本改动引起的问题
v1.5.9 版本发布了,此次更新主要解决了URL Encoder在某些情况下不正确的问题
为此自己重新实现了URL解析和URL编码,抛弃了原有的Java自带的URI对象解析和URLEncoder类
实现了URL语义化的字符串模板
/**
* 新版本能够识别 {a} 和 {b} 是一个URL的Query参数
* 会按照查询参数的要求进行URL编码:会将'&'符号转义
* 而 {path} 会被识别为URL路径的一部分
* 会按照URL路径的要求来进行URL编码:会保留'&'符号,不做转义
*/
@Get("/data/{path}?a={a}&b={b}")
String getData(@Var("path") String path, @Var("a") String a, @Var("b") String b);
{}
与${}
的区别{}
代表一个Query参数{}
模板参数,在?a={a}
的情况下,会被认为是一个Query参数,即便变量可能包含"1&x=10&y=20"这样多个参数的字符串,也会被转义成一个Query参数
@Get("http://localhost/data?a={a}&b={b}")
String getData(@Var("a") String a, @Var("b") String b)
// 最终产生的URL是
// http://localhost/data?a=1%26x%3D10%26y%3D20&b=hello
// 也就是只会有 a 和 b 两个Query参数
myClient.getData("1&x=10&y=20", "hello");
${}
可以包含多个Query参数而 ${}
模板参数,可以认为是一种字符串替换,替换完再对URL参数进行解析,所以一个模板参数引用的变量中可能包含多个参数,也会被解析成多个参数
@Get("http://localhost/data?a=${a}&b=${b}")
String getData(@Var("a") String a, @Var("b") String b)
// 最终产生的URL是
// http://localhost/data?a=1&x=10&y=20&b=hello
// 也就是只会有 a、x、y、b 四个Query参数
myClient.getData("1&x=10&y=20", "hello");
{}
作为模板参数基于这两种模板参数各自的特性,都各有各的用处,但一般情况下,推荐使用{}
因为它更结构化、更语义化,也更容易让人理解,不容易出错,尤其是在URL参数中传递另一个URL地址时的作用更为突出
比如,要传一个带参数的子URL:https://search.gitee.com/?type=repository&q=forest
接到父URL后为 http://localhost/data?call={url}
如果是用${url}
就会出问题
@Get("/data?call=${url}")
String getData(@Var("url") String url);
// 最后产生的URL是
// http://localhost/data?call=https://search.gitee.com/?type=repository&q=forest
咋看起来没错,但最后那部分&q=forest
会被认为是父URL的Query参数,但其实应该是子URL的
如果用 {url}
就没这个问题,即使后来再有其它参数也毫无问题
@Get("/data?call={url}&x={x}")
String getData(@Var("url") String url, @Var("x") String x);
// 最后产生的URL是
// http://localhost/data?call=https://search.gitee.com/?type=repository%26q=forest&x=xxx
可以看到,子URL中Query参数的连接符&
被转义了,这样就解决了子URL参数和父URL参数(如后面的x
)之间产生的歧义
v1.5.8 版本发布,该版本主要解决以下BUG:
@DownloadFile
下载文件时,在某些环境下会发送阻塞 (#I4DLBI:使用@DownloadFile下载文件,在某些环境下会发送阻塞)v1.5.7 版本发布,主要解决 google protobuf 包依赖问题
FIX的BUG:
v1.5.6 版本发布了,此次更新主要修复一些BUG
v1.5.5 版本发布了,此版本主要支持了Protobuf,以及修改了若干Bug
@Post(
url = "/proto",
contentType = ContentType.APPLICATION_OCTET_STREAM)
ProtobufProto.Data sendProtobufData(@ProtobufBody ProtobufProto.Data data);
// 切换到 okhttp3
@OkHttp3
@Post("/data1")
String sendData1(@Body MyUser user);
// 切换到 httpclient
@HttpClient
@Post("/data2")
String sendData2(@Body MyUser user);
@OkHttp3
注解@HttpClient
注解Forest v1.5.4 发布
此版本主要注解一些遗留问题,此外补充一些接口
// 将Map中的每个键值对作为Query参数加到请求中
Map<String, Object> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
Forest.get("/")
// 添加 Map 到 Query参数
.addQuery(map)
// 执行请求
.execute();
// query参数为 a=1&b=2&c=3
// 添加列表到Query参数
Forest.get("/")
.addQuery("a", Arrays.asList(1, 2, 3))
.execute();
// query参数为 a=1&a=2&a=3
// 添加数组到Query参数
Forest.get("/")
.addQuery("a", new Object[] {1, 2, 3})
.execute();
// query参数为 a=1&a=2&a=3
// 添加列表到Query数组参数
Forest.get("/")
.addArrayQuery("a", Arrays.asList(1, 2, 3))
.execute();
// query参数为 a[]=1&a[]=2&a[]=3
// 添加数组到Query数组参数
Forest.get("/")
.addArrayQuery("a", new Object[] {1, 2, 3})
.execute();
// query参数为 a[]=1&a[]=2&a[]=3
// 添加列表到Query数组参数
Forest.get("/")
.addJSONQuery("a", Arrays.asList(1, 2, 3))
.execute();
// query参数为 a=[1,2,3]
// 注意:这里的JSON数据最终会被 URLEncode
// 所以最终请求的参数为 a=%5B1%2C2%2C3%5D
@Query
注解修饰Map参数时,Map中的列表属性无法正常序列化 (#I4C8UC:@Query 注解修饰Map参数时,Map中的列表属性无法正常序列化)v1.5.3 本版发布了,本次版本更新新增了较多功能,其中有许多重大更新。
以前版本使用 Forest,必须先定义一个 interface 接口类,这种形式可以满足大多数情况的场景。
但若想快速访问一个url可能显得不合时宜。
所以本次更新新增了快捷接口,不用再从定义接口开始了。
它大概长这个样子:
// Get请求
// 并以 String 类型接受数据
String str = Forest.get("/").executeAsString();
// Post请求
// 并以自定义的 MyResult 类型接受
MyResult myResult = Forest
.post("/")
.execute(MyResult.class);
// 通过 TypeRefernce 引用类传递泛型参数
// 就可以将响应数据以带复杂泛型参数的类型接受了
Result<List<User>> userList = Forest
.post("/")
.execute(new TypeReferenceList<Result<List<User>>>() {});
// 定义各种参数
// 并以 Map 类型接受
Map<String, Object> map = Forest.post("/")
.backend("okhttp3") // 设置后端为 okhttp3
.contentTypeJson() // 设置 Content-Type 头为 application/json
.host("127.0.0.1") // 设置地址的host为 127.0.0.1
.port(8080) // 设置地址的端口为 8080
.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();
@Success
注解先要定义 SuccessWhen 接口的实现类
public class TestSuccessWhen implements SuccessWhen {
/**
* 请求成功条件
* @param req Forest请求对象
* @param res Forest响应对象
* @return 是否成功
*/
@Override
public boolean successWhen(ForestRequest req, ForestResponse res) {
// 没有异常 并且 状态码在正常范围 并且 状态码不等于203
// 当然在这里也可以写其它条件,比如 通过 res.getData() 或 res.getConent() 获取业务数据
// 再更具业务数据判断是否成功
return res.noException() && res.statusOk() && res.statusCode() != 203;
}
}
在Forest请求接口方法上挂上 @Success
注解
@Get("http://localhost:${port}/")
@Success(condition = TestSuccessWhen.class)
String getData();
若调用 getData() 后,返回的状态码为 203, 就会被认为是请求失败,如果设置了重试次数大于0,就会去执行重试任务。
若没有重试次数可用,则进入 onError 请求失败流程
@Retry
注解先定义 RetryWhen 接口实现类
public class TestRetryWhen implements RetryWhen {
/**
* 请求重试条件
* @param request Forest请求对象
* @param response Forest响应对象
* @return 是否重试
*/
@Override
public boolean retryWhen(ForestRequest request, ForestResponse response) {
// 如果响应状态码为 203 就进行重试,尽管此时请求是成功的
// 当然在这里也可以写其它条件,比如 通过 res.getData() 或 res.getConent() 获取业务数据
// 再更具业务数据判断是否进行重试
return response.statusIs(203);
}
}
在Forest请求接口方法上挂上 @Retry
注解
// maxRetryCount 为最大重试次数
// maxRetryInterval 为最大重试时间间隔, 单位为毫秒
// condition 为请求重试条件,即自定义的 RetryWhen 接口实现类
@Get("http://localhost:${port}/")
@Retry(maxRetryCount = "3", maxRetryInterval = "10", condition = TestRetryWhen.class)
String sendData();
若调用 sendData() 后,返回的状态码为 203, 就会被认为需要重试,如果设置了重试次数大于0,就会去执行重试任务。
若没有重试次数可用,则进入 onSuccess 请求成功流程
可能有小伙伴有疑问,既然通过 SuccessWhen 成功条件判断失败后也可以触发重试,那为何还要 RetryWhen 呢?
Forest的重试机制是这样的:1. 先判定是否成功,失败的话触发重试 2. 如果是成功的,则判断是否符合重试条件,符合的话也触发重试.
这两者的区别是:
因为请求失败而重试的请求,当最后一次重试也是失败的话,就会进入请求失败流程(如调用 onError)
因为触发重试条件而重试的请求,此时请求判断是成功的,所以只要最后一次重试也是成功的,就会进入请求成功流程 (如调用 onSuccess)
简单一句话描述: successWhen失败就重试,retryWhen即便成功也重试
OnRetry
回调函数不管是哪一种重试方式,只要触发了请求重试,都会在重试请发送之前调用 OnRetry
回调函数
可以在拦截器中实现 onRetry 回调函数
public class TestRetryInterceptor implements Interceptor<Object> {
/**
* 在请重试前调用 onRetry 回调函数
*
* @param request Forest请求对象
* @param response Forest响应对象
*/
@Override
public void onRetry(ForestRequest request, ForestResponse response) {
// 将当前重试次数添加到 Forest 请求对象的附件中
request.addAttachment("retry-interceptor", request.getCurrentRetryCount());
}
}
将实现了 onRetry 方法的拦截器关联到相关接口上
@BaseRequest(baseURL = "http://localhost:${port}/", interceptor = TestRetryInterceptor.class)
public interface RetryClient {
@Get("/")
@Retry(maxRetryCount = "${0}", maxRetryInterval = "${1}", condition = TestRetryWhen.class)
ForestRequest<String> testRetryRequest(int retryCount, long retryInterval);
@Get("/")
@Retry(maxRetryCount = "${0}", maxRetryInterval = "${1}", condition = TestRetryWhen.class)
String testRetry(int retryCount, long retryInterval, OnSuccess<String> onSuccess);
}
此时 RetryClient 接口下的任意一个方法触发重试时,都会先调用 TestRetryInterceptor 拦截器类的 onRetry 方法。
feat: Forest快捷接口 (#I4893Q:Forest快捷接口)
feat: 支持全局变量动态绑定方法 (#I478N2:支持全局变量动态绑定方法)
feat: 支持引用properties的字符串模板 (#I3P1QK:支持引用properties的字符串模板)
feat: 支持获取响应原因短语,即响应状态文本 (#I4BJVF:支持获取响应原因短语,即响应状态文本)
feat: 自定义组合注解 (#I4BISF:支持自定义组合注解)
feat: 可自定义请求是否成功的条件 (#I4AEMT:可自定义请求是否成功的条件)
feat: 可动态设置主机地址和端口号 (#I4AEJ8:可动态设置主机地址和端口号)
feat: 自定义重试条件 (#I493N3:自定义重试条件)
feat: 新增 OnRetry 回调函数 (#I493N6:新增 OnRetry 回调函数)
feat: 新增 @Headers
注解 (#I4BJQ6:新增 @Headers 注解)
feat: Forest请求接口继承规则 (#I4B0N7:Forest请求接口继承规则)
feat: 自动重定向控制 (#I4B0FM:自动重定向控制)
feat: 全局变量支持动态绑定方法 (#I478N2:支持全局变量动态绑定方法)
feat: 在请求日志中显示后端框架名称 (#I4AKTD:在请求日志中显示后端框架名称)
feat: 新建forest-mock子项目 (#I468JB:新建forest-mock子项目)
fix: POST请求中,空Map无法转成{} JSON字符串 (#I455O2:POST请求中,空Map无法转成{} JSON字符串)
fix: 过滤器参数总是为第一个参数 (#I43VV0:过滤器参数总是为第一个参数)
fix: 自定义请求头content-type会替换为大写 (#I46WNW:自定义请求头content-type会替换为大写)
fix: 在Spring项目中如果不配置转换器就会找不到Converter (#I46FKV:在Spring项目中如果不配置转换器就会找不到Converter)
fix: Response不带Content-Type和Content-Encoding头时无法正常解析 (#I455PO:Response不带Content-Type和Content-Encoding头时无法正常解析)
fix: 当请求 302 请求时,Forest 会自动的访问重定向的url,导致 302 的响应头拿不到 (#I4AF3B:当请求 302 请求时,Forest 会自动的访问重定向的url,导致 302 的响应头拿不到。)
fix: SpringSSLKeyStore 在Spring中初始化失败
fix: 配置有ForestConfiguration参数的转换器的时候,在springboot中会初始化失败 (#I4AKT3:配置有ForestConfiguration参数的转换器的时候,在springboot中会初始化失败)
fix: 在多线程环境下使用上传文件接口,运行时间长后会报出堆栈溢出的错误 (#I37UGY:在多线程环境下使用上传文件接口,运行时间长后会报出堆栈溢出的错误)
fix: BeanPostProcessor 接口在低版本 springboot 环境下不兼容
opt: 优化 StringUtils 工具类方法
opt: 优化 URLUtils 工具类方法
add: SpringForestProperties类
add: 在所有请求注解中(如 @Request
, @Get
)添加 responseEncoding 属性,用于强制指定响应数据的编码格式
add: SpringForestObjectFactory类
add: ForestResponse.isRedirection 方法
add: ForestResponse.getRedirectionLocation 方法
add: ForestResponse.redirectionRequest 方法
add: ForestHeaderMap.clone 方法
add: ForestQueryMap.clone 方法
refactor: retryCount属性不在建议使用
update: 去掉MethodLifeCycle
refactor: 修改Forest接口扫描逻辑
refactor: 将 TypeReference 类改为抽象类
感谢他们参与的贡献,排名不分先后
特别感谢这几位小伙伴的贡献 (排名不分先后)