一、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两大模块,能提供更好的性能和更大的灵活性。扩展:最全的java面试题库
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即可。扩展:最全的java面试题库
(4)享元模式
通过线程池、连接池共享对象
(5)工厂模式
通过OkHttpClient生产出产品RealCall
四、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 基本参数
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请求相关的日志,其中包括:请求日志、响应状态日志、响应内容日志。扩展:最全的java面试题库
(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中引入该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提供多种异常处理的方法来处理请求失败的过程。扩展:最全的java面试题库
(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进行测试:
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。