轻量级HTTP客户端框架使用(Forest)
一、Forest
1.1 业务需求
一般情况下是后端提供接口,前端调用,解决需求,但是有的时候为了方便,复用别人的接口(网上的,公共的第三方接口(短信、天气等)),就出现了后端调用后端接口的情况。
此外,因为业务关系,要和许多不同第三方公司进行对接。这些服务商都提供基于 http 的 api,但是每家公司提供 api 具体细节差别很大。
有的基于 RESTFUL 规范,有的基于传统的 http 规范;有的需要在 header 里放置签名,有的需要 SSL 的双向认证,有的只需要 SSL 的单向认证;有的以 JSON 方式进行序列化,有的以 XML 方式进行序列化・・・・・・类似于这样细节的差别较多。
1.2 Forest 简介
Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。
使用 Forest 就像使用类似 Dubbo 那样的 RPC 框架一样,只需要定义接口,调用接口即可,不必关心具体发送 HTTP 请求的细节。同时将 HTTP 请求信息与业务代码解耦,方便统一管理大量 HTTP 的 URL、Header 等信息。
而请求的调用方完全不必在意 HTTP 的具体内容,即使该 HTTP 请求信息发生变更,大多数情况也不需要修改调用发送请求的代码。
1.2.1 Forest 特性
- 以 Httpclient 和 OkHttp 为后端框架
- 通过调用本地方法的方式去发送 Http 请求,实现了业务逻辑与 Http 协议之间的解耦
- 因为针对第三方接口,所以不需要依赖 Spring Cloud 和任何注册中心
- 支持所有请求方法:GET,HEAD,OPTIONS,TRACE,POST,DELETE,PUT,PATCH
- 支持文件上传和下载
- 支持灵活的模板表达式
- 支持拦截器处理请求的各个生命周期
- 支持自定义注解
- 支持 OAuth2 验证
- 支持过滤器来过滤传入的数据
- 基于注解、配置化的方式定义 Http 请求
- 支持 Spring 和 Springboot 集成
- JSON 字符串到 Java 对象的自动化解析
- XML 文本到 Java 对象的自动化解析
- JSON、XML 或其他类型转换器可以随意扩展和替换
- 支持 JSON 转换框架:Fastjson,Jackson,Gson
- 支持 JAXB 形式的 XML 转换
- 可以通过 OnSuccess 和 OnError 接口参数实现请求结果的回调
- 配置简单,一般只需要 @Request 一个注解就能完成绝大多数请求的定义
- 支持异步请求调用
- 约定大于配置
- 自定义拦截器、自定义注解,扩展 Forest 的能力
1.2.2 Forest 工作原理
Forest 会将定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API (httpclient 等 API) 执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。请求发送方调用这个接口时,实际上就是在调用这个干脏活累活的实现类。
1.2.3 Forest 架构
HTTP 发送请求的过程分为前端部分和后端部分,Forest 本身是处理前端过程的框架,是对后端 HTTP API 框架的进一步封装。
前端部分:
(1)Forest 配置: 负责管理 HTTP 发送请求所需的配置。
(2)Forest 注解: 用于定义 HTTP 发送请求的所有相关信息,一般定义在 interface 上和其方法上。
(3)动态代理: 用户定义好的 HTTP 请求的 interface 将通过动态代理产生实际执行发送请求过程的代理类。
(4)模板表达式: 模板表达式可以嵌入在几乎所有的 HTTP 请求参数定义中,它能够将用户通过参数或全局变量传入的数据动态绑定到 HTTP 请求信息中。
(5)数据转换: 此模块将字符串数据和 JSON 或 XML 形式数据进行互转。目前 JSON 转换器支持 Jackson、Fastjson、Gson 三种,XML 支持 JAXB 一种。
(6)拦截器: 用户可以自定义拦截器,拦截指定的一个或一批请求的开始、成功返回数据、失败、完成等生命周期中的各个环节,以插入自定义的逻辑进行处理。
(7)过滤器: 用于动态过滤和处理传入 HTTP 请求的相关数据。
(8)SSL: Forest 支持单向和双向验证的 HTTPS 请求,此模块用于处理 SSL 相关协议的内容
后端部分:
后端为实际执行 HTTP 请求发送过程的第三方 HTTP API,目前支持 okHttp3 和 httpclient 两种后端 API
Spring Boot Starter Forest
:提供对 Spring Boot 的支持
四、Forest 使用
4.1 Forest 基础
4.1.1 配置层级
- 全局配置:
application.yml
/application.properties
配置(spring、Spring Boot 项目)以及通过ForestConfiguration
对象(普通 Java 项目)设置 - 接口配置
- 请求配置
Forest 的配置层级:
- 全局配置:针对全局所有请求,作用域最大,配置读取的优先级最小。
- 接口配置:作用域为某一个 interface 中定义的请求,读取的优先级最小。可以通过在 interface 上修饰
@BaseRequest
注解进行配置。 - 请求配置:作用域为某一个具体的请求,读取的优先级最高。可以在接口的方法上修饰
@Request
注解进行 HTTP 信息配置的定义。
4.1.2 全局基本配置
下面以 Spring Boot 项目为例:
在 application.yaml
/ application.properties
中配置的 HTTP 基本参数
1 | forest: |
配置 Bean ID
Forest 允许在 yaml 文件中配置 Bean Id,它对应着 ForestConfiguration
对象在 Spring 上下文中的 Bean 名称。
然后便可以在 Spring 中通过 Bean 的名称引用到它
1 |
|
4.1.3 构建接口
在 Forest 依赖加入好之后,就可以构建 HTTP 请求的接口了,在 Forest 中,所有的 HTTP 请求信息都要绑定到某一个接口的方法上,不需要编写具体的代码去发送请求。
请求发送方通过调用事先定义好 HTTP 请求信息的接口方法,自动去执行 HTTP 发送请求的过程,其具体发送请求信息就是该方法对应绑定的 HTTP 请求信息。
1 | public interface MyClient { |
4.1.4 HTTP Method
(1)POST 方式
1 | public interface MyClient { |
(2)GET 请求
1 | // GET请求 |
(3)PUT 请求
1 | // PUT请求 |
(4)HEAD 请求
1 | // HEAD请求 |
(5)Options 请求
1 | // Options请求 |
(6)Delete 请求
1 | // Delete请求 |
注:
@Get
和@GetRequest
两个注解的效果是等价的,@Post
和@PostRequest
、@Put
和@PutRequest
等注解也是同理- HEAD 请求类型没有对应的
@Head
注解,只有@HeadRequest
注解,原因是容易和@Header
注解混淆
4.1.5 HTTP URL
HTTP 请求可以没有请求头、请求体,但一定会有 URL,以及很多请求的参数都是直接绑定在 URL 的 Query 部分上。
基本 URL 设置方法只要在 url 属性中填入完整的请求地址即可。
除此之外,也可以从外部动态传入 URL:
1 | /** |
4.1.6 HTTP Header
(1)headers 属性
该接口调用后所实际产生的 HTTP 请求如下:
1 | GET http://localhost:8080/hello/user |
(2)数据绑定
这段调用所实际产生的 HTTP 请求如下:
1 | GET http://localhost:8080/hello/user |
(3)@Header 注解
1 | /** |
4.1.7 HTTP Body
在 POST 和 PUT 等请求方法中,通常使用 HTTP 请求体进行传输数据。在 Forest 中有多种方式设置请求体数据。
(1)@Body 注解
您可以使用 @Body 注解修饰参数的方式,将传入参数的数据绑定到 HTTP 请求体中。
1 | /** |
(2)表单格式
上面使用 @Body
注解的例子用的是普通的表单格式,也就是 contentType 属性为 application/x-www-form-urlencoded
的格式,即 contentType
不做配置时的默认值。
表单格式的请求体以字符串 key1=value1&key2=value2&...&key{n}=value{n}
的形式进行传输数据,其中 value 都是已经过 URL Encode
编码过的字符串。
1 | /** |
(3)JSON 格式
(4)XML 格式
4.1.8 @BaseRequest 注解
@BaseRequest
注解定义在接口类上,在 @BaseRequest
上定义的属性会被分配到该接口中每一个方法上,但方法上定义的请求属性会覆盖 @BaseRequest
上重复定义的内容。 因此可以认为 @BaseRequest
上定义的属性内容是所在接口中所有请求的默认属性。
1 | /** |
4.1.9 数据转换
(1)序列化
Forest 中对数据进行序列化可以通过指定 contentType
属性或 Content-Type
头指定内容格式。
1 |
|
同理,指定为 application/xml
会将参数序列化为 XML 格式,text/plain
则为文本,默认的 application/x-www-form-urlencoded
则为表格格式。
(2)反序列化
HTTP 请求响应后返回结果的数据同样需要转换,Forest 则会将返回结果自动转换为您通过方法返回类 型指定对象类型。这个过程就是反序列化,您可以通过 dataType
指定返回数据的反序列化格式。
1 |
|
4.1.10 日志管理
Forest 在发送请求时和接受响应数据时都会自动打印出 HTTP 请求相关的日志,其中包括:请求日志、响应状态日志、响应内容日志。
(1)请求日志
请求日志会打印出所有请求发送的内容,其中包括请求行、请求头、请求体三部分
1 | [Forest] Request: |
(2)响应状态日志
响应状态日志包含了 HTTP 请求响应后接受到的状态码,以及响应时间
1 | [Forest] Response: Status = 200, Time = 11ms |
(3)响应内容日志
响应内容日志则会打印出请求发送的目标服务器响应后,返回给请求接受方的实际数据内容
1 | [Forest] Response: Content={"flag":"success","message":"成功"} |
此外,Forest 还支持回调函数以及异步请求等。
4.2 Forest 进阶
4.2.1 HTTPS
(1)单向认证
如果访问的目标站点的 SSL 证书由信任的 Root CA 发布的,无需做任何事情便可以自动信任
1 | public interface Gitee { |
Forest 的单向验证的默认协议为 SSLv3,如果一些站点的 API 不支持该协议,可以在全局配置中将 ssl-protocol
属性修改为其它协议,如:TLSv1.1
, TLSv1.2
, SSLv2
等等。
1 | forest: |
全局配置可以配置一个全局统一的 SSL 协议,但现实情况是有很多不同服务(尤其是第三方)的 API 会使用不同的 SSL 协议,这种情况需要针对不同的接口设置不同的 SSL 协议。
1 | /** |
也可以在 @BaseRequest
注解中设置一整个接口类的 SSL 协议
1 |
|
(2)双向认证
若是需要在 Forest 中进行双向验证的 HTTPS 请求,处理如下:
在全局配置中添加 keystore 配置:
1 | forest: |
接着,在 @Request
中引入该 keystore 的 id 即可
1 |
|
也可以在全局配置中配多个 keystore:
1 | forest: |
4.2.2 异常处理
发送 HTTP 请求不会总是成功的,总会有失败的情况。Forest 提供多种异常处理的方法来处理请求失败的过程。
(1)try-catch 方式
最常用的是直接用 try-catch
。Forest 请求失败的时候通常会以抛异常的方式报告错误, 获取错误信息只需捕获 ForestNetworkException
异常类的对象,如示例代码所示:
1 | /** |
(2)回调函数方式
第二种方式是使用 OnError 回调函数,如示例代码所示:
1 | /** |
调用的代码如下:
1 | // 在调用接口时,在Lambda中处理错误结果 |
(3)ForestResponse
第三种,用 ForestResponse
类作为请求方法的返回值类型,示例代码如下:
1 | /** |
调用和处理的过程如下:
1 | ForestResponse<String> response = myClient.send("foo"); |
(4)拦截器方式
若要批量处理各种不同请求的异常情况,可以定义一个拦截器,并在拦截器的 onError 方法中处理异常,示例代码如下:
1 | public class ErrorInterceptor implements Interceptor<String> { |
4.2.3 拦截器
(1)构建拦截器
实现 com.dtflys.forest.interceptor.Interceptor
接口
1 | public class SimpleInterceptor implements Interceptor<String> { |
4.2.4 文件上传下载
(1)上传
1 | /** |
调用上传接口以及监听上传进度的代码如下:
1 | Map result = myClient.upload("D:\\TestUpload\\xxx.jpg", progress -> { |
在文件上传的接口定义中,除了可以使用字符串表示文件路径外,还可以用以下几种类型的对象表示要上传的文件:
1 | /** |
(2)多文件批量上传
1 | /** |
(3)下载
1 | /** |
调用下载接口以及监听上传进度的代码如下:
1 | File file = myClient.downloadFile("D:\\TestDownload", progress -> { |
如果您不想将文件下载到硬盘上,而是直接在内存中读取,可以去掉 @DownloadFile
注解,并且用以下几种方式定义接口:
1 | /** |
4.2.5 其它
使用 Cookie、使用代理、自定义注解、模板表达式・・・・・・
本文档部分内容摘自官方文档,具体详情可参见:Forest 官网:http://forest.dtflyx.com/