一. 背景:

最近小熙在写对接,涉及到一些远程调用,用的是httpclient实现的,但是觉得有些麻烦。有没有封装过的框架,让操作更方便呢,有的比如:Forset

  1. 介绍:
    Forest 是一个开源的 Java HTTP 客户端框架,它能够将 HTTP 的所有请求信息(包括 URL、Header 以及 Body 等信息)绑定到您自定义的 Interface 方法上,能够通过调用本地接口方法的方式发送 HTTP 请求。

  2. 好处:
    使用 Forest 就像使用类似 Dubbo 那样的 RPC 框架一样,只需要定义接口,调用接口即可,不必关心具体发送 HTTP 请求的细节。同时将 HTTP 请求信息与业务代码解耦,方便您统一管理大量 HTTP 的 URL、Header 等信息。而请求的调用方完全不必在意 HTTP 的具体内容,即使该 HTTP 请求信息发生变更,大多数情况也不需要修改调用发送请求的代码。

  3. 原理:
    Forest 会将您定义好的接口通过动态代理的方式生成一个具体的实现类,然后组织、验证 HTTP 请求信息,绑定动态数据,转换数据形式,SSL 验证签名,调用后端 HTTP API(httpclient 等 API)执行实际请求,等待响应,失败重试,转换响应数据到 Java 类型等脏活累活都由这动态代理的实现类给包了。 请求发送方调用这个接口时,实际上就是在调用这个干脏活累活的实现类。

  4. 架构:
    forest架构图

二: 依赖引入和配置yml:

这里小熙使用的项目是 springboot 的,所以是基于此演示的
坐标版本引用的是 1.5.0-RC2 的,所以以下演示都是基于此版本的

  1. 引入坐标:

    <dependency>
        <groupId>com.dtflys.forest</groupId>
        <artifactId>spring-boot-starter-forest</artifactId>
        <version>1.5.0-RC2</version>
    </dependency>
    

    辅助 json 解析工具坐标(Fastjson依赖:版本 >= 1.2.48):

       <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>fastjson</artifactId>
          <version>1.2.48</version>
      </dependency>
    
  2. yml中的配置:

       forest:
         bean-id: forestConfiguration # 在spring上下文中bean的id, 默认值为forestConfiguration
         backend: okhttp3             # 后端HTTP API: 默认使用: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,也可以在单个方法上加注解是否开启:@LogEnabled
    

    获取Forest在spring上下文中的bean

    @Resource(name = "forestConfiguration")
    private ForestConfiguration forestConfiguration;
    

三. 使用介绍:

1. 请求类型:可支持(GET, POST, PUT, HEAD, OPTIONS, DELETE)

	    /**
	     * 测试访问百度
	     * 这里使用的是Get请求,你也可以简写为:@Get
	     * 
	     * 下面的请求同理
	     * 
	     * @return
	     */
	    @GetRequest(url = "http://www.baidu.com")
	    String accessBaiDu();
        
        /**
         * 也可以在请求里面指定类型
         * 
         * 下面的请求同理
         * 
         * @return
	     */
        @Request(url = "http://www.baidu.com", type = "get")
        String accessBaiDuRequestType();
        
        @Post(url = "http://www.baidu.com")
        String accessBaiDuPost();
    
        @Put(url = "http://www.baidu.com")
        String accessBaiDuPut();
    
        @DeleteRequest(url = "http://www.baidu.com")
        String accessBaiDuDelete();

2. 动态替换请求中变量(可传入):

	    /**
	     * post请求
	     * @param domainName 参数传递domainName(变量绑定是@DataVariable,视情况而定可以不出现在url上)
	     * @param port 参数传递 port(变量绑定是@DataVariable)
	     * @return
	     */
	    @PostRequest(url = "http://${domainName}:${port}")
	    String testDataVariable(@DataVariable("domainName") String domainName, @DataVariable("port") Integer port);

3. 动态替换请求变量的优化(对模板替换和批量参数等不够优雅和繁杂)

	    /**
	     * Query的时候需要注意的点:
	     * (1) 需要单个单个定义 参数名=参数值 的时候,@Query注解的value值一定要有,比如 @Query("name") String name
	     *
	     * (2) 需要绑定对象的时候,@Query注解的value值一定要空着,比如 @Query User user 或 @Query Map map
	     */
	   
	    /**
	     * post请求
	     * @param domainName (@Query修饰的参数一定会出现在url中)
	     * @param port
	     * @return
	     */
	    @PostRequest(url = "http://domainName:port")
	    String testQuery(@Query("domainName") String domainName, @Query("port") Integer port);

       /**
	     * post请求
	     * 使用 @Query 注解,可以修饰 Map 类型的参数
	     * 很自然的,Map 的 Key 将作为 URL 的参数名, Value 将作为 URL 的参数值
	     * 这时候 @Query 注解不定义名称
	     * @param map
	     * @return
	     */
	    @PostRequest(url = "http://domainName:port")
	    String testQuery(@Query Map<String, Object> map);

	    /**
	     * @Query 注解也可以修饰自定义类型的对象参数
	     * 依据对象类的 Getter 和 Setter 的规则取出属性
	     * 其属性名为 URL 参数名,属性值为 URL 参数值
	     * 这时候 @Query 注解不定义名称
	     * @param urlClass 这是你的入参对象
	     * @return
	     */
	    @PostRequest(url = "http://domainName:port")
	    String testQuery(@Query UrlClass urlClass);

4. 关于请求头的设置(以下例子中依次递增的是优化方案):

		    /*
		     *  @Header使用注意事项
		     * (1) 需要单个单个定义请求头的时候,@Header注解的value值一定要有,比如 @Header("Content-Type") String contentType
		     *
		     * (2) 需要绑定对象的时候,@Header注解的value值一定要空着,比如 @Header MyHeaders headers 或 @Header Map headerMap
		     */
		   
		    /**
		     * 默认get请求,在head中也可以使用@Query设置值同理@DataVariable
		     * @param encoding
		     * @return
		     */
		    @Request(
		            url = "http://www.baidu.com",
		            headers = {
		                    "Accept-Charset: ${encoding}",
		                    "Content-Type: text/plain"
		            }
		    )
		    String testheaders(@Query("encoding") String encoding);
	
		    /**
		     * post请求
		     * 使用 @Header 注解将参数绑定到请求头上
		     * @Header 注解的 value 指为请求头的名称,参数值为请求头的值
		     * @Header("Accept") String accept将字符串类型参数绑定到请求头 Accept 上
		     * @Header("accessToken") String accessToken将字符串类型参数绑定到请求头 accessToken 上
		     * @param accept
		     * @param accessToken
		     */
		    @Post("http://www.baidu.com")
		    void testHead(@Header("Accept") String accept, @Header("accessToken") String accessToken);
		
		    /**
		     * 使用 @Header 注解可以修饰 Map 类型的参数
		     * Map 的 Key 指为请求头的名称,Value 为请求头的值
		     * 通过此方式,可以将 Map 中所有的键值对批量地绑定到请求头中
		     * @param headerMap
		     */
		    @Post("http://www.baidu.com")
		    void testHead(@Header Map<String, Object> headerMap);
		
		
		    /**
		     * 使用 @Header 注解可以修饰自定义类型的对象参数
		     * 依据对象类的 Getter 和 Setter 的规则取出属性
		     * 其属性名为 URL 请求头的名称,属性值为请求头的值
		     * 以此方式,将一个对象中的所有属性批量地绑定到请求头中
		     */
		    @Post("http://localhost:8080/hello/user?username=foo")
		    void testHead(@Header HeaderInfo headersInfo);

5. 请求中的附加参数(head和body中):

	    /**
	     * delete请求
	     * @param url
	     * @param param 这是数据参数,附加在请求上,get在head,post在body
	     *              此方法@DataParam已过时
	     * @return
	     */
	    @DeleteRequest(url = "${url}")
	    String testVariableAndParam(@DataVariable("url") String url, @DataParam("param") String param);

        /**
	     * @Body注解修饰的参数一定会绑定到请求体中
	     * 默认body格式为 application/x-www-form-urlencoded,即以表单形式序列化数据
	     * @param username
	     * @param password
	     * @return
	     *    username=xxx&password=xxx (这是默认格式,如果要转为json格式,在请求头中设置即可Content-Type: application/json)
	     */
	    @Post(
	            url = "http://www.baidu.com",
	            headers = {"Accept:text/plain"}
	    )
	    String testBody(@Body("username") String username,  @Body("password") String password);
	
	    /**
	     * 也可以使用@Body修饰,将整个类以表单方式传输
	     * @param item
	     * @return
	     */
	    @Post(
	            url = "http://www.baidu.com",
	            headers = {"Accept:text/plain"}
	    )
	    String testBody(@Body Item item);
	
	    /**
	     * 被 @JSONBody注解修饰的参数会根据其类型被自定解析为JSON字符串, (也可以拆分开,修饰单个参数,如 @JSONBody(string) String string)
	     *  修饰map等亦可以,当修饰 list的时候,结果类型可为["A", "B", "C"]
	     * 使用 @JSONBody注解时可以省略 contentType = "application/json"属性设置
	     * @param item
	     * @return
	     *    {"username": "xx", "password": "xxx"}
	     */
	    @Post("http://www.baidu.com")
	    String testJSONBody(@JSONBody Item item);

6. 基础请求(提取封装请求相关属性,避免重复编写):

	   /**
	     * @BaseRequest 为配置接口层级请求信息的注解,
	     * 其属性会成为该接口下所有请求的默认属性,
	     * 但可以被方法上定义的属性所覆盖
	     *
	     * @BaseRequest注解中的所有字符串属性都可以通过模板表达式引用全局变量或方法中的参数。
	     * 若全局变量中已定义 baseUrl
	     * 便会将全局变量中的值绑定到 @BaseRequest 的属性中
	     */
	    @BaseRequest(
	            baseURL = "${baseUrl}",     // 默认域名
	            headers = {
	                    "Accept:text/plain"                // 默认请求头
	            },
	            sslProtocol = "sslType"                    // 默认单向SSL协议(TLS)
	    )
	    interface MyClient {
	
	        /**
	         * 在 @BaseRequest 中的属性亦可以引用方法中的绑定变量名的参数
	         * @param baseUrl 在BaseRequest注解中被引用
	         * @return
	         */
	        @Get("/hello/user")
	        String testOne(@DataVariable("baseUrl") String baseUrl);
	
	        /**
	         * 方法的URL不必再写域名部分
	         * @param sslType 在BaseRequest注解中被引用
	         * @return
	         */
	        @Get("/hello/user")
	        String testTwo(@Query("sslType") String sslType);
	
	        /**
	         * 若方法的URL是完整包含http://开头的,那么会以方法的URL中域名为准,不会被接口层级中的baseUrl属性覆盖
	         * @param username
	         * @return
	         */
	        @Get("http://www.xxx.com/hello/user")
	        String testThree(@Query("username") String username);
	
	        /**
	         * 覆盖请求头中信息
	         * @param username
	         * @return
	         */
	        @Get(
	                url = "/hello/user",
	                headers = {
	                        "Accept:application/json"      // 覆盖接口层级配置的请求头信息
	                }
	        )
	        String testFour(@Query("username") String username);
	
	    }

7. 接收响应封装数据:

	   /**
	     * dataType属性指定了该请求响应返回的数据类型,目前可选的数据类型有三种: text, json, xml
	     * Forest请求会自动将响应的返回数据反序列化成您要的数据类型。想要接受指定类型的数据需要完成两步操作:
	     * 第一步:定义dataType属性
	     * 第二步:指定反序列化的目标类型
	     * 从1.4.0版本开始,dataType 属性默认为 auto(自动判断数据类型), 也就是说 dataType 属性可以完全省略不填,Forest会自行判断返回的数据类型是哪种格式。
	     */
	
	    /**
	     * dataType为text或不填时,请求响应的数据将以文本字符串的形式返回回来
	     * @return
	     */
	    @Request(
	            url = "http://localhost:8080/text/data",
	            dataType = "text"
	    )
	    String testQueryStringData();
	
	    /**
	     * dataType属性指明了返回的数据类型为JSON
	     * 从1.4.0版本开始,dataType 属性默认为 auto(自动判断数据类型),
	     * 也就是说 dataType 属性可以完全省略不填,Forest会自行判断返回的数据类型是哪种格式。
	     * @param id
	     * @return
	     */
	    @Get(
	            url = "http://localhost:8080/user?id=${0}",
	            dataType = "json"   // 注意这里从1.4开始可以不写,自动判断
	    )
	    User testQueryUserData(Integer id);
	
	
	    /**
	     * ForestResponse 可以获取响应内容,也可以得到响应头等信息
	     * ForestResponse 可以作为请求方法的返回类型
	     * ForestResponse 为带泛型的类,其泛型参数中填的类作为其响应反序列化的目标类型
	     * @param item
	     * @return
	     */
	    @Post("http://localhost:8080/user")
	    ForestResponse<String> testResponseString(@JSONBody Item item);
	
	    /**
	     * 响应封装在对象中
	     *  日志打印默认为true
	     * @param item
	     * @return
	     */
	    @LogEnabled(value = true)
	    @Post("http://localhost:8080/user")
	    ForestResponse<Item> testResponseItem(@JSONBody Item item);

其中用ForestResponse对象接到请求响应数据后便可以获取响应内容:

	// 以 ForestResponse类型变量接受响应数据
	ForestResponse<String> response = client.postUser(user);
	
	// 用 isError方法去判断请求是否失败
	if (response.isError()) {
	    ... ...
	}
	
	// 用isSuccess方法去判断请求是否成功
	if (response.isSuccess()) {
	    ... ...
	}
	
	// 以字符串方式读取请求响应内容
	String text = response.readAsString();
	
	// getContent方法可以获取请求响应内容文本
	// 和 readAsString方法不同的地方在于,getContent方法不会读取二进制形式数据内容,
	// 而 readAsString方法会将二进制数据转换成字符串读取
	String content = response.getContent();
	
	// 获取反序列化成对象类型的请求响应内容
	// 因为返回类型为 ForetReponse<String>, 其泛型参数为String
	// 所以这里也用 String类型获取结果        
	String result = response.getResult();
	
	// 以字节数组的形式获取请求响应内容
	byte[] byteArray = response.getByteArray();
	
	// 以输入流的形式获取请求响应内容
	InputStream in = response.getInputStream();

    // 根据响应头名称获取单个请求响应头
	ForestHeader header = response.getHeader("Content-Type");
	// 响应头名称
	String headerName = header.getName();
	// 响应头值
	String headerValue = header.getValue();
	
	// 根据响应头名称获取请求响应头列表
	List<ForestHeader> heaers = response.getHeaders("Content-Type");
	
	// 根据响应头名称获取请求响应头值
	String val = response.getHeaderValue("Content-Type");
	
	// 根据响应头名称获取请求响应头值列表
	List<String> vals = response.getHeaderValues("Content-Type");

8. 回调函数(可用于成功和失败默认回调):

	   /**
	     * 在异步请求中只能通过OnSuccess<T>回调函数接或Future<T>返回值接受数据。
	     * 而在同步请求中,OnSuccess<T>回调函数和任何类型的返回值都能接受到请求响应的数据。
	     * OnError回调函数可以用于异常处理,一般在同步请求中使用try-catch也能达到同样的效果。
	     */
	    
	    /**
	     * 请求使用成功和失败回调
	     *
	     * 如这两个回调函数的类名所示的含义一样,OnSuccess<T>在请求成功调用响应时会被调用,而OnError在失败或出现错误的时候被调用。
	     * 其中OnSuccess<T>的泛型参数T定义为请求响应返回结果的数据类型。
	     * @param username
	     * @param onSuccess
	     * @param onError
	     * @return
	     */
	    @Request(
	            url = "http://localhost:8080/hello/user",
	            headers = {"Accept:text/plain"},
	            data = "username=${username}"
	    )
	    String sendAndCallback(@DataVariable("username") String username, OnSuccess<String> onSuccess, OnError onError);

回调使用演示:

	testInterface.sendAndCallback("小熙", (String resultString, ForestRequest request, ForestResponse response) -> {
	                    // 成功响应回调(如果是异步,这里可以操作响应数据封装)
	                    System.out.println(resultString);
	                },
	                (ForestRuntimeException ex, ForestRequest request, ForestResponse response) -> {
	                    // 异常回调
	                    System.out.println(ex.getMessage());
	                });

9. 异步请求:

	    /**
	     * 在Forest使用异步请求,可以通过设置@Request注解的async属性为true实现,不设置或设置为false即为同步请求。
	     */
	
	    /**
	     * 异步请求,成功回调(在异步请求中只能通过OnSuccess<T>回调函数接或Future<T>返回值接受数据,所以返回值为void)
	     * @param username
	     * @param onSuccess
	     */
	    @Request(
	            url = "http://localhost:8080/hello/queryUserByName/username",
	            async = true,
	            headers = {"Accept:text/plain"}
	    )
	    void testAsyncGet(@Query("username") String username, OnSuccess<String> onSuccess);
	
	    /**
	     * 异步请求,Future封装接收
	     * @param username
	     * @return
	     */
	    @Request(
	            url = "http://localhost:8080/hello/queryUserByName/username",
	            async = true,
	            headers = {"Accept:text/plain"}
	    )
	    Future<String> testAsyncGet(@Query("username") String username);

调用演示:

	        // 回调异步
	        testInterface.testAsyncGet("小熙", (String resultString, ForestRequest request, ForestResponse response) -> {
	            // 打印成功请求返回的结果
	            System.out.println(resultString);
	        });
	        
	        // future异步
	        Future<String> stringFuture = testInterface.testAsyncGet("小熙");
	        // 获取future异步成功调用的返回结果
	        String string = stringFuture.get();

10. 文件上传:

	    /**
	     * 上传下载
	     * Forest从 1.4.0 版本开始支持多种形式的文件上传和文件下载功能
	     */
	
	    /**
	     * 用@DataFile注解修饰要上传的参数对象
	     * OnProgress 参数为监听上传进度的回调函数
	     * @param filePath
	     * @param onProgress
	     * @return
	     */
	    @Post(url = "/upload")
	    Map testUpload(@DataFile("file") String filePath, OnProgress onProgress);
	
	    /**
	     * File类型对象
	     */
	    @Post(url = "/upload")
	    Map testUpload(@DataFile("file") File file, OnProgress onProgress);
	
	    /**
	     * byte数组
	     * 使用byte数组和Inputstream对象时一定要定义fileName属性
	     */
	    @Post(url = "/upload")
	    Map testUpload(@DataFile(value = "file", fileName = "${1}") byte[] bytes, String filename);
	
	    /**
	     * Inputstream 对象
	     * 使用byte数组和Inputstream对象时一定要定义fileName属性
	     */
	    @Post(url = "/upload")
	    Map testUpload(@DataFile(value = "file", fileName = "${1}") InputStream in, String filename);
	
	    /**
	     * Spring Web MVC 中的 MultipartFile 对象
	     */
	    @PostRequest(url = "/upload")
	    Map testUpload(@DataFile(value = "file") MultipartFile multipartFile, OnProgress onProgress);
	
	    /**
	     * Spring 的 Resource 对象
	     */
	    @Post(url = "/upload")
	    Map testUpload(@DataFile(value = "file") Resource resource);

调用演示:

	Map result = testInterface.testUpload("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!   --------");
	            }
	        });

批量上传:

	   /**
	     * 批量上传
	     */
	
	    /**
	     * 上传Map包装的文件列表
	     * 其中 ${_key} 代表Map中每一次迭代中的键值
	     * @param byteArrayMap
	     * @return
	     */
	    @PostRequest(url = "/upload")
	    ForestRequest<Map> uploadByteArrayMap(@DataFile(value = "file", fileName = "${_key}") Map<String, byte[]> byteArrayMap);
	
	    /**
	     * 上传List包装的文件列表
	     * 其中 ${_index} 代表每次迭代List的循环计数(从零开始计)
	     * @param byteArrayList
	     * @return
	     */
	    @PostRequest(url = "/upload")
	    ForestRequest<Map> uploadByteArrayList(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") List<byte[]> byteArrayList);
	
	    /**
	     * 上传数组包装的文件列表
	     * 其中 ${_index} 代表每次迭代List的循环计数(从零开始计)
	     * @param byteArrayArray
	     * @return
	     */
	    @PostRequest(url = "/upload")
	    ForestRequest<Map> uploadByteArrayArray(@DataFile(value = "file", fileName = "test-img-${_index}.jpg") byte[][] byteArrayArray);
	

11. 文件下载:

		    /**
		     * 在方法上加上@DownloadFile注解
		     * dir属性表示文件下载到哪个目录
		     * filename属性表示文件下载成功后以什么名字保存,如果不填,这默认从URL中取得文件名
		     * OnProgress参数为监听上传进度的回调函数
		     * @param dir
		     * @param filename
		     * @param onProgress
		     * @return
		     */
		    @Get(url = "http://localhost:8080/images/xxx.jpg")
		    @DownloadFile(dir = "${0}", filename = "${1}")
		    File testDownloadFile(String dir, String filename, OnProgress onProgress);
		
		    /**
		     * 如果您不想将文件下载到硬盘上,而是直接在内存中读取,可以去掉@DownloadFile注解,并且用以下几种方式定义接口:
		     */
		
		    /**
		     * 返回类型用byte[],可将下载的文件转换成字节数组
		     * @return
		     */
		    @GetRequest(url = "http://localhost:8080/images/test-img.jpg")
		    byte[] downloadImageToByteArray();
		
		    /**
		     * 返回类型用InputStream,用流的方式读取文件内容
		     * @return
		     */
		    @GetRequest(url = "http://localhost:8080/images/test-img.jpg")
		    InputStream downloadImageToInputStream();

调用演示:

		File file = testInterface.testDownloadFile("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!   --------");
		            }
		        });

四: HTTPS请求:

为保证网络访问安全,现在大多数企业都会选择使用SSL验证来提高网站的安全性。

1. 单向认证:

全局配置可以配置一个全局统一的SSL协议,但现实情况是有很多不同服务(尤其是第三方)的API会使用不同的SSL协议,这种情况需要针对不同的接口设置不同的SSL协议。

    /**
     * 在某个请求接口上通过 sslProtocol 属性设置单向SSL协议
     * @return
     */
    @Get(
            url = "https://localhost:5555/hello/user",
            sslProtocol = "SSL"
    )
    ForestResponse<String> testTruestSSLGet();

    /**
     * 在一个个方法上设置太麻烦,也可以在 @BaseRequest 注解中设置一整个接口类的SSL协议
     */
    @BaseRequest(sslProtocol = "TLS")
    public interface SSLClient {

        /**
         * 类中使用
         * @return
         */
        @Get("https://localhost:5555/hello/user")
        String testTruestSSLGet();

    }

2. 双向认证

在yml中添加证书等配置:

		forest:
		  ...
		  ssl-key-stores:
			- id: keystore1                      # id为该keystore的名称,必填。你可以配置多个key,调用不同key即可
			     file: test.keystore             # 公钥文件地址
			     keystore-pass: 123456           # keystore秘钥
			     cert-pass: 123456               # cert秘钥
			     protocols: SSLv3                # SSL协议  
		    - id: keystore2                      # 第二个keystore
		      file: test2.keystore    
		      keystore-pass: abcdef  
		      cert-pass: abcdef      
		      protocols: SSLv3       
		      ...

接下来在代码中直接引用这个 id 对应的key:

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

五. 后语:

Forest 作为远程之间调用的封装,还有很多功能,这里小熙只是列出了大部分基础的功能,感兴趣的可以去官网看看哟。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐