该丢弃 HttpClient 了,这款轻量级 HTTP 客户端 API 框架很强

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
可视分析地图(DataV-Atlas),3 个项目,100M 存储空间
数据可视化DataV,5个大屏 1个月
简介: 一般情况下是后端提供接口,前端调用,解决需求,但是有的时候为了方便,复用别人的接口(网上的,公共的第三方接口(短信、天气等)),就出现了后端调用后端接口的情况。此外,因为业务关系,要和许多不同第三方公司进行对接。这些服务商都提供基于http的api,但是每家公司提供api具体细节差别很大。

一、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-TypeContent-LengthcookieConnectionHostAccept-Encoding等请求头以及对返回结果进行解压、保持cookie等。

(3)CacheInterceptor: 负责读取缓存以及更新缓存。

  • 在请求阶段:
  • 读取候选缓存cacheCandidate
  • 根据originOequestcacheresponse创建缓存策略CacheStrategy
  • 根据缓存策略,来决定是否使用网络或者使用缓存或者返回错误。

(4)ConnectInterceptor: 负责与服务器建立连接,使用StreamAllocation.newStream来和服务端建立连接,并返回输入输出流(HttpCodec),实际上是通过

StreamAllocation中的findConnection寻找一个可用的Connection,然后调用Connectionconnect方法,使用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给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关实践学习
DataV Board用户界面概览
本实验带领用户熟悉DataV Board这款可视化产品的用户界面
阿里云实时数仓实战 - 项目介绍及架构设计
课程简介 1)学习搭建一个数据仓库的过程,理解数据在整个数仓架构的从采集、存储、计算、输出、展示的整个业务流程。 2)整个数仓体系完全搭建在阿里云架构上,理解并学会运用各个服务组件,了解各个组件之间如何配合联动。 3&nbsp;)前置知识要求 &nbsp; 课程大纲 第一章&nbsp;了解数据仓库概念 初步了解数据仓库是干什么的 第二章&nbsp;按照企业开发的标准去搭建一个数据仓库 数据仓库的需求是什么 架构 怎么选型怎么购买服务器 第三章&nbsp;数据生成模块 用户形成数据的一个准备 按照企业的标准,准备了十一张用户行为表 方便使用 第四章&nbsp;采集模块的搭建 购买阿里云服务器 安装 JDK 安装 Flume 第五章&nbsp;用户行为数据仓库 严格按照企业的标准开发 第六章&nbsp;搭建业务数仓理论基础和对表的分类同步 第七章&nbsp;业务数仓的搭建&nbsp; 业务行为数仓效果图&nbsp;&nbsp;
相关文章
|
10天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
1月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
42 4
|
1月前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
105 3
|
1月前
|
算法 数据库 数据安全/隐私保护
摘要认证,使用HttpClient实现HTTP digest authentication
这篇文章提供了使用HttpClient实现HTTP摘要认证(digest authentication)的详细步骤和示例代码。
169 2
|
11天前
|
API PHP 数据库
PHP中哪个框架最适合做API?
在数字化时代,API作为软件应用间通信的桥梁至关重要。本文探讨了PHP中适合API开发的主流框架,包括Laravel、Symfony、Lumen、Slim、Yii和Phalcon,分析了它们的特点和优势,帮助开发者选择合适的框架,提高开发效率、保证接口稳定性和安全性。
29 3
|
18天前
|
JavaScript 中间件 API
Node.js进阶:Koa框架下的RESTful API设计与实现
【10月更文挑战第28天】本文介绍了如何在Koa框架下设计与实现RESTful API。首先概述了Koa框架的特点,接着讲解了RESTful API的设计原则,包括无状态和统一接口。最后,通过一个简单的博客系统示例,详细展示了如何使用Koa和koa-router实现常见的CRUD操作,包括获取、创建、更新和删除文章。
36 4
|
1月前
使用Netty实现文件传输的HTTP服务器和客户端
本文通过详细的代码示例,展示了如何使用Netty框架实现一个文件传输的HTTP服务器和客户端,包括服务端的文件处理和客户端的文件请求与接收。
43 1
使用Netty实现文件传输的HTTP服务器和客户端
|
26天前
|
前端开发 JavaScript 中间件
前端全栈之路Deno篇(四):Deno2.0如何快速创建http一个 restfulapi/静态文件托管应用及oak框架介绍
Deno 是由 Node.js 创始人 Ryan Dahl 开发的新一代 JavaScript 和 TypeScript 运行时,旨在解决 Node.js 的设计缺陷,具备更强的安全性和内置的 TypeScript 支持。本文介绍了如何使用 Deno 内置的 `Deno.serve` 快速创建 HTTP 服务,并详细讲解了 Oak 框架的安装和使用方法,包括中间件、路由和静态文件服务等功能。Deno 和 Oak 的结合使得创建 RESTful API 变得高效且简便,非常适合快速开发和部署现代 Web 应用程序。
|
11天前
|
安全 API 网络架构
Python中哪个框架最适合做API?
本文介绍了Python生态系统中几个流行的API框架,包括Flask、FastAPI、Django Rest Framework(DRF)、Falcon和Tornado。每个框架都有其独特的优势和适用场景。Flask轻量灵活,适合小型项目;FastAPI高性能且自动生成文档,适合需要高吞吐量的API;DRF功能强大,适合复杂应用;Falcon高性能低延迟,适合快速API开发;Tornado异步非阻塞,适合高并发场景。文章通过示例代码和优缺点分析,帮助开发者根据项目需求选择合适的框架。
36 0
|
1月前
|
数据采集 人工智能 自然语言处理
Python实时查询股票API的FinanceAgent框架构建股票(美股/A股/港股)AI Agent
金融领域Finance AI Agents方面的工作,发现很多行业需求和用户输入的 query都是和查询股价/行情/指数/财报汇总/金融理财建议相关。如果需要准确的 金融实时数据就不能只依赖LLM 来生成了。常规的方案包括 RAG (包括调用API )再把对应数据和prompt 一起拼接送给大模型来做文本生成。稳定的一些商业机构的金融数据API基本都是收费的,如果是以科研和demo性质有一些开放爬虫API可以使用。这里主要介绍一下 FinanceAgent,github地址 https://github.com/AI-Hub-Admin/FinanceAgent