轻量级HTTP客户端框架—Forest学习笔记

轻量级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? = 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 / 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, 默认值为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,以及很多请求的参数都是直接绑定在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属性
![在这里插入_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM4MjMzMjU4,size_16,color_FFFFFF,t_70)
该接口调用后所实际产生的 HTTP 请求如下:

GET http://localhost:8080/hello/user
HEADER:
    Accept-Charset: utf-8
    Content-Type: text/plain

(2)数据绑定

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gje912qK-1618655431394)(C:\Users\XY\Desktop\Forest.assets\image-20210328165859272.png)]
这段调用所实际产生的 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

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 HTTP
    Headers: 
        accessToken: abcdefg123456
    Body: 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          # 第一个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

在这里插入图片描述

  • 14
    点赞
  • 75
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kevin&Amy

感谢您的鼓励!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值