轻量级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)数据转换: 此模块将字符串数据和JSONXML形式数据进行互转。目前 JSON 转换器支持JacksonFastjsonGson三种,XML 支持JAXB一种。

(6)拦截器: 用户可以自定义拦截器,拦截指定的一个或一批请求的开始、成功返回数据、失败、完成等生命周期中的各个环节,以插入自定义的逻辑进行处理。

(7)过滤器: 用于动态过滤和处理传入 HTTP 请求的相关数据。

(8)SSL: Forest 支持单向和双向验证的 HTTPS 请求,此模块用于处理 SSL 相关协议的内容

后端部分:

后端为实际执行 HTTP 请求发送过程的第三方 HTTP API,目前支持okHttp3httpclient两种后端 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? = nullinternal var method: Stringinternal var headers: Headers.Builderinternal var body: RequestBody? = nullconstructor() {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 / application.properties配置(spring、Spring Boot项目)以及通过ForestConfiguration对象(普通Java项目)设置
  • 接口配置
  • 请求配置

    Forest 的配置层级:
  1. 全局配置:针对全局所有请求,作用域最大,配置读取的优先级最小。
  2. 接口配置:作用域为某一个interface中定义的请求,读取的优先级最小。可以通过在interface上修饰@BaseRequest注解进行配置。
  3. 请求配置:作用域为某一个具体的请求,读取的优先级最高。可以在接口的方法上修饰@Request注解进行 HTTP 信息配置的定义。

4.1.2 全局基本配置

下面以Spring Boot项目为例:

application.yaml / application.properties中配置的 HTTP 基本参数

forest:bean-id: config0              # 在spring上下文中bean的id, 默认值为forestConfigurationbackend: okhttp3               # 后端HTTP API: okhttp3,默认为okhttp3,也可以改为httpclientmax-connections: 1000      # 连接池最大连接数,默认值为500max-route-connections: 500 # 每个路由的最大连接数,默认值为500timeout: 3000                # 请求超时时间,单位为毫秒, 默认值为3000connect-timeout: 3000        # 连接超时时间,单位为毫秒, 默认值为2000retry-count: 1               # 请求失败后重试次数,默认为0次不重试ssl-protocol: SSLv3          # 单向验证的HTTPS的默认SSL协议,默认为SSLv3logEnabled: true                # 打开或关闭日志,默认为truelog-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,以及很多请求的参数都是直接绑定在URLQuery部分上。

基本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-8Content-Type: text/plain

(2)数据绑定


这段调用所实际产生的 HTTP 请求如下:

GET http://localhost:8080/hello/user
HEADER:Accept-Charset: gbkContent-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

POSTPUT等请求方法中,通常使用 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 HTTPHeaders: accessToken: abcdefg123456Body: username=foo&password=bar

(2)响应状态日志

响应状态日志包含了HTTP请求响应后接受到的状态码,以及响应时间

[Forest] Response: Status = 200, Time = 11ms

(3)响应内容日志

响应内容日志则会打印出请求发送的目标服务器响应后,返回给请求接受方的实际数据内容

[Forest] Response: Content={"flag":"success","message":"成功"}

此外,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中引入该keystoreid即可

@Request(url = "https://localhost:5555/hello/user",keyStore = "keystore1"
)
String send();

也可以在全局配置中配多个keystore

forest:...ssl-key-stores:- id: keystore1          # 第一个keystorefile: test1.keystore    keystore-pass: 123456  cert-pass: 123456      protocols: SSLv3       - id: keystore2          # 第二个keystorefile: 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> {// ... ...@Overridepublic 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 方法被调用时传入的参数数组 */@Overridepublic 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请求对象*/@Overridepublic boolean beforeExecute(ForestRequest request) {log.info("invoke Simple beforeExecute");// 执行在发送请求之前处理的代码request.addHeader("accessToken", "11111111");  // 添加Headerrequest.addQuery("username", "foo");  // 添加URL的Query参数return true;  // 继续执行请求返回true}/*** 该方法在请求成功响应时被调用*/@Overridepublic 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");}/*** 该方法在请求发送失败时被调用*/@Overridepublic 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(); // 获取方法返回类型对应的返回数据结果}/*** 该方法在请求发送之后被调用*/@Overridepublic 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

轻量级HTTP客户端框架—Forest学习笔记相关推荐

  1. Forest一款轻量级HTTP客户端框架

    Forest一款轻量级HTTP客户端框架 Forest Forest特性 Forest工作原理 Forest架构 HttpClient Okhttp Forest Forest 是一个开源的 Java ...

  2. Forest - 轻量级HTTP客户端框架

    Forest - 轻量级HTTP客户端框架 参考:再见,HttpClient!再见,Okhttp! 我觉得对于尤其是做对接第三方api的开发同学来说,这款开源框架能帮你提高很多效率. Forest 底 ...

  3. ssm 转发请求_千呼万唤!阿里内部终于把这份SSM框架技术学习笔记分享出来了...

    SSM SSM(Spring+SpringMVC+MyBatis)框架集由Spring.MyBatis两个开源框架整合而成(SpringMVC是Spring中的部分内容).常作为数据源较简单的web项 ...

  4. php mvc 路由,PHP MVC框架路由学习笔记

    文章主要介绍了PHP MVC框架路由学习笔记的相关资料,需要的朋友可以参考下. 提到PHP开发web,自然离不开开发框架,开发框架为我们提供了灵活的开发方式,MVC层分离,业务解耦等... 第一篇先来 ...

  5. Vue.js构建用户界面的渐进式框架(前端学习笔记1.0)

    文章目录 前言 一.Vue是什么? 二.前端核心分析 1.1.概述 1.2.前端三要素 1.3.结构层(HTML) 1.4.表现层(CSS) 1.5.行为层(JavaScript) 二.前端发展史 2 ...

  6. php怎么自己写框架,PHP学习笔记,自己动手写个MVC的框架

    最新在大家自己的博客的过程中,发现各种开源的博客系统都或多或少的用起来别扭.于是想动手自己写个博客系统.既然写,就想好好写.那就先写个MVC框架.一点一点来.写的过程中有很多想法.还希望大家能够多多指 ...

  7. ET框架---ActorComponent学习笔记

    ActorComponent 请大家关注我的微博:@NormanLin_BadPixel坏像素 /// <summary> /// 挂上这个组件表示该Entity是一个Actor, 它会将 ...

  8. ET框架---ClientDispatcher学习笔记

    ClientDispatcher学习笔记 请大家关注我的微博:@NormanLin_BadPixel坏像素 public class ClientDispatcher: IMessageDispatc ...

  9. linux查看ogg客户端版本,OGG学习笔记05-OGG的版本

    刚接触OGG的时候,很容易被众多的版本搞晕,虽然官方有提供各版本对应认证OS和DB的表格. 个人认为一个比较简单的方式,是直接去edelivery.oracle.com下载OGG,选定一个大版本后,这 ...

最新文章

  1. 2021年大数据Flink(三十八):​​​​​​​Table与SQL ​​​​​​案例五 FlinkSQL整合Hive
  2. 关于深度学习中GPU显存使用的介绍
  3. SVN导致目录图标出现“?”号解决方案
  4. cad直线和圆弧倒角不相切_曲线操作-直线,圆弧,圆,倒斜角
  5. struct vsf_sysutil_statbuf
  6. mysql数据库技术答案_高校邦《MySQL数据库高级技术》全套答案
  7. RNN梯度消失和爆炸的原因
  8. OriginPro8.5画双柱状图
  9. 【linux】循序渐进学运维-基础篇-修复文件系统实战
  10. python读取二维数组的行列数_Python获取二维数组的行列数的2种方法
  11. 【C语言网】C语言基础题集训练详解(一)
  12. 【期权系列】期权市场 PCR 指标的策略应用
  13. 卡西欧计算机fx82cnx怎么玩游戏,卡西欧fx-82ES计算器乱码玩法问题
  14. 计算机网络安全教程第三版课后答案
  15. 将文件复制到ftp服务器时发生错误,请检查是否有权将文件放到该服务器上
  16. 《viva la vida》 歌词
  17. skyline二次开发:mpt简析
  18. js es6 Thread.sleep如何使用
  19. 中信所怎么查期刊影响因子_年sci期刊影响因子分区中信所.xls
  20. 华为认证官网报名_华为认证报名网址是多少

热门文章

  1. 在计算机教学方面的论文,关于计算机教学方面的论文
  2. Android获取本机浏览器包名,并调用指定浏览器打开网页
  3. sangerbox制作heapmap_使用highmaps制作中国地图
  4. weblogic密码加解密
  5. 小程序12306服务器,12306小程序今天正式上线,有三大功能但不包括购票
  6. 服务器集群负载均衡原理
  7. MATLAB中果蝇味道浓度判定函数,果蝇优化算法-matlab实现、过程剖析
  8. 案例二:客户信息管理
  9. 用html5写一个模拟钢琴
  10. Android闪光灯实现