文章目录
- 前言
- Forest介绍
- 为什么使用 Forest?
- Forest 如何使用?
- Forest 的工作原理
- Forest 的架构
- Forest的使用
- 依赖导入
- 配置yml
- 请求
- 请求方法
- URL 参数
- 数据转换
- Content-Type 请求头
- 请求体类型
- Encoder
- 拦截器(常用)
- 一、构建拦截器
- 二、拦截器与 Spring 集成
- 注解说明:
- @Body 注解
- @JSONBody注解修饰对象(常用)
- @XMLBody注解修饰对象
- @BaseRequest 注解(常用)
- @Success 注解
- `@Retry`注解
- 文档和示例:
前言
这段时间我们公司在开发一个商城,我负责的是产品中心,需求有以下3点:
- 创建时需要同步到erp
- 同步时需要异步
- 同步失败时重试,3次
- 3次都失败时推送钉钉给对应的开发人员
其实1、2点都很容易实现,使用异步线程池,或者消息中间件等等都可以实现,第3点的话可以有很多实现方式, 可以在表中加个同步次数的字段,用定时任务去扫描,达到三次就发送,所以说实现的方式有很多种,就看你的选择,而我的选择是Forest,理由就是能提高我的效率,不需要关系内部实现,而且它有很好的集成,还有一点是没用过,想学习新的框架,还有就是它是中文的,学习成本相对来说不高
学习一个新的东西之前问自己三个问题,是什么?为什么?怎么用?下面将引用官方文档来说明
Forest介绍
Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。
为什么使用 Forest?
使用 Forest 就像使用类似 Dubbo 那样的 RPC 框架一样,只需要定义接口,调用接口即可,不必关心具体发送 HTTP 请求的细节。同时将 HTTP 请求信息与业务代码解耦,方便您统一管理大量 HTTP 的 URL、Header 等信息。而请求的调用方完全不必在意 HTTP 的具体内容,即使该 HTTP 请求信息发生变更,大多数情况也不需要修改调用发送请求的代码。
Forest 如何使用?
Forest 不需要您编写具体的 HTTP 调用过程,只需要您定义一个接口,然后通过 Forest 注解将 HTTP 请求的信息添加到接口的方法上即可。请求发送方通过调用您定义的接口便能自动发送请求和接受请求的响应。
Forest 的工作原理
Forest 会将您定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API(httpclient 等 API)执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。 请求发送方调用这个接口时,实际上就是在调用这个干脏活累活的实现类。
Forest 的架构
Forest的使用
forest有两种使用方式,一种是声明式接口,也就是我们熟悉的注解方式,另一种是编程式接口,本篇文章只介绍声明式使用方式;
依赖导入
<dependency><groupId>com.dtflys.forest</groupId><artifactId>forest-spring-boot-starter</artifactId><version>1.5.26</version>
</dependency>
配置yml
forest:backend: okhttp3 # 目前 Forest 支持okhttp3和httpclient两种后端 HTTP API,若不配 置该属性,默认为okhttp3 当然,您也可以改为httpclientmax-connections: 1000 # 连接池最大连接数(默认为 500)max-route-connections: 500 # 每个路由的最大连接数(默认为 500)max-request-queue-size: 100 # [自v1.5.22版本起可用] 最大请求等待队列大小max-async-thread-size: 300 # [自v1.5.21版本起可用] 最大异步线程数max-async-queue-size: 16 # [自v1.5.22版本起可用] 最大异步线程池队列大小timeout: 3000 # [已不推荐使用] 请求超时时间,单位为毫秒(默认为 3000)connect-timeout: 3000 # 连接超时时间,单位为毫秒(默认为 timeout)read-timeout: 3000 # 数据读取超时时间,单位为毫秒(默认为 timeout)max-retry-count: 0 # 请求失败后重试次数(默认为 0 次不重试)ssl-protocol: TLS # 单向验证的HTTPS的默认TLS协议(默认为 TLS)log-enabled: true # 打开或关闭日志(默认为 true)log-request: true # 打开/关闭Forest请求日志(默认为 true)log-response-status: true # 打开/关闭Forest响应状态日志(默认为 true)log-response-content: true # 打开/关闭Forest响应内容日志(默认为 false)
请求
请求一、
创建一个interface
,并用@Request
注解修饰接口方法。
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
中。
请求方法
Forest 使用不同的请求注解来标识某个接口方法来进行发送不同类型的请求,其支持的HTTP方法如下表所示:
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方法 |
URL 参数
可通过{参数序号}
来动态 获取参数,也可以在参数中使用@Var
注解来标明变量名配合{变量名来获取}
/*** 整个完整的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);
数据转换
几乎所有数据格式的转换都包含序列化和反序列化,Forest的数据转换同样如此
序列化是指,将原始的 Java 类型数据对象转化为 HTTP 请求想要发送的数据格式(如:JSON、XML、Protobuf 等
Content-Type 请求头
Forest中对数据进行序列化可以通过指定contentType
属性或Content-Type
头指定内容格式
@Post(url = "http://localhost:8080/hello/user",contentType = "application/json" // 指定contentType为application/json
)
String postJson(@Body MyUser user); // 自动将user对象序列化为JSON格式
同理,指定为application/xml
会将参数序列化为XML
格式,text/plain
则为文本,默认的application/x-www-form-urlencoded
则为表格格式。
请求体类型
或者,也可以通过@BodyType
注解指定type
属性
// 自动将user对象序列化为JSON格式
// 但此方式不会在请求中带有 Content-Type 请求头
@Post("http://localhost:8080/hello/user")
@BodyType("json")
String postJson(@Body MyUser user);
Encoder
// 指定仅仅使用 Jackson 转换器来序列化数据
@Post("http://localhost:8080/hello/user")
@BodyType(type = "json", encoder = ForestJacksonConverter.class)
String postJson(@Body MyUser user);
提示
在方法不指定 Encoder 的默认情况下,会去找接口上有没有设置 Encoder,如接口上也没有则使用全局的转换器为改方法请求的 Encoder
拦截器(常用)
用过Spring MVC的朋友一定对Spring的拦截器并不陌生,Forest也同样支持针对Forest请求的拦截器。
一、构建拦截器
定义一个拦截器需要实现com.dtflys.forest.interceptor.Interceptor
接口
public class SimpleInterceptor<T> implements Interceptor<T> {private final static Logger log = LoggerFactory.getLogger(SimpleInterceptor.class);/*** 该方法在被调用时,并在beforeExecute前被调用 * @Param request Forest请求对象* @Param args 方法被调用时传入的参数数组 */@Overridepublic void onInvokeMethod(ForestRequest req, ForestMethod method, Object[] args) {log.info("on invoke method");// req 为Forest请求对象,即 ForestRequest 类实例// method 为Forest方法对象,即 ForestMethod 类实例// addAttribute作用是添加和Forest请求对象以及该拦截器绑定的属性addAttribute(req, "A", "value1");addAttribute(req, "B", "value2");}/*** 该方法在请求发送之前被调用, 若返回false则不会继续发送请求* @Param request Forest请求对象*/@Overridepublic boolean beforeExecute(ForestRequest req) {log.info("invoke Simple beforeExecute");// 执行在发送请求之前处理的代码req.addHeader("accessToken", "11111111"); // 添加Headerreq.addQuery("username", "foo"); // 添加URL的Query参数return true; // 继续执行请求返回true}/*** 该方法在请求成功响应时被调用*/@Overridepublic void onSuccess(T data, ForestRequest req, ForestResponse res) {log.info("invoke Simple onSuccess");// 执行成功接收响应后处理的代码int status = res.getStatusCode(); // 获取请求响应状态码String content = res.getContent(); // 获取请求的响应内容String result = (String)data; // data参数是方法返回类型对应的返回数据结果,注意需要视情况修改对应的类型否则有可能出现类转型异常result = res.getResult(); // getResult()也可以获取返回的数据结果response.setResult("修改后的结果: " + result); // 可以修改请求响应的返回数据结果// 使用getAttributeAsString取出属性,这里只能取到与该Forest请求对象,以及该拦截器绑定的属性String attrValue1 = getAttributeAsString(req, "A1");}/*** 该方法在请求发送失败时被调用*/@Overridepublic void onError(ForestRuntimeException ex, ForestRequest req, ForestResponse res) {log.info("invoke Simple onError");// 执行发送请求失败后处理的代码int status = res.getStatusCode(); // 获取请求响应状态码String content = res.getContent(); // 获取请求的响应内容String result = res.getResult(); // 获取方法返回类型对应的返回数据结果}/*** 该方法在请求发送之后被调用*/@Overridepublic void afterExecute(ForestRequest req, ForestResponse res) {log.info("invoke Simple afterExecute");// 执行在发送请求之后处理的代码int status = res.getStatusCode(); // 获取请求响应状态码String content = res.getContent(); // 获取请求的响应内容String result = res.getResult(); // 获取方法返回类型对应的最终数据结果}
}
Interceptor
接口带有一个泛型参数,其表示的是请求响应后返回的数据类型。 Interceptor即代表返回的数据类型为 String
。
在拦截器的方法参数中基本都有 ForestRequest 类对象,即Forest请求对象,Forest的绝大部分操作都是围绕请求对象所作的工作。
二、拦截器与 Spring 集成
若我要在拦截器中注入 Spring 的 Bean 改如何做?
/*** 在拦截器的类上加上@Component注解,并保证它能被Spring扫描到*/
@Component
public class SimpleInterceptor implements Interceptor<String> {// 如此便能直接注入Spring上下文中所有的Bean了@Resouceprivate UserService userService;... ...
}
注解说明:
@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
注解修饰相关参数就可以了,该注解自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);
@XMLBody注解修饰对象
发送XML也非常简单,只要用@XMLBody
注解修饰相关参数就可以了,该注解自1.5.0-RC1
版本起可以使用。
/*** 被@JSONBody注解修饰的参数会根据其类型被自定解析为XML字符串* 其修饰的参数类型必须支持JAXB,可以使用JAXB的注解进行修饰* 使用@XMLBody注解时可以省略 contentType = "application/xml"属性设置*/
@Post("http://localhost:8080/hello/user")
String sendXmlMessage(@XMLBody User user);
@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);}
@BaseRequest
注解中的所有字符串属性都可以通过模板表达式{}
引用全局变量
或方法中的参数。
/** * 若全局变量中已定义 baseUrl 和 accept,* 便会将全局变量中的值绑定到 @BaseRequest 的属性中*/
@BaseRequest(baseURL = "${baseUrl}", // 默认域名headers = {"Accept:${accept}" // 默认请求头}
)
public interface MyClient {// 方法的URL的域名将会引用全局变量中定义的 baseUrl@Get("/hello/user") String send1(@Query("username") String username);// @BaseRequest 中的属性亦可以引用方法中的绑定变量名的参数@Get("/hello/user")String send2(@Var("baseUrl") String baseUrl);}
@Success 注解
Forest 提供了默认的请求成功/失败条件,其逻辑如下:
- 判断是否在发送和等待响应的过程中出现异常,如: 网络连接错误、超时等
- 在取得响应结果后,判断其响应状态码是否在正常范围内 (
100
~399
)
以上两条判断条件如有一条不满足,则就判定为请求失败,否则为成功。
默认的判断条件可以满足绝大部分场景的需要,也比较符合HTTP协议标准的规范,但也存在一些特殊场景,并不以HTTP标准为判断逻辑,这时候就需要用户进行自定义的请求成功/失败条件的判断了.
第一步:先要定义 SuccessWhen 接口的实现类
// 自定义成功/失败条件实现类
// 需要实现 SuccessWhen 接口
public class MySuccessCondition implements SuccessWhen {/*** 请求成功条件* @param req Forest请求对象* @param res Forest响应对象* @return 是否成功,true: 请求成功,false: 请求失败*/@Overridepublic 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() 获取业务数据// 再更具业务数据判断是否成功}
}
特别注意
在
successWhen
方法的逻辑代码中,千万不能调用res.isSuccess()
或!res.isError()
进行判断不然会引起死循环
第二步,挂上 @Success
注解
public interface MyClient {/*** 挂上了 @Success 注解* <p>该方法的请求是否成功* <p>以自定义成功条件类 MySuccessCondition 的判断方法为准* * @return 请求响应结果*/@Get("/")@Success(condition = MySuccessCondition.class)String sendData();
}
若调用sendData()
方法后,返回的状态码为 203, 就会被认为是请求失败,如果设置了重试次数大于0,就会去执行重试任务。 若没有重试次数可用,则进入 onError 请求失败流程
@Retry
注解
重试机制
public interface MyClient {// maxRetryCount 为最大重试次数,默认为 0 次// maxRetryInterval 为最大重试时间间隔, 单位为毫秒,默认为 0 毫秒@Get("/")@Retry(maxRetryCount = "3", maxRetryInterval = "10")String sendData();
}
这里的@Retry
注解设置了两个属性:
maxRetryCount
: 请求的最大重试次数,当重试次数大于该属性,将停止触发请求重试流程,默认为0
次,即不会触发重试maxRetryInterval
: 请求的最大重试时间间隔,单位为毫秒,默认为0
毫秒,即没有时间间隔
当调用该接口的sendData()
方法后,若请求失败就会不断进行重试,直到不满足重试条件为止 (即请求成功,或达到最大请求次数限制)
文档和示例:
- 项目主页
- 中文文档
- JavaDoc
- Demo工程