轻量级HTTP客户端框架—Forest学习笔记
一、Forest
1.1 业务需求
一般情况下是后端提供接口,前端调用,解决需求,但是有的时候为了方便,复用别人的接口(网上的,公共的第三方接口(短信、天气等)),就出现了后端调用后端接口的情况。
此外,因为业务关系,要和许多不同第三方公司进行对接。这些服务商都提供基于http的api,但是每家公司提供api具体细节差别很大。有的基于RESTFUL规范,有的基于传统的http规范;有的需要在header里放置签名,有的需要SSL的双向认证,有的只需要SSL的单向认证;有的以JSON方式进行序列化,有的以XML方式进行序列化······类似于这样细节的差别较多。
不涉及业务的公共http调用套件 ???
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
的支持
二、HttpClient
HTTP 协议可能是现在 Internet 上使用得最多、最重要的协议了,越来越多的 Java 应用程序需要直接通过 HTTP 协议来访问网络资源。虽然JDK 的 java net包中已经提供了访问 HTTP 协议的基本功能,但是对于大部分应用程序来说,JDK 库本身提供的功能还不够丰富和灵活。HttpClient用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议的最新版本。
最初,HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
如今,Apache Jakarta Commons HttpClient项目已经寿终正寝,不再开发和维护。取而代之的是Apache Httpcomponents项目,它包括HttpClient和HttpCore两大模块,能提供更好的性能和更大的灵活性。
2.1 主要功能
HttpClient 提供的主要的功能:
- 实现了所有HTTP的方法(GET、POST、PUT、HEAD等)
- 支持自动转向
- 支持 HTTPS 协议
- 支持代理服务器
- ······
2.2 使用方法
使用HttpClient发送请求和接收响应一般分为以下几步:
(1)创建HttpClient对象;
(2)创建请求方法的实例,并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象;
(3)如果需要发送请求参数,可调用HttpGet、HttpPost共同的setParams(HetpParams params)方法来添加请求参数;对于HttpPost对象而言,也可调用
setEntity(HttpEntity entity)方法来设置请求参数;
(4)调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse;
(5)调用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器的响应内容,程序可通过该对象获取服务器的响应内容;
(6)释放连接。无论执行方法是否成功,都必须释放连接。
三、Okhttp
3.1 Okhttp简介
Okhttp作为目前Android使用最为广泛的网络框架之一,是一个高效的HTTP Client,其高效性体现在:
- 支持Spdy、Http1.X、Http2、Quic以及WebSocket
- 连接池复用底层TCP(Socket),减少请求延时
- 无缝的支持GZIP减少数据流量
- 缓存响应数据减少重复的网络请求
- 请求失败自动重试主机的其他ip,自动重定向
- ······
3.2 Okhttp请求机制
首先来了解下HTTP client、request、response。HTTP client的作用是接受request请求并返回response信息。request请求通常包含一个 URL,一个方法 (比如GET/POST),以及一个headers列表,还可能包含一个body(特定内容类型的数据流)。response则通常用响应代码(比如200表示成功,404表示未找到)、headers和可选的body来响应request请求。
Okhttp的请求机制,可以概括为以下流程:
(1)通过OkhttpClient创建一个Call,发起同步或异步请求;
(2)okhttp通过Dispatcher对所有的RealCall(Call的具体实现类)进行统一管理,并通过execute()及enqueue()方法对同步或异步请求进行处理;
(3)execute()及enqueue()这两个方法会最终调用RealCall中的getResponseWithInterceptorChain()方法,从拦截器链中获取返回结果;
(4)拦截器链中,依次通过RetryAndFollowUpInterceptor(重定向拦截器)、BridgeInterceptor(桥接拦截器)、CacheInterceptor(缓存拦截器)、
ConnectInterceptor(连接拦截器)、CallServerInterceptor(网络拦截器)对请求依次处理,与服务器建立连接后,获取返回数据,再经过上述拦截器依次 处理后,最后将结果返回给调用方。具体过程如下图所示:
3.3 具体架构图
(1)RetryAndFollowUpInterceptor:负责重定向:构建一个StreamAllocation对象,然后调用下一个拦截器获取结果,从返回结果中获取重定向的request,如果重定向的request不为空的话,并且不超过重定向最大次数的话就进行重定向,否则返回结果。注意:这里是通过一个while(true)的循环完成下一轮的重定向请求。
-
StreamAllocation为什么在第一个拦截器中就进行创建?
便于取消请求以及出错释放资源。
-
StreamAllocation的作用是什么?
StreamAllocation负责统筹管理Connection、Stream、Call三个实体类,具体就是为一个Call(Realcall),寻找( findConnection() )一个Connection(RealConnection),获取一个Stream(HttpCode)。
(2)BridgeInterceptor:负责将原始Requset转换给发送给服务端的Request以及将Response转化成对调用方友好的Response。
具体就是对request添加Content-Type、Content-Length、cookie、Connection、Host、Accept-Encoding等请求头以及对返回结果进行解压、保持cookie等。
(3)CacheInterceptor:负责读取缓存以及更新缓存。
在请求阶段:
读取候选缓存cacheCandidate;
根据originOequest和cacheresponse创建缓存策略CacheStrategy;
根据缓存策略,来决定是否使用网络或者使用缓存或者返回错误。
(4)ConnectInterceptor:负责与服务器建立连接,使用StreamAllocation.newStream来和服务端建立连接,并返回输入输出流(HttpCodec),实际上是通过
StreamAllocation中的findConnection寻找一个可用的Connection,然后调用Connection的connect方法,使用socket与服务端建立连接。
(5)CallServerInterceptor:负责从服务器读取响应的数据,主要的工作就是把请求的Request写入到服务端,然后从服务端读取Response。
3.4 设计模式
(1)拦截器:责任链模式
(2)okhttpclient:外观模式
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new HttpLoggingInterceptor())
.readTimeout(500, TimeUnit.MILLISECONDS)
.build();
在这里,我们实例化了一个HTTP的客户端client,然后配置了它的一些参数,比如拦截器、超时时间。我们通过一个统一的对象,调用一个接口或方法,就能完成我们的需求,而其内部的各种复杂对象的调用和跳转都不需要我们关心,从而降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。
(3)Request:建造者模式
val request: Request = Request.Builder()
.url(url)
.build()
//Request.kt
open class Builder {
internal var url: HttpUrl? = null
internal var method: String
internal var headers: Headers.Builder
internal var body: RequestBody? = null
constructor() {
this.method = "GET"
this.headers = Headers.Builder()
}
open fun build(): Request {
return Request(
checkNotNull(url) { "url == null" },
method,
headers.build(),
body,
tags.toImmutableMap()
)
}
}
从Request的生成代码中可以看到,用到了其内部类Builder,然后通过Builder类组装出了一个完整的有着各种参数的Request类。我们可以通过Builder,构建不同的Request请求,只需要传入不同的请求地址url,请求方法method,头部信息headers,请求体body即可。
(4)享元模式:通过线程池、连接池共享对象
(5)工厂模式:通过OkHttpClient生产出产品RealCall
四、Forest使用
4.1 Forest基础
4.1.1 配置层级
- 全局配置:
/application.yml
配置(spring、Spring Boot项目)以及通过application.properties
对象(普通Java项目)设置ForestConfiguration
- 接口配置
- 请求配置 Forest 的配置层级:
- 全局配置:针对全局所有请求,作用域最大,配置读取的优先级最小。
- 接口配置:作用域为某一个
中定义的请求,读取的优先级最小。可以通过在interface
上修饰interface
注解进行配置。@BaseRequest
- 请求配置:作用域为某一个具体的请求,读取的优先级最高。可以在接口的方法上修饰
注解进行 HTTP 信息配置的定义。@Request
4.1.2 全局基本配置
下面以Spring Boot项目为例:
在
application.yaml
/
application.properties
中配置的 HTTP 基本参数
forest:
bean-id: config0 # 在spring上下文中bean的id, 默认值为forestConfiguration
backend: okhttp3 # 后端HTTP API: okhttp3,默认为okhttp3,也可以改为httpclient
max-connections: 1000 # 连接池最大连接数,默认值为500
max-route-connections: 500 # 每个路由的最大连接数,默认值为500
timeout: 3000 # 请求超时时间,单位为毫秒, 默认值为3000
connect-timeout: 3000 # 连接超时时间,单位为毫秒, 默认值为2000
retry-count: 1 # 请求失败后重试次数,默认为0次不重试
ssl-protocol: SSLv3 # 单向验证的HTTPS的默认SSL协议,默认为SSLv3
logEnabled: true # 打开或关闭日志,默认为true
log-request: true # 打开/关闭Forest请求日志(默认为 true)
log-response-status: true # 打开/关闭Forest响应状态日志(默认为 true)
log-response-content: true # 打开/关闭Forest响应内容日志(默认为 false)
配置Bean ID
Forest 允许在 yaml 文件中配置 Bean Id,它对应着
ForestConfiguration
对象在 Spring 上下文中的 Bean 名称。
forest:
bean-id: config0 # 在spring上下文中bean的id,默认值为forestConfiguration
然后便可以在 Spring 中通过 Bean 的名称引用到它
@Resource(name = "config0")
private ForestConfiguration config0;
4.1.3 构建接口
在 Forest 依赖加入好之后,就可以构建 HTTP 请求的接口了,在 Forest 中,所有的 HTTP 请求信息都要绑定到某一个接口的方法上,不需要编写具体的代码去发送请求。请求发送方通过调用事先定义好 HTTP 请求信息的接口方法,自动去执行 HTTP 发送请求的过程,其具体发送请求信息就是该方法对应绑定的 HTTP 请求信息。
public interface MyClient {
/**
* 获取用户所有设备信息
*/
@Post(url = "https://yunlong.farm.xiaomaiot.com/v6/device_chunk/all",
headers = {
"token: ${token}",
"Content-Type:application/json"
})
String getDevice(@DataVariable("token") String token);
}
4.1.4 HTTP Method
(1)POST方式
public interface MyClient {
/**
* 通过 @Request 注解的 type 参数指定 HTTP 请求的方式。
*/
@Request(
url = "http://localhost:8080/hello",
type = "POST"
)
String simplePost();
/**
* 使用 @Post 注解,可以去掉 type = "POST" 这行属性
*/
@Post("http://localhost:8080/hello")
String simplePost();
/**
* 使用 @PostRequest 注解,和上面效果等价
*/
@PostRequest("http://localhost:8080/hello")
String simplePost();
}
(2)GET请求
// GET请求
@Request(
url = "http://localhost:8080/hello",
type = "get"
)
String simpleGet();
(3)PUT请求
// PUT请求
@Request(
url = "http://localhost:8080/hello",
type = "put"
)
String simplePut();
(4)HEAD请求
// HEAD请求
@Request(
url = "http://localhost:8080/hello",
type = "head"
)
String simpleHead();
(5)Options请求
// Options请求
@Request(
url = "http://localhost:8080/hello",
type = "options"
)
String simpleOptions();
(6)Delete请求
// Delete请求
@Request(
url = "http://localhost:8080/hello",
type = "delete"
)
String simpleDelete();
注:
-
和@Get
两个注解的效果是等价的,@GetRequest
和@Post
、@PostRequest
和@Put
等注解也是同理@PutRequest
- HEAD请求类型没有对应的@Head注解,只有@HeadRequest注解,原因是容易和@Header注解混淆
4.1.5 HTTP URL
HTTP请求可以没有请求头、请求体,但一定会有
URL
,以及很多请求的参数都是直接绑定在
URL
的
Query
部分上。
基本
URL
设置方法只要在
url
属性中填入完整的请求地址即可。
除此之外,也可以从外部动态传入
URL
:
/**
* 整个完整的URL都通过 @DataVariable 注解修饰的参数动态传入
*/
@Request("${myURL}")
String send1(@DataVariable("myURL") String myURL);
/**
* 通过参数转入的值值作为URL的一部分
*/
@Request("http://${myURL}/abc")
String send2(@DataVariable("myURL") String myURL);
4.1.6 HTTP Header
(1)headers属性
该接口调用后所实际产生的 HTTP 请求如下:
GET http://localhost:8080/hello/user
HEADER:
Accept-Charset: utf-8
Content-Type: text/plain
(2)数据绑定
这段调用所实际产生的 HTTP 请求如下:
GET http://localhost:8080/hello/user
HEADER:
Accept-Charset: gbk
Content-Type: text/plain
(3)@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);
/**
* 使用 @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);
4.1.7 HTTP Body
在
POST
和
PUT
等请求方法中,通常使用 HTTP 请求体进行传输数据。在 Forest 中有多种方式设置请求体数据。
(1)@Body注解
您可以使用
@Body
注解修饰参数的方式,将传入参数的数据绑定到 HTTP 请求体中。
/**
* 默认body格式为 application/x-www-form-urlencoded,即以表单形式序列化数据
*/
@Post(
url = "http://localhost:8080/user",
headers = {"Accept:text/plain"}
)
String sendPost(@Body("username") String username, @Body("password") String password);
(2)表单格式
上面使用 @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",
headers = {"Accept:text/plain"}
)
String sendPost(@Body("key1") String value1, @Body("key2") Integer value2, @Body("key3") Long value3);
(3)JSON格式
(4)XML格式
4.1.8 @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);
}
4.1.9 数据转换
(1)序列化
Forest中对数据进行序列化可以通过指定
contentType
属性或
Content-Type
头指定内容格式。
@Request(
url = "http://localhost:8080/hello/user",
type = "post",
contentType = "application/json" // 指定contentType为application/json
)
String postJson(@Body MyUser user); // 自动将user对象序列化为JSON格式
同理,指定为
application/xml
会将参数序列化为
XML
格式,
text/plain
则为文本,默认的
application/x-www-form-urlencoded
则为表格格式。
(2)反序列化
HTTP请求响应后返回结果的数据同样需要转换,Forest则会将返回结果自动转换为您通过方法返回类型指定对象类型。这个过程就是反序列化,您可以通过
dataType
指定返回数据的反序列化格式。
@Request(
url = "http://localhost:8080/data",
dataType = "json" // 指定dataType为json,将按JSON格式反序列化数据
)
Map getData(); // 请求响应的结果将被转换为Map类型对象
4.1.10 日志管理
Forest在发送请求时和接受响应数据时都会自动打印出HTTP请求相关的日志,其中包括:请求日志、响应状态日志、响应内容日志。
(1)请求日志
请求日志会打印出所有请求发送的内容,其中包括请求行、请求头、请求体三部分
[Forest] Request:
POST http://localhost:8080/test HTTP
Headers:
accessToken: abcdefg123456
Body: username=foo&password=bar
(2)响应状态日志
响应状态日志包含了HTTP请求响应后接受到的状态码,以及响应时间
[Forest] Response: Status = 200, Time = 11ms
(3)响应内容日志
响应内容日志则会打印出请求发送的目标服务器响应后,返回给请求接受方的实际数据内容
此外,Forest还支持回调函数以及异步请求等。
4.2 Forest进阶
4.2.1 HTTPS
(1)单向认证
如果访问的目标站点的SSL证书由信任的Root CA发布的,无需做任何事情便可以自动信任
public interface Gitee {
@Request(url = "https://gitee.com")
String index();
}
Forest的单向验证的默认协议为
SSLv3
,如果一些站点的API不支持该协议,可以在全局配置中将
ssl-protocol
属性修改为其它协议,如:
TLSv1.1
,
TLSv1.2
,
SSLv2
等等。
forest:
...
ssl-protocol: TLSv1.2
全局配置可以配置一个全局统一的SSL协议,但现实情况是有很多不同服务(尤其是第三方)的API会使用不同的SSL协议,这种情况需要针对不同的接口设置不同的SSL协议。
/**
* 在某个请求接口上通过 sslProtocol 属性设置单向SSL协议
*/
@Get(
url = "https://localhost:5555/hello/user",
sslProtocol = "SSL"
)
ForestResponse<String> truestSSLGet();
也可以在
@BaseRequest
注解中设置一整个接口类的SSL协议
@BaseRequest(sslProtocol = "TLS")
public interface SSLClient {
@Get("https://localhost:5555/hello/user")
String testSend();
}
(2)双向认证
若是需要在Forest中进行双向验证的HTTPS请求,处理如下:
在全局配置中添加
keystore
配置:
forest:
...
ssl-key-stores:
- id: keystore1 # id为该keystore的名称,必填
file: test.keystore # 公钥文件地址
keystore-pass: 123456 # keystore秘钥
cert-pass: 123456 # cert秘钥
protocols: SSLv3 # SSL协议
接着,在
@Request
中引入该
keystore
的
id
即可
@Request(
url = "https://localhost:5555/hello/user",
keyStore = "keystore1"
)
String send();
也可以在全局配置中配多个
keystore
:
forest:
...
ssl-key-stores:
- id: keystore1 # 第一个keystore
file: test1.keystore
keystore-pass: 123456
cert-pass: 123456
protocols: SSLv3
- id: keystore2 # 第二个keystore
file: test2.keystore
keystore-pass: abcdef
cert-pass: abcdef
protocols: SSLv3
...
4.2.2 异常处理
发送HTTP请求不会总是成功的,总会有失败的情况。Forest提供多种异常处理的方法来处理请求失败的过程。
(1)try-catch方式
最常用的是直接用
try-catch
。Forest请求失败的时候通常会以抛异常的方式报告错误, 获取错误信息只需捕获
ForestNetworkException
异常类的对象,如示例代码所示:
/**
* try-catch方式:捕获ForestNetworkException异常类的对象
*/
try {
String result = myClient.send();
} catch (ForestNetworkException ex) {
int status = ex.getStatusCode(); // 获取请求响应状态码
ForestResponse response = ex.getResponse(); // 获取Response对象
String content = response.getContent(); // 获取请求的响应内容
String resResult = response.getResult(); // 获取方法返回类型对应的最终数据结果
}
(2)回调函数方式
第二种方式是使用
OnError
回调函数,如示例代码所示:
/**
* 在请求接口中定义OnError回调函数类型参数
*/
@Request(
url = "http://localhost:8080/hello/user",
headers = {"Accept:text/plain"},
data = "username=${username}"
)
String send(@DataVariable("username") String username, OnError onError);
调用的代码如下:
// 在调用接口时,在Lambda中处理错误结果
myClient.send("foo", (ex, request, response) -> {
int status = response.getStatusCode(); // 获取请求响应状态码
String content = response.getContent(); // 获取请求的响应内容
String result = response.getResult(); // 获取方法返回类型对应的最终数据结果
});
(3)ForestResponse
第三种,用
ForestResponse
类作为请求方法的返回值类型,示例代码如下:
/**
* 用`ForestResponse`类作为请求方法的返回值类型, 其泛型参数代表实际返回数据的类型
*/
@Request(
url = "http://localhost:8080/hello/user",
headers = {"Accept:text/plain"},
data = "username=${username}"
)
ForestResponse<String> send(@DataVariable("username") String username);
调用和处理的过程如下:
ForestResponse<String> response = myClient.send("foo");
// 用isError方法判断请求是否失败, 比如404, 500等情况
if (response.isError()) {
int status = response.getStatusCode(); // 获取请求响应状态码
String content = response.getContent(); // 获取请求的响应内容
String result = response.getResult(); // 获取方法返回类型对应的最终数据结果
}
(4)拦截器方式
若要批量处理各种不同请求的异常情况,可以定义一个拦截器, 并在拦截器的
onError
方法中处理异常,示例代码如下:
public class ErrorInterceptor implements Interceptor<String> {
// ... ...
@Override
public void onError(ForestRuntimeException ex, ForestRequest request, ForestResponse response) {
int status = response.getStatusCode(); // 获取请求响应状态码
String content = response.getContent(); // 获取请求的响应内容
Object result = response.getResult(); // 获取方法返回类型对应的返回数据结果
}
}
4.2.3 拦截器
(1)构建拦截器
实现com.dtflys.forest.interceptor.Interceptor接口
public class SimpleInterceptor implements Interceptor<String> {
private final static Logger log = LoggerFactory.getLogger(SimpleInterceptor.class);
/**
* 该方法在被调用时,并在beforeExecute前被调用
* @Param request Forest请求对象
* @Param args 方法被调用时传入的参数数组
*/
@Override
public void onInvokeMethod(ForestRequest request, ForestMethod method, Object[] args) {
log.info("on invoke method");
// addAttribute作用是添加和Request以及该拦截器绑定的属性
addAttribute(request, "A", "value1");
addAttribute(request, "B", "value2");
}
/**
* 该方法在请求发送之前被调用, 若返回false则不会继续发送请求
* @Param request Forest请求对象
*/
@Override
public boolean beforeExecute(ForestRequest request) {
log.info("invoke Simple beforeExecute");
// 执行在发送请求之前处理的代码
request.addHeader("accessToken", "11111111"); // 添加Header
request.addQuery("username", "foo"); // 添加URL的Query参数
return true; // 继续执行请求返回true
}
/**
* 该方法在请求成功响应时被调用
*/
@Override
public void onSuccess(String data, ForestRequest request, ForestResponse response) {
log.info("invoke Simple onSuccess");
// 执行成功接收响应后处理的代码
int status = response.getStatusCode(); // 获取请求响应状态码
String content = response.getContent(); // 获取请求的响应内容
String result = data; // data参数是方法返回类型对应的返回数据结果
result = response.getResult(); // getResult()也可以获取返回的数据结果
response.setResult("修改后的结果: " + result); // 可以修改请求响应的返回数据结果
// 使用getAttributeAsString取出属性,这里只能取到与该Request对象,以及该拦截器绑定的属性
String attrValue1 = getAttributeAsString(request, "A1");
}
/**
* 该方法在请求发送失败时被调用
*/
@Override
public void onError(ForestRuntimeException ex, ForestRequest request, ForestResponse response) {
log.info("invoke Simple onError");
// 执行发送请求失败后处理的代码
int status = response.getStatusCode(); // 获取请求响应状态码
String content = response.getContent(); // 获取请求的响应内容
String result = response.getResult(); // 获取方法返回类型对应的返回数据结果
}
/**
* 该方法在请求发送之后被调用
*/
@Override
public void afterExecute(ForestRequest request, ForestResponse response) {
log.info("invoke Simple afterExecute");
// 执行在发送请求之后处理的代码
int status = response.getStatusCode(); // 获取请求响应状态码
String content = response.getContent(); // 获取请求的响应内容
String result = response.getResult(); // 获取方法返回类型对应的最终数据结果
}
}
4.2.4 文件上传下载
(1)上传
/**
* 用@DataFile注解修饰要上传的参数对象
* OnProgress参数为监听上传进度的回调函数
*/
@Post(url = "/upload")
Map upload(@DataFile("file") String filePath, OnProgress onProgress);
调用上传接口以及监听上传进度的代码如下:
Map result = myClient.upload("D:\\TestUpload\\xxx.jpg", progress -> {
System.out.println("total bytes: " + progress.getTotalBytes()); // 文件大小
System.out.println("current bytes: " + progress.getCurrentBytes()); // 已上传字节数
System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%"); // 已上传百分比
if (progress.isDone()) { // 是否上传完成
System.out.println("-------- Upload Completed! --------");
}
});
在文件上传的接口定义中,除了可以使用字符串表示文件路径外,还可以用以下几种类型的对象表示要上传的文件:
/**
* File类型对象
*/
@Post(url = "/upload")
Map upload(@DataFile("file") File file, OnProgress onProgress);
/**
* byte数组
* 使用byte数组和Inputstream对象时一定要定义fileName属性
*/
@Post(url = "/upload")
Map upload(@DataFile(value = "file", fileName = "${1}") byte[] bytes, String filename);
/**
* Inputstream 对象
* 使用byte数组和Inputstream对象时一定要定义fileName属性
*/
@Post(url = "/upload")
Map upload(@DataFile(value = "file", fileName = "${1}") InputStream in, String filename);
/**
* Spring Web MVC 中的 MultipartFile 对象
*/
@PostRequest(url = "/upload")
Map upload(@DataFile(value = "file") MultipartFile multipartFile, OnProgress onProgress);
/**
* Spring 的 Resource 对象
*/
@Post(url = "/upload")
Map upload(@DataFile(value = "file") Resource resource);
(2)多文件批量上传
/**
* 上传Map包装的文件列表
* 其中 ${_key} 代表Map中每一次迭代中的键值
*/
@PostRequest(url = "/upload")
ForestRequest<Map> uploadByteArrayMap(@DataFile(value = "file", fileName = "${_key}") Map<String, byte[]> byteArrayMap);
/**
* 上传List包装的文件列表
* 其中 ${_index} 代表每次迭代List的循环计数(从零开始计)
*/
@PostRequest(url = "/upload")
ForestRequest<Map> uploadByteArrayList(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") List<byte[]> byteArrayList);
/**
* 上传数组包装的文件列表
* 其中 ${_index} 代表每次迭代List的循环计数(从零开始计)
*/
@PostRequest(url = "/upload")
ForestRequest<Map> uploadByteArrayArray(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") byte[][] byteArrayArray);
(3)下载
/**
* 在方法上加上@DownloadFile注解
* dir属性表示文件下载到哪个目录
* filename属性表示文件下载成功后以什么名字保存,如果不填,这默认从URL中取得文件名
* OnProgress参数为监听上传进度的回调函数
*/
@Get(url = "http://localhost:8080/images/xxx.jpg")
@DownloadFile(dir = "${0}", filename = "${1}")
File downloadFile(String dir, String filename, OnProgress onProgress);
调用下载接口以及监听上传进度的代码如下:
File file = myClient.downloadFile("D:\\TestDownload", progress -> {
System.out.println("total bytes: " + progress.getTotalBytes()); // 文件大小
System.out.println("current bytes: " + progress.getCurrentBytes()); // 已下载字节数
System.out.println("progress: " + Math.round(progress.getRate() * 100) + "%"); // 已下载百分比
if (progress.isDone()) { // 是否下载完成
System.out.println("-------- Download Completed! --------");
}
});
如果您不想将文件下载到硬盘上,而是直接在内存中读取,可以去掉@DownloadFile注解,并且用以下几种方式定义接口:
/**
* 返回类型用byte[],可将下载的文件转换成字节数组
*/
@GetRequest(url = "http://localhost:8080/images/test-img.jpg")
byte[] downloadImageToByteArray();
/**
* 返回类型用InputStream,用流的方式读取文件内容
*/
@GetRequest(url = "http://localhost:8080/images/test-img.jpg")
InputStream downloadImageToInputStream();
4.2.5 其它
使用Cookie、使用代理、自定义注解、模板表达式······
本文档部分内容摘自官方文档,具体详情可参见:Forest官网:http://forest.dtflyx.com/
注:
笔者写了一个基于Springboot的demo(Maven项目),分别采用Forest、HttpClient、Okhttp三种方式调用高德地图API,Forest中还包括拦截器的使用、下载图片等示例,项目具体目录结构如下图所示,包括Forest、HttpClient、Okhttp三部分:
使用Postman进行测试:
本案例中所有代码已上传至本人博客资源下载首页:https://download.csdn.net/download/qq_38233258/16731710