在Java的天下中,Http客户端之前一贯是Apache家的HttpClient霸占主导,但是由于此包较为弘大,API又比较难用,因此并不适用很多场景。
而新兴的OkHttp、Jodd-http固然好用,但是面对一些场景时,学习本钱还是有一些的。
很多时候,我们想追求轻量级的Http客户端,并且追求大略易用。
而JDK自带的HttpUrlConnection可以知足大部分需求。
Hutool针对此类做了一层封装,使Http要求变得无比大略。

1.Hutool-http先容

Hutool-http针对JDK的HttpUrlConnection做一层封装,简化了HTTPS要求、文件上传、Cookie影象等操作,使Http要求变得无比大略。

Hutool-http的核心集中在两个类:

phptracecallstackJava实现长途挪用计划汇总 HTML

HttpRequestHttpResponse

同时针对大部分情境,封装了HttpUtil工具类。

2.Hutool-http优点根据URL自动判断是要求HTTP还是HTTPS,不须要单独写多余的代码。
表单数据中有File工具时自动转为multipart/form-data表单,不必单独做操作。
默认情形下Cookie自动记录,比如可以实现仿照登录,即第一次访问登录URL后后续要求便是登录状态。
自动识别304跳转并二次要求自动识别页面编码,即根据header信息或者页面中的干系标签信息自动识别编码,最大可能避免乱码。
自动识别并解压Gzip格式返回内容3.Get要求利用办法

// 最大略的HTTP要求,可以自动通过header等信息判断编码,不区分HTTP和HTTPSString result1= HttpUtil.get("https://www.baidu.com");// 当无法识别页面编码的时候,可以自定义要求页面的编码String result2= HttpUtil.get("https://www.baidu.com", CharsetUtil.CHARSET_UTF_8);//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中HashMap<String, Object&gt; paramMap = new HashMap<>();paramMap.put("city", "北京");String result3= HttpUtil.get("https://www.baidu.com", paramMap);

但是在我们日常事情中, 一样平常的调用都是有要求头和要求参数的, 以是重点代码如下:

/ 不带有要求头,带有表单参数的Get要求 @param url @param params @return / public static String getRequestBody(String url, Map<String, Object> params) { return get(url).form(params).execute().body(); } / 带有要求头,不带有表单参数的Get要求 @param url @param headers @return / public static String getRequestHeaders(String url, Map<String, String> headers) { return get(url).addHeaders(headers).execute().body(); } / 带有要求头,表单参数的Get要求 @param url @param headers @param params @return / public static String getRequest(String url, Map<String, String> headers, Map<String, Object> params) { return get(url).addHeaders(headers).form(params).execute().body(); } / 获取Get要求连接工具 默认不打开重定向 @param url @return / public static HttpRequest get(String url) { return HttpUtil.createGet(url); }

上述代码中, 利用hutool工具提炼出一个获取HttpRequest工具的方法, 后续所有方案都在该工具上进行,实质上,HttpUtil中的get和post工具方法都是HttpRequest工具的封装,因此如果想更加灵巧操作Http要求,可以利用HttpRequest。

4.Post要求利用办法

/ @author XiaoSheng @date 2023-11-17 @dec Post要求封装的方法类 /public class PostUtils { / 获取Post要求工具 @param url @return / public static HttpRequest post(String url) { return get(Method.POST, url); } / 创建Http要求工具 Method 是一个列举工具 拥有如下方法: GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE, CONNECT, PATCH @return / public static HttpRequest post(Method method, String url) { return HttpUtil.createPost(url); } / 发送post要求<br> 要求体body参数支持两种类型: <pre> 1. 标准参数,例如 a=1&b=2 这种格式 2. Rest模式,此时body须要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type </pre> @param url 网址 @param body post表单数据 @return 返回数据 / public static String postJSONData(String url, String body) { return post(url).timeout(HttpGlobalConfig.getTimeout()).body(body).execute().body(); } / 发送post要求<br> 要求体body参数支持两种类型: <pre> 1. 标准参数,例如 a=1&b=2 这种格式 2. Rest模式,此时body须要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type </pre> @param url 网址 @param body post表单数据 @return 返回数据 / public static JSONObject postJSONData(String url, Map<String, String> headers, String body) { return postJSONData(url, headers, body, HttpGlobalConfig.getTimeout()); } / 发送post要求<br> 要求体body参数支持两种类型: <pre> 1. 标准参数,例如 a=1&b=2 这种格式 2. Rest模式,此时body须要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type </pre> @param url 网址 @param body post表单数据 @return 返回数据 / public static JSONObject postJSONData(String url, Map<String, String> headers, String body, int timeout) { String respData = post(url).addHeaders(headers).timeout(timeout).body(body).execute().body(); return (JSONObject) JSONObject.parse(respData); } / 发送post要求 @param url 网址 @param paramMap post表单数据 @return 返回数据 / public static String postFormData(String url, Map<String, Object> paramMap) { return postFormData(url, paramMap, HttpGlobalConfig.getTimeout()); } / 发送post要求 @param url 网址 @param paramMap post表单数据 @param timeout 超时时长,-1表示默认超时,单位毫秒 @return 返回数据 @since 3.2.0 / public static String postFormData(String url, Map<String, Object> paramMap, int timeout) { return post(url).form(paramMap).timeout(timeout).execute().body(); }}

Post要求也与上面Get要求一样,我们还是封装了一个获取HttpRequest的post方法,用于后续其他方法的调用。
方法详细的功能都在上述代码注释中进行了比较好的阐述。
下面我们来做个Demo

5.文件下载上传

文件上传

HashMap<String, Object> paramMap = new HashMap<>();//文件上传只需将参数中的键指定(默认file),值设为文件工具即可,对付利用者来说,文件上传与普通表单提交并无差异paramMap.put("file", FileUtil.file("D:\\face.jpg"));String result= HttpUtil.post("https://www.baidu.com", paramMap);

文件下载

由于Hutool-http机制问题,要求页面返回结果是一次性解析为byte[]的,如果要求URL返回结果太大(比如文件下载),那内存会爆掉,因此针对文件下载HttpUtil单独做了封装。
文件下载在面对大文件时采取流的办法读写,内存中只是保留一定量的缓存,然后分块写入硬盘,因此大文件情形下不会对内存有压力。

String fileUrl = "http://mirrors.sohu.com/centos/8.4.2105/isos/x86_64/CentOS-8.4.2105-x86_64-dvd1.iso";//将文件下载后保存在E盘,返回结果为下载文件大小long size = HttpUtil.downloadFile(fileUrl, FileUtil.file("e:/"));System.out.println("Download size: " + size);

当然,如果我们想感知下载进度,还可以利用另一个重载方法回调感知下载进度:

//带进度显示的文件下载HttpUtil.downloadFile(fileUrl, FileUtil.file("e:/"), new StreamProgress(){@Overridepublic void start() {Console.log("开始下载。



");}@Overridepublic void progress(long progressSize) {Console.log("已下载:{}", FileUtil.readableFileSize(progressSize));}@Overridepublic void finish() {Console.log("下载完成!
");}});

当然,工具类供应了一个更加抽象的方法:HttpUtil.download,此方法会要求URL,将返回内容写入到指定的OutputStream中。
利用这个方法,可以更加灵巧的将HTTP内容转换写出,以适应更多场景。

引用文章:

doc.hutool.cn/pages/HttpU…

6.爬取开源中国的开源咨询

这也是我第一次通过Java打仗到爬虫,,详细思路便是通过调用网页内容链接, 要求新内容, 并通过获取到的网页内容(html代码),进行正则表达式的匹配,然后将内容整理输出。
当然,拿到这些内容之后就可以做很多事情了,比如AI剖析,通过邮件发送给自己从而获取到某网站最新最多的新闻资讯。

前期事情: 剖析页面打开红薯家的主页,我们找到最显眼的开源资讯模块,然后点击“更多”,打开“开源资讯”板块。
打开F12调试器,点击快捷键F12打开Chrome的调试器,点击“Network”选项卡,然后在页面上点击“全部资讯”。
由于红薯家的列表页是通过下拉翻页的,因此下拉到底部会触发第二页的加载,此时我们下拉到底部,然后不雅观察调试器中是否有新的要求涌现。
如图,我们创造第二个要求是列表页的第二页。
我们打开这个要求地址,可以看到纯纯的内容。
红框所指地址为第二页的内容,很明显p参数代表了页码page。
我们右键点击后查看源码,可以看到源码。
找到标题部分的HTML源码,然后搜索这个包围标题的HTML部分,看是否可以定位标题。
编写代码

详细代码如下:

//要求列表页String listContent = HttpUtil.get("https://www.oschina.net/action/ajax/get_more_news_list?newsType=&p=2");//利用正则获取所有标题List<String> titles = ReUtil.findAll("<span class=\"text-ellipsis\">(.?)</span>", listContent, 1);for (String title : titles) {//打印标题Console.log(title);}代码剖析

重点: 第一行要求页面内容,第二行正则定位所有标题行并提取标题部分。

ReUtil.findAll方法用于查找所有匹配正则表达式的内容部分,第二个参数1表示提取第一个括号(分组)中的内容,0表示提取所有正则匹配到的内容。
这个方法可以看下core模块中ReUtil章节理解详情。

<span class=\"text-ellipsis\">(.?)</span>这个正则便是我们上面剖析页面源码后得到的正则,个中(.?)表示我们须要的内容,.表示任意字符,表示0个或多个,?表示最短匹配,全体正则的意思便是。
,以<span class=\"text-ellipsis\">开头,</span>结尾的中间所有字符,中间的字符要达到最短。
?的浸染实在便是将范围限定到最小,不然</span>很可能匹配到后面去了。

三,OkHttp远程调用办法

OkHttp 在 Java 和 Android 天下中被广泛利用,深入学习源代码有助于节制软件特性和提高编程水平。

在生产实践中,常常会碰着这样的场景:须要针对某一类 Http 要求做统一的处理,例如在 Header 里添加要求参数或者修正要求相应等等。
这类问题的一种比较优雅的办理方案是利用拦截器来对要乞降相应做统一处理。

在 Android 和 Java 天下里 OkHttp 凭借其高效性和易用性被广泛利用。
作为一款精良的开源 Http 要求框架,深入理解它的实现事理,可以学习精良软件的设计和编码履历,帮助我们更好到地利用它的特性,并且有助于分外场景下的问题排查。
本文考试测验从源代码出发探究 OkHttp 的基本事理,并列举了一个大略的例子解释拦截器在我们项目中的实际运用。

1.OkHttp基本事理

OkHttp 可以用来发送同步或异步的要求,异步要求与同步要求的紧张差异在于异步要求会交由线程池来调度要求的实行。
利用 OkHttp 发送一个同步要求的代码相称简洁,示例代码如下:

同步Get要求:

// 1.创建OkHttpClient客户端OkHttpClient client = new OkHttpClient();public String getSync(String url) throws IOException { OkHttpClient client = new OkHttpClient(); // 2.创建一个Request工具 Request request = new Request.Builder() .url(url) .build(); // 3.创建一个Call工具并调用execute()方法 try (Response response = client.newCall(request).execute()) { return response.body().string(); } }

个中execute()方法是要求发起的入口,RealCall工具的execute()方法的源代码如下:

@Override public Response execute() throws IOException { synchronized (this) { // 同步锁定当前工具,将当前工具标记为“已实行” if (executed) throw new IllegalStateException("Already Executed"); executed = true; } captureCallStackTrace(); // 捕获调用栈 eventListener.callStart(this); // 事宜监听器记录“调用开始”事宜 try { client.dispatcher().executed(this); // 调度器将当前工具放入“运行中”行列步队 Response result = getResponseWithInterceptorChain(); // 通过拦截器发起调用并获取相应 if (result == null) throw new IOException("Canceled"); return result; } catch (IOException e) { eventListener.callFailed(this, e); // 非常时记录“调用失落败事件” throw e; } finally { client.dispatcher().finished(this); // 将当前工具从“运行中”行列步队移除 }}

execute() 方法首先将当前要求标记为“已实行”,然后会为重试跟踪拦截器添加堆栈追踪信息,接着事宜监听器记录“调用开始”事宜,调度器将当前工具放入“运行中”行列步队 ,之后通过拦截器发起调用并获取相应,末了在 finally 块中将当前要求从“运行中”行列步队移除,非常发生时势宜监听器记录“调用失落败”事宜。
个中关键的方法是 getResponseWithInterceptorChain() ,其源代码如下:

Response getResponseWithInterceptorChain() throws IOException { // 构建一个全栈的拦截器列表 List<Interceptor> interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); interceptors.add(retryAndFollowUpInterceptor); interceptors.add(new BridgeInterceptor(client.cookieJar())); interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client)); if (!forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain(interceptors, ……); return chain.proceed(originalRequest); }

该方法中按照特定的顺序创建了一个有序的拦截器列表,之后利用拦截器列表创建拦截器链并发起proceed() 方法调用。
在chain.proceed() 方法中会利用递归的办法将列表中的拦截器串联起来依次对要求工具进行处理。
拦截器链的实现是 OkHttp 的一个奥妙所在,在后文我们会用一小节专门谈论。
在连续往下剖析之前,通过以上的代码片段我们已经大致看到了一个要求发起的整体流程。

2.OkHttp核心实行流程

一个 OkHttp 要求的核心实行过程如以下流程图所示:

OKHttp要求实行流程图

OkHttpClient: 是全体 OkHttp 的核心管理类,从面向工具的抽象表示上来看它代表了客户端本身,是要求的调用工厂,用来发送要乞降读取相应。
在大多数情形下这个类该当是被共享的,由于每个 Client 工具持有自己的连接池和线程池。
重复创建则会造成在空闲池上的资源摧残浪费蹂躏。
Client工具可以通过默认的无参布局方法创建也可以通过 Builder 创建自定义的 Client 工具。
Client 持有的线程池和连接池资源在空闲时可以自动开释无需客户端代码手动开释,在分外情形下也支持手动开释。

1、里面包含了很多工具,实在OKhttp的很多功能模块都包装进这个类,让这个类单独供应对外的API,这种外不雅观模式的设计十分的优雅。
外不雅观模式。

2、而内部模块比较多,就利用了Builder模式(建造器模式)。
Builder模式(建造器模式)

3、它的方法只有一个:newCall.返回一个Call工具(一个准备好了的可以实行和取消的要求)。

Request: 一个 Request 工具代表了一个 Http 要求。
它包含了要求地址 url,要求方法类型 method ,要求头 headers,要求体 body 等属性,该工具具有的属性普遍利用了 final 关键字来润色,正如该类的解释文档中所述,当这个类的 body 为空或者 body 本身是不可变工具时,这个类是一个不可变工具。
Response: 一个 Response 工具代表了一个 Http 相应。
这个实例工具是一个不可变工具,只有 responseBody 是一个可以一次性利用的值,其他属性都是不可变的。
1、Request、Response分别抽象成要乞降相应2、个中Request包括Headers和RequestBody,而RequestBody是abstract的,他的子类是有FormBody (表单提交的)和 MultipartBody(文件上传),分别对应了两种不同的MIME类型 FormBody :"application/x-www-form-urlencoded" MultipartBody:"multipart/"+xxx.3、个中Response包括Headers和RequestBody,而ResponseBody是abstract的,以是他的子类也是有两个:RealResponseBody和CacheResponseBody,分别代表真实相应和缓存相应。
4、由于RFC协议规定,以是所有的头部信息不是随便写的,request的header与response的header的标准都不同。
详细的见 List of HTTP header fields。
OKHttp的封装类Request和Response为了运用程序编程方便,会把一些常用的Header信息专门提取出来,作为局部变量。
比如contentType,contentLength,code,message,cacheControl,tag...它们实在都因此name-value对的形势,存储在网络要求的头部信息中。
RealCall: 一个 RealCall 工具代表了一个准备好实行的要求调用。
它只能被实行一次。
同时卖力了调度和任务链组织的两大重任。
1、OkHttpClient的newCall方法里面new了RealCall的工具,但是RealCall的布局函数须要传入一个OKHttpClient工具和Request工具(PS:第三个参数false表示不是webSokcet).因此RealCall包装了Request工具。
以是RealCall可以很方便地利用这两个工具。
2、RealCall里面的两个关键方法是:execute 和 enqueue。
分别用于同步和异步得实行网络要求。
3、RealCall还有一个主要方法是:getResponseWithInterceptorChain,添加拦截器,通过拦截器可以将一个流式事情分解为可配置的分段流程,既增加了灵巧性也实现理解耦,关键还可以自有配置,非常完美。
Dispatcher: 调度器。
它决定了异步调用何时被实行,内部利用 ExecutorService 调度实行,支持自定义 Executor。
EventListener: 事宜监听器。
抽象类 EventListener 定义了在一个要求生命周期中记录各种事宜的方法,通过监听各种事宜,可以用来捕获运用程序 HTTP 要求的实行指标。
从而监控 HTTP 调用的频率和性能。
Interceptor: 拦截器。
对应了软件设计模式中的拦截器模式,拦截器可用于改变、增强软件的常规处理流程,该模式的核心特色是对软件系统的改变是透明的和自动的。
OkHttp 将全体要求的繁芜逻辑拆分成多个独立的拦截器实现,通过任务链的设计模式将它们串联到一起,完成发送要求获取相应结果的过程。
3.OkHttp整体架构

通过进一步阅读 OkHttp 源码,可以看到 OkHttp 是一个分层的构造。
软件分层是繁芜系统设计的常用手段,通过分层可以将繁芜问题划分成规模更小的子问题,分而治之。
同时分层的设计也有利于功能的封装和复用。
OkHttp 的架构可以分为:运用接口层,协议层,连接层,缓存层,I/O层。
不同的拦截器为各个层次的处理供应调用入口,拦截器通过任务链模式串联成拦截器链,从而完成一个Http要求的完全处理流程。
如下图所示:

4.OkHttp拦截器的种类和浸染

OkHttp 的核心功能是通过拦截器来实现的,各种拦截器的浸染分别为:

client.interceptors: 由开拓者设置的拦截器,会在所有的拦截器处理之提高行最早的拦截处理,可用于添加一些公共参数,如自定义 header、自定义 log 等等。
RetryAndFollowUpInterceptor:紧张卖力进行重试和重定向的处理。
BridgeInterceptor:紧张卖力要乞降相应的转换。
把用户布局的 request 工具转换成发送到做事器 request工具,并把做事器返回的相应转换为对用户友好的相应。
CacheInterceptor:紧张卖力缓存的干系处理,将 Http 的要求结果放到到缓存中,以便不才次进行相同的要求时,直接从缓存中读取结果,提高相应速率。
ConnectInterceptor:紧张卖力建立连接,建立 TCP 连接或者 TLS 连接。
client.networkInterceptors:由开拓者设置的拦截器,实质上和第一个拦截器类似,但是由于位置不同,以是用途也不同。
CallServerInterceptor:紧张卖力网络数据的要乞降相应,也便是实际的网络I/O操作。
将要求头与要求体发送给做事器,以及解析做事器返回的response。

除了框架供应的拦截器外,OkHttp 支持用户自定义拦截器来对要求做增强处理,自定义拦截器可以分为两类,分别是运用程序拦截器和网络拦截器,他们发挥浸染的层次构造如下图:

不同的拦截器有不同的适用场景,他们各自的优缺陷如下:

运用程序拦截器无需担心重定向和重试等中间相应。
总是被调用一次,纵然 HTTP 相应是从缓存中供应的。
可以不雅观察到运用程序的原始要求。
不关心 OkHttp 注入的标头。
许可短路而不调用 Chain.proceed()方法。
许可重试并多次调用 Chain.proceed()方法。
可以利用 withConnectTimeout、withReadTimeout、withWriteTimeout 调度呼叫超时。
网络拦截器能够对重定向和重试等中间相应进行操作缓存相应不会调用可以不雅观察到通过网络传输的原始数据可以访问携带要求的链接5.任务链模式串联拦截器调用

OkHttp 内置了 5 个核心的拦截器用来完成要求生命周期中的关键处理,同时它也支持添加自定义的拦截器来增强和扩展 Http 客户端,这些拦截器通过任务链模式串联起来,使得的要求可以在不同拦截器之间流转和处理。

任务链

适用场景 包括:

当程序须要利用不同办法处理不同种类的要求时当程序必须按顺序实行多个处理者时当所须要的处理者及其顺序必须在运行时进行改变时

优点:

可以掌握要求处理的顺序可对发起操作和实行操作的类进行解耦。
可以在不变动现有代码的情形下在程序中新增处理者。
拦截器的串联

任务链的入口从第一个 RealInterceptorChain 工具的 proceed() 方法调用开始。
这个方法的设计非常奥妙,在完全的 proceed() 方法里会做一些更为严谨的校验,去掉这些校验后该方法的核心代码如下:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException { if (index >= interceptors.size()) throw new AssertionError(); // …… // Call the next interceptor in the chain. RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); // …… return response; }

这段代码可以算作三个步骤:

索引判断。
index 初始值为0,它指示了拦截器工具在列表中的索引顺序,每实行一次 proceed 方法该参数自增1,当索引值大于拦截器列表的索引下标时非常退出。
创建下一个任务链工具。
按照索引顺序获取一个拦截器,并调用 intercept() 方法。
任务链串联

单独看这个方法彷佛并不能将所有拦截器都串联起来,串联的关键在于 intercept() 方法,intercept() 方法是实现 interceptor 接口时必须要实现的方法,该方法持有下一个任务链 工具 chain,在拦截器的实现类里只须要在 intercept() 方法里的适当地方再次调用 chain.proceed() 方法,程序指令便会重新回到以上代码片段,于是就可以触发对付下一个拦截器的查找和调用了,在这个过程中拦截器工具在列表中的先后顺序非常主要,由于拦截器的调用顺序便是其在列表中的索引顺序。

递归方法

从另一个角度来看,proceed() 方法可以算作是一个递归方法。
递归方法的基本定义为“函数的定义中调用函数自身”,虽然 proceed() 方法没有直接调用自身,但是除了末了一个拦截器以外,拦截器链中的其他拦截器都会在适当的位置调用 chain.proceed() 方法,任务链工具和拦截器工具合在一起则组成了一个自己调用自己的逻辑循环。
按照笔者个人理解,这也是为什么源代码里 Chain 接口被设计成 Interceptor 接口的内部接口,在理解这段代码的时候要把它们两个接口当成一个整体来看,从这样的角度看的话,这样的接口设计是符合“高内聚”的原则的。

拦截器 interceptor 和任务链 chain 的关系如下图:

markdown复制代码Interceptor 和 Chain 的关系图6.OkHttp拦截器在项目中的运用

在我们的项目中,有一类要求须要在要求头 Header 中添加认证信息,利用拦截器来实现可以极大地简化代码,提高代码可读性和可掩护性。
核心代码只须要实现符合业务须要的拦截器如下:

添加要求头的拦截器

public class EncryptInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request originRequest = chain.request(); // 打算认证信息 String authorization = this.encrypt(originRequest); // 添加要求头 Request request = originRequest.newBuilder() .addHeader("Authorization", authorization) .build(); // 向任务链后面通报 return chain.proceed(request); }}

OkHttp的拦截器链可谓是其全体框架的精髓,用户可传入的 interceptor 分为两类: ①一类是全局的 interceptor,该类 interceptor 在全体拦截器链中最早被调用,通过 OkHttpClient.Builder#addInterceptor(Interceptor) 传入; ②其余一类是非网页要求的 interceptor ,这类拦截器只会在非网页要求中被调用,并且是在组装完要求之后,真正发起网络要求前被调用,所有的 interceptor 被保存在 List<Interceptor> interceptors 凑集中,按照添加顺序来逐个调用,详细可参考 RealCall#getResponseWithInterceptorChain() 方法。
通过 OkHttpClient.Builder#addNetworkInterceptor(Interceptor) 传入;

7.OkHttp要求利用示例get要求办法

package cn.org.xiaosheng.okhttp.utils;import com.sun.istack.internal.NotNull;import lombok.extern.slf4j.Slf4j;import okhttp3.;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import javax.annotation.Resource;import java.io.IOException;import java.util.Map;import java.util.Objects;/ @author XiaoSheng @date 2023-11-20 @dec 利用OkHttp作为Get要求的工具类底座 /@Component@Slf4jpublic class GetOkHttpUtils { @Resource private OkHttpClient okHttpClient; @Resource(name = "buildOkHttpClient") @Autowired private OkHttpClient buildOkHttpClient; / Get要求, 并供应header参数入参 同步调用 @param url @param headers @param params @return / public String getRequest(String url, Map<String, String> headers, Map<String, String> params) { if (params != null && !params.isEmpty()) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } // 删除末了一个拼接符号& sb.deleteCharAt(sb.length() - 1); url = url + "?" + sb.toString(); } Request.Builder reqBuild = new Request.Builder().url(url); //if (headers != null && !headers.isEmpty()) { // for (int i = 0; i < headers.size(); i++) { // reqBuild.addHeader(Objects.toString(headers.keySet().toArray()[i]), // Objects.toString(headers.values().toArray()[i])); // } //} // client.newCall(request)会返回一个Call工具,通过调用Call工具的execute方法就会实行同步要求,并返回一个Response工具 // 对付同步调用,我们可以利用try代码块,编译器会帮我们在隐含的finally代码块中调用close方法。
try (Response response = buildOkHttpClient.newCall(reqBuild.build()).execute()) { if (!response.isSuccessful()) { throw new IOException("Unexpected code: " + response); } // 打印相应体, 把稳response.body().string() 调用一次之后Okio流将会关闭 // OkHttpClient的返回值调用两次导致流已经关闭 会涌现 java.lang.IllegalStateException: closed缺点! String respData = response.body().string(); System.out.println(respData); return respData; } catch (IOException e) { e.printStackTrace(); } return null; } / Get要求, 并供应header参数入参 异步调用 @param url @param headers @param params @return / public String getRequestAsyn(String url, Map<String, String> headers, Map<String, String> params) { if (params != null && !params.isEmpty()) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : params.entrySet()) { sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); } // 删除末了一个拼接符号& sb.deleteCharAt(sb.length() - 1); url = url + "?" + sb.toString(); } Request.Builder reqBuild = new Request.Builder().url(url); if (headers != null && !headers.isEmpty()) { for (int i = 0; i < headers.size(); i++) { reqBuild.addHeader(Objects.toString(headers.keySet().toArray()[i]), Objects.toString(headers.values().toArray()[i])); } } // client.newCall(request)会返回一个Call工具,通过调用Call工具的execute方法就会实行同步要求,并返回一个Response工具 // 对付异步调用 okHttpClient.newCall(reqBuild.build()).enqueue(new Callback() { @Override public void onFailure(@NotNull Call call, @NotNull IOException e) { // 非主线程 e.printStackTrace(); } @Override public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { try (ResponseBody responseBody = response.body()) { if (!response.isSuccessful()) { throw new IOException("Unexpected code " + response); } // 把稳避坑! OkHttpClient的返回值调用两次导致流已经关闭 会涌现 java.lang.IllegalStateException: closed缺点! System.out.println(responseBody.string()); } } }); return null; }}
post要求办法

package cn.org.xiaosheng.okhttp.utils;import com.alibaba.fastjson.JSONObject;import okhttp3.;import org.springframework.stereotype.Component;import javax.annotation.Resource;import javax.naming.ldap.PagedResultsControl;import java.io.File;import java.io.IOException;import java.util.HashMap;import java.util.Objects;/ @author XiaoSheng @date 2023-11-20 @dec 利用OkHttp作为POST要求的工具类底座 /@Componentpublic class PostOkHttpUtils { public static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json;charset=utf-8"); public static final MediaType MEDIA_TYPE_MULTIPART = MediaType.parse("multipart/form-data;charset=utf-8"); @Resource private OkHttpClient okHttpClient; / Post要求(以表单形式提交参数) 通过FormBody.Builder我们可以构建表单参数形式的要求体(类似于HTML中的<form>标签) 键值对将以兼容HTML表单的办法进行编码。
@param url @param params @param headers @return / public JSONObject postOkHttpByFormData(String url, HashMap<String, Object> params, HashMap<String, String> headers) { Request.Builder reqBuild = new Request.Builder().url(url); // for循环添加要求头 if (headers != null && !headers.isEmpty()) { for (int i = 0; i < headers.size(); i++) { reqBuild.addHeader(Objects.toString(headers.keySet().toArray()[i]), Objects.toString(headers.values().toArray()[i])); } } // 循环添加要求体参数 if (params != null && !params.isEmpty()) { FormBody.Builder formBody = new FormBody.Builder(); for (int i = 0; i < params.size(); i++) { formBody.add(Objects.toString(params.keySet().toArray()[i]), Objects.toString(params.values().toArray()[i])); } reqBuild.post(formBody.build()); } else { return null; } try (Response response = okHttpClient.newCall(reqBuild.build()).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); String respData = response.body().string(); return (JSONObject) JSONObject.parse(respData); } catch (Exception e) { e.printStackTrace(); } return null; } / 提交文件 (以表单形式提交参数) @param url @param mediaType @param file @param headers @return / public JSONObject postOkHttpByFormFile(String url, MediaType mediaType, File file, HashMap<String, String> headers){ Request.Builder reqBuild = new Request.Builder().url(url); if (headers != null && !headers.isEmpty()) { for (int i = 0; i < headers.size(); i++) { reqBuild.addHeader(Objects.toString(headers.keySet().toArray()[i]), Objects.toString(headers.values().toArray()[i])); } } reqBuild.post(RequestBody.create(mediaType, file)); try (Response response = okHttpClient.newCall(reqBuild.build()).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); String string = response.body().string(); return (JSONObject) JSONObject.parse(string); } catch (Exception e) { e.printStackTrace(); } return null; } / Post要求(以MultiPart的格式提交文件,分块要求) / //public JSONObject postOkHttpByMultipartFile(String url, // HashMap<String, String> params, // File file, // HashMap<String, String> headers) { // // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image // RequestBody requestBody = new MultipartBody.Builder() // .setType(MultipartBody.FORM) // .addFormDataPart("title", "Square Logo") // .addFormDataPart("image", "logo-square.png", // RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png"))) // .build(); // // Request request = new Request.Builder() // .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID) // .url("https://api.imgur.com/3/image") // .post(requestBody) // .build(); // // try (Response response = client.newCall(request).execute()) { // if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); // // System.out.println(response.body().string()); // } //} / post要求办法 JSON通报格式 @param url @param params @param headers @return / public JSONObject postOkHttpByJson(String url, HashMap<String, String> params, HashMap<String, String> headers) { return postOkHttp(url, MEDIA_TYPE_JSON, JSONObject.toJSONString(params), headers); } / 通过OkHttp,以Post要求办法,要求第三方接口 @param url @param mediaType @param params @param headers @return / public JSONObject postOkHttp(String url, MediaType mediaType, String params, HashMap<String, String> headers){ Request.Builder reqBuild = new Request.Builder().url(url); if (headers != null && !headers.isEmpty()) { for (int i = 0; i < headers.size(); i++) { reqBuild.addHeader(Objects.toString(headers.keySet().toArray()[i]), Objects.toString(headers.values().toArray()[i])); } } reqBuild.post(RequestBody.create(mediaType, params)); try (Response response = okHttpClient.newCall(reqBuild.build()).execute()) { if (!response.isSuccessful()) throw new IOException("Unexpected code " + response); String string = response.body().string(); return (JSONObject) JSONObject.parse(string); } catch (Exception e) { e.printStackTrace(); } return null; }}

总结: OKHttp曾经作为Android的通信底层,,效率还是很明显的,同时其拦截器让我们可以在要求之前很优雅的做很多通用途理。

参考文章:

juejin.cn/post/723432… www.jianshu.com/p/82f74db14… juejin.cn/post/713834… blog.csdn.net/z372574152/… www.jianshu.com/p/da4a806e5… square.github.io/okhttp/ zhuanlan.zhihu.com/p/630292370 www.cnblogs.com/fnlingnzb-l…

四,RestTemplate远程调用办法1.RestTemplate技能简介

RestTemplate是从 Spring3.0 开始支持的一个 HTTP 要求工具,它供应了常见的REST要求方案的模版,例如 GET 要求、POST 要求、PUT 要求、DELETE 要求以及一些通用的要求实行方法 exchange 以及 execute。

RestTemplate是一个实行HTTP要求的同步壅塞式工具类,它仅仅只是在 HTTP 客户端库(例如 JDK HttpURLConnection,Apache HttpComponents,okHttp 等)根本上,封装了更加大略易用的模板方法 API,方便程序员利用已供应的模板方法发起网络要乞降处理,能很大程度上提升我们的开拓效率。

RestTemplate能大幅简化了提交表单数据的难度,并且附带了自动转换JSON数据的功能,但只有理解了HttpEntity的组成构造(header与body),且理解了与uriVariables之间的差异,才能真正节制其用法。
这一点在Post要求更加突出,下面会先容到。

该类的入口紧张是根据HTTP的六个方法制订:

HTTP method

RestTemplate methods

DELETE

delete

GET

getForObject

getForEntity

HEAD

headForHeaders

OPTIONS

optionsForAllow

POST

postForLocation

postForObject

PUT

put

any

exchange

execute

此外,exchange和excute可以通用上述方法。

在内部,RestTemplate默认利用HttpMessageConverter实例将HTTP转换成POJO或者从POJO转换成HTTP。
默认情形下会注册主mime类型的转换器,但也可以通过setMessageConverters注册其他的转换器。
(实在这点在利用的时候是察觉不到的,很多方法有一个responseType 参数,它让你传入一个相应体所映射成的工具,然后底层用HttpMessageConverter将其做映射)。

HttpMessageConverterExtractor<T> responseExtractor =new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);

HttpMessageConverter.java源码

public interface HttpMessageConverter<T> { //指示此转换器是否可以读取给定的类。
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType); //指示此转换器是否可以写给定的类。
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType); //返回List<MediaType>List<MediaType> getSupportedMediaTypes(); //读取一个inputMessageT read(Class<? extends T> clazz, HttpInputMessage inputMessage)throws IOException, HttpMessageNotReadableException; //往output message写一个Objectvoid write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)throws IOException, HttpMessageNotWritableException;}

在内部,RestTemplate默认利用SimpleClientHttpRequestFactory和DefaultResponseErrorHandler来分别处理HTTP的创建和缺点,但也可以通过setRequestFactory和setErrorHandler来覆盖。

2.开拓配置非Spring环境下利用RestTempate

如果当前项目不是Spring项目,加入spring-web包,即可引入RestTemplate类

xml复制代码<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>5.2.6.RELEASE</version></dependency>

编写一个单元测试类,利用RestTemplate发送一个GET要求,看见地式运行是否正常。

@Testpublic void simpleTest() { RestTemplate restTemplate = new RestTemplate(); String url = "http://jsonplaceholder.typicode.com/posts/1"; String str = restTemplate.getForObject(url, String.class); System.out.println(str);}Spring环境下利用RestTemplate

如果当前项目是SpringBoot,添加如下依赖接口!

xml复制代码<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>

同时,将RestTemplate配置初始化为一个Bean

@Configurationpublic class RestTemplateConfig { / 没有实例化RestTemplate时,初始化RestTemplate @return / @ConditionalOnMissingBean(RestTemplate.class) @Bean public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(); return restTemplate; }}

把稳,这种初始化方法,是利用了JDK自带的HttpURLConnection作为底层HTTP客户端实现。

当然,我们还可以修正RestTemplate默认的客户端,例如将其改成HttpClient客户端,办法如下:

@Configurationpublic class RestTemplateConfig { @ConditionalOnMissingBean(RestTemplate.class) @Bean public RestTemplate restTemplate(){ RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory()); return restTemplate; } / 利用HttpClient作为底层客户端 @return / private ClientHttpRequestFactory getClientHttpRequestFactory() { int timeout = 5000; RequestConfig config = RequestConfig.custom() .setConnectTimeout(timeout) .setConnectionRequestTimeout(timeout) .setSocketTimeout(timeout) .build(); CloseableHttpClient client = HttpClientBuilder .create() .setDefaultRequestConfig(config) .build(); return new HttpComponentsClientHttpRequestFactory(client); }}

在须要利用RestTemplate的位置,注入并利用即可!

@Autowiredprivate RestTemplate restTemplate;

从开拓职员的反馈,和网上的各种HTTP客户端性能以及易用程度评测来看,OkHttp 优于 Apache的HttpClient、Apache的HttpClient优于HttpURLConnection。

/ 利用OkHttpClient作为底层客户端 @return /private ClientHttpRequestFactory getClientHttpRequestFactory(){ OkHttpClient okHttpClient = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .writeTimeout(5, TimeUnit.SECONDS) .readTimeout(5, TimeUnit.SECONDS) .build(); return new OkHttp3ClientHttpRequestFactory(okHttpClient);}3.配置测试-Spring环境下增加线程号

利用RestTemplate调用远程接口时,有时须要在header中通报信息,比如:traceId,source等,便于在查询日志时能够串联一次完全的要求链路,快速定位问题。
这种业务场景就能通过ClientHttpRequestInterceptor接口实现,详细做法如下

第一步,定义一个LogFilter拦截所有接口要求,在MDC中设置traceId:

public class LogFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { MDC.put("TRACE_ID",UUID.randomUUID().toString()); System.out.println("记录要求日志"); chain.doFilter(request, response); System.out.println("记录相应日志"); } @Override public void destroy() { }}

第二步,实现ClientHttpRequestInterceptor接口,MDC中获取当前要求的traceId,然后设置到header中

public class RestTemplateInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { request.getHeaders().set("traceId", MDC.get("TRACE_ID")); ClientHttpResponse response = execution.execute(request, body); return response; }}

第三步,定义配置类,配置上面定义的RestTemplateInterceptor类:

@Configurationpublic class RestTemplateConfiguration { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(restTemplateInterceptor())); return restTemplate; } @Bean public RestTemplateInterceptor restTemplateInterceptor() { return new RestTemplateInterceptor(); }}

能利用MDC保存traceId等参数的根本缘故原由是,用户要求到运用做事器,Tomcat会从线程池等分配一个线程去处理该要求。
那么该要求的全体过程中,保存到MDC的ThreadLocal中的参数,也是该线程独享的,以是不会有线程安全问题

4.Get要求实践

通过RestTemplate发送HTTP GET协议要求,常常利用到的方法有两个:

getForObject():返回值是HTTP协议的相应体getForEntity():返回的是ResponseEntity,ResponseEntity是对HTTP相应的封装,除了包含相应体,还包含HTTP状态码、contentType、contentLength、Header等信息

在Spring Boot环境下写一个单元测试用例,首先创建一个Api接口,然后编写单元测试进行做事测试。

不带参get要求

@RestControllerpublic class TestController { / 不带参的get要求 @return / @RequestMapping(value = "testGet", method = RequestMethod.GET) public ResponseBean testGet(){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("要求成功,方法:testGet"); return result; }}public class ResponseBean { private String code; private String msg; 省去getset方法 }@Autowiredprivate RestTemplate restTemplate;/ 单元测试(不带参的get要求) /@Testpublic void testGet(){ //要求地址 String url = "http://localhost:8080/testGet"; //发起要求,直接返回工具 ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class); System.out.println(responseBean.toString());}带参的get要求(利用占位符号传参)

@RestControllerpublic class TestController { / 带参的get要求(restful风格) @return / @RequestMapping(value = "testGetByRestFul/{id}/{name}", method = RequestMethod.GET) public ResponseBean testGetByRestFul(@PathVariable(value = "id") String id, @PathVariable(value = "name") String name){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("要求成功,方法:testGetByRestFul,要求参数id:" + id + "要求参数name:" + name); return result; }}

@Autowiredprivate RestTemplate restTemplate; / 单元测试(带参的get要求) /@Testpublic void testGetByRestFul(){ //要求地址 String url = "http://localhost:8080/testGetByRestFul/{1}/{2}"; //发起要求,直接返回工具(restful风格) ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, "001", "张三"); System.out.println(responseBean.toString());}带参的get要求(restful风格)

@RestControllerpublic class TestController { / 带参的get要求(利用占位符号传参) @return / @RequestMapping(value = "testGetByParam", method = RequestMethod.GET) public ResponseBean testGetByParam(@RequestParam("userName") String userName, @RequestParam("userPwd") String userPwd){ ResponseBean result = new ResponseBean(); result.setCode("200"); result.setMsg("要求成功,方法:testGetByParam,要求参数userName:" + userName + ",userPwd:" + userPwd); return result; }}@Autowiredprivate RestTemplate restTemplate; / 单元测试(带参的get要求) /@Testpublic void testGetByParam(){ //要求地址 String url = "http://localhost:8080/testGetByParam?userName={userName}&userPwd={userPwd}"; //要求参数 Map<String, String> uriVariables = new HashMap<>(); uriVariables.put("userName", "唐三藏"); uriVariables.put("userPwd", "123456"); //发起要求,直接返回工具(带参数要求) ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, uriVariables); System.out.println(responseBean.toString());}getForEntity利用示例

上面的所有的getForObject要求传参方法,getForEntity都可以利用,利用方法上也险些是同等的,只是在返回结果吸收的时候略有差别。

利用ResponseEntity<T> responseEntity来吸收相应结果。
用responseEntity.getBody()获取相应体。

/ 单元测试 /@Testpublic void testAllGet(){ //要求地址 String url = "http://localhost:8080/testGet"; //发起要求,返回全部信息 ResponseEntity<ResponseBean> response = restTemplate.getForEntity(url, ResponseBean.class); // 获取相应体 System.out.println("HTTP 相应body:" + response.getBody().toString()); // 以下是getForEntity比getForObject多出来的内容 HttpStatus statusCode = response.getStatusCode(); int statusCodeValue = response.getStatusCodeValue(); HttpHeaders headers = response.getHeaders(); System.out.println("HTTP 相应状态:" + statusCode); System.out.println("HTTP 相应状态码:" + statusCodeValue); System.out.println("HTTP Headers信息:" + headers);}header设置参数

//要求头HttpHeaders headers = new HttpHeaders();headers.add("token", "123456789");//封装要求头HttpEntity<MultiValueMap<String, Object>> formEntity = new HttpEntity<>(headers);ResponseEntity<Map> exchange = restTemplate.exchange('要求的url', HttpMethod.GET, formEntity, Map.class);工程工具类

package cn.org.xiaosheng.resttemplate.utils;import cn.hutool.core.collection.CollUtil;import com.alibaba.fastjson.JSONObject;import org.apache.http.protocol.HTTP;import org.springframework.http.HttpEntity;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpMethod;import org.springframework.http.ResponseEntity;import org.springframework.stereotype.Component;import org.springframework.web.client.RestTemplate;import javax.annotation.Resource;import java.util.Map;/ @author XiaoSheng @date 2023-11-19 @dec 利用RestTemplate的Get要求工具类 /@Componentpublic class GetUtils { @Resource private RestTemplate restTemplate; / 通过GET要求调用第三方平台的API @param url 要求接口路径 @param params 要求参数凑集 @return / public ResponseEntity<JSONObject> getRequest(String url, Map<String, Object> headers, Map<String, Object> params ) { return requestObject(url, HttpMethod.GET, params, headers); } / 通过GET要求调用第三方平台的API @param url 要求接口路径 @param params 要求参数凑集 @return / public JSONObject getRequest(String url, Map<String, Object> params) { // 1. 设置要求头 HttpHeaders httpHeaders = new HttpHeaders(); HttpEntity<Object> entity = new HttpEntity<>(params, httpHeaders); JSONObject forObject = restTemplate.getForObject(url, JSONObject.class, params); return forObject; } / @param url url 写成 "http://localhost:8080/testGet2?name={name}&age={age}"; 后面参数标明, 利用{}与凑集中的参数名对应并进行参数绑定 @param params @return / public JSONObject getRequestByUrl(String url, Map<String, Object> params) { return restTemplate.getForObject(url, JSONObject.class, params); } / 通过GET要求调用第三方平台的API @param url 要求接口路径 @param params 要求参数凑集 exchange办法适用于get, post要求, 需指定详细的要求方法 @return / public ResponseEntity<JSONObject> requestObject(String url, HttpMethod method, Map<String, Object> params, Map<String, Object> headers) { // 1. 设置要求头 HttpHeaders httpHeaders = new HttpHeaders(); if (CollUtil.isNotEmpty(headers)) { for (String key : headers.keySet()) { httpHeaders.set(key, headers.get(key).toString()); } } System.out.println(httpHeaders); HttpEntity<Object> entity = new HttpEntity<>(httpHeaders); ResponseEntity<JSONObject> exchange = restTemplate.exchange(url, method, entity, JSONObject.class, params); return exchange; }}5.POST要求

实在POST要求方法和GET要求方法上大同小异,RestTemplate的POST要求也包含两个紧张方法:

postForObject():返回body工具postForEntity():返回全部的信息仿照表单要求

@Autowiredprivate RestTemplate restTemplate;/ 仿照表单提交,post要求 /@Testpublic void testPostByForm(){ //要求地址 String url = "http://localhost:8080/testPostByForm"; // 要求头设置,x-www-form-urlencoded格式的数据 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); //提交参数设置 MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("userName", "唐三藏"); map.add("userPwd", "123456"); // 组装要求体 HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers); //发起要求 ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class); System.out.println(responseBean.toString());}仿照JSON要求

@Autowiredprivate RestTemplate restTemplate;/ 仿照JSON提交,post要求 /@Testpublic void testPostByJson(){ //要求地址 String url = "http://localhost:8080/testPostByJson"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //发送post要求,并打印结果,以String类型吸收相应结果JSON字符串 ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class); System.out.println(responseBean.toString());}仿照页面重定向

@Autowiredprivate RestTemplate restTemplate;/ 重定向,post要求 /@Testpublic void testPostByLocation(){ //要求地址 String url = "http://localhost:8080/testPostByLocation"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //用于提交完成数据之后的页面跳转,返回跳转url URI uri = restTemplate.postForLocation(url, request); System.out.println(uri.toString());}输出结果如下:http://localhost:8080/index.htmlPUT要求

@Autowiredprivate RestTemplate restTemplate;/ 仿照JSON提交,put要求 /@Testpublic void testPutByJson(){ //要求地址 String url = "http://localhost:8080/testPutByJson"; //入参 RequestBean request = new RequestBean(); request.setUserName("唐三藏"); request.setUserPwd("123456789"); //仿照JSON提交,put要求 restTemplate.put(url, request);}DELETE要求

@Autowiredprivate RestTemplate restTemplate;/ 仿照JSON提交,delete要求 /@Testpublic void testDeleteByJson(){ //要求地址 String url = "http://localhost:8080/testDeleteByJson"; //仿照JSON提交,delete要求 restTemplate.delete(url);}通用要求方法exchange

如果以上方法还不知足你的哀求。
在RestTemplate工具类里面,还有一个exchange通用协议要求方法,它可以发送GET、POST、DELETE、PUT、OPTIONS、PATCH等等HTTP方法要求。

打开源码,我们可以很清晰的看到这一点。

在RestTemplate类中搜索exchange方法,然后进入HttpMethod类,就能够看到其支持的所有方法了。

采取exchange方法,可以知足各种场景下的要求操作。

6.文件操作文件上传

@RestControllerpublic class FileUploadController { private static final String UPLOAD_PATH = "/springboot-frame-example/springboot-example-resttemplate/"; / 文件上传 @param uploadFile @return / @RequestMapping(value = "upload", method = RequestMethod.POST) public ResponseBean upload(@RequestParam("uploadFile") MultipartFile uploadFile, @RequestParam("userName") String userName) { // 在 uploadPath 文件夹中通过用户名对上传的文件归类保存 File folder = new File(UPLOAD_PATH + userName); if (!folder.isDirectory()) { folder.mkdirs(); } // 对上传的文件重命名,避免文件重名 String oldName = uploadFile.getOriginalFilename(); String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf(".")); //定义返回视图 ResponseBean result = new ResponseBean(); try { // 文件保存 uploadFile.transferTo(new File(folder, newName)); result.setCode("200"); result.setMsg("文件上传成功,方法:upload,文件名:" + newName); } catch (IOException e) { e.printStackTrace(); result.setCode("500"); result.setMsg("文件上传失落败,方法:upload,要求文件:" + oldName); } return result; }}@Autowiredprivate RestTemplate restTemplate;/ 文件上传,post要求 /@Testpublic void upload(){ //须要上传的文件 String filePath = "/Users/panzhi/Desktop/Jietu20220205-194655.jpg"; //要求地址 String url = "http://localhost:8080/upload"; // 要求头设置,multipart/form-data格式的数据 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); //提交参数设置 MultiValueMap<String, Object> param = new LinkedMultiValueMap<>(); param.add("uploadFile", new FileSystemResource(new File(filePath))); //做事端如果接管额外参数,可以通报 param.add("userName", "张三"); // 组装要求体 HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(param, headers); //发起要求 ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class); System.out.println(responseBean.toString());}文件下载

@RestControllerpublic class FileUploadController { private static final String UPLOAD_PATH = "springboot-frame-example/springboot-example-resttemplate/"; / 带参的get要求(restful风格) @return / @RequestMapping(value = "downloadFile/{userName}/{fileName}", method = RequestMethod.GET) public void downloadFile(@PathVariable(value = "userName") String userName, @PathVariable(value = "fileName") String fileName, HttpServletRequest request, HttpServletResponse response) throws Exception { File file = new File(UPLOAD_PATH + userName + File.separator + fileName); if (file.exists()) { //获取文件流 FileInputStream fis = new FileInputStream(file); //获取文件后缀(.png) String extendFileName = fileName.substring(fileName.lastIndexOf('.')); //动态设置相应类型,根据前台通报文件类型设置相应类型 response.setContentType(request.getSession().getServletContext().getMimeType(extendFileName)); //设置相应头,attachment表示以附件的形式下载,inline表示在线打开 response.setHeader("content-disposition","attachment;fileName=" + URLEncoder.encode(fileName,"UTF-8")); //获取输出流工具(用于写文件) OutputStream os = response.getOutputStream(); //下载文件,利用spring框架中的FileCopyUtils工具 FileCopyUtils.copy(fis,os); } }}@Autowiredprivate RestTemplate restTemplate;/ 小文件下载 @throws IOException /@Testpublic void downloadFile() throws IOException { String userName = "张三"; String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg"; //要求地址 String url = "http://localhost:8080/downloadFile/{1}/{2}"; //发起要求,直接返回工具(restful风格) ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class, userName,fileName); System.out.println("文件下载要求结果状态码:" + rsp.getStatusCode()); // 将下载下来的文件内容保存到本地 String targetPath = "/Users/panzhi/Desktop/" + fileName; Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(), "未获取到下载文件"));}

这种下载方法实际上是将下载文件一次性加载到客户端本地内存,然后从内存将文件写入磁盘。
这种办法对付小文件的下载还比较适宜,如果文件比较大或者文件下载并发量比较大,随意马虎造成内存的大量占用,从而降落运用的运行效率

大文件下载

@Autowiredprivate RestTemplate restTemplate;/ 大文件下载 @throws IOException /@Testpublic void downloadBigFile() throws IOException { String userName = "张三"; String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg"; //要求地址 String url = "http://localhost:8080/downloadFile/{1}/{2}"; //定义要求头的吸收类型 RequestCallback requestCallback = request -> request.getHeaders() .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL)); //对相应进行流式处理而不是将其全部加载到内存中 String targetPath = "/Users/panzhi/Desktop/" + fileName; restTemplate.execute(url, HttpMethod.GET, requestCallback, clientHttpResponse -> { Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath)); return null; }, userName, fileName);}

这种下载办法的差异在于: 设置了要求头APPLICATION_OCTET_STREAM,表示以流的形式进行数据加载 RequestCallback结合File.copy担保了吸收到一部分文件内容,就向磁盘写入一部分内容。
而不是全部加载到内存,末了再写入磁盘文件。
不才载大文件时,例如excel、pdf、zip等等文件,特殊管用,

参考文章

www.jianshu.com/p/58949f833…

blog.csdn.net/weixin_4001…

juejin.cn/post/684490…

blog.csdn.net/qq_41907769…

五.WebClient

在 Spring 5.0 之前,如果我们想要调用其他系统供应的 HTTP 做事,常日可以利用 Spring 供应的 RestTemplate 来访问,不过由于 RestTemplate 是 Spring 3 中引入的同步壅塞式 HTTP 客户端,因此存在一定性能瓶颈。
根据 Spring 官方文档先容,在将来的版本中它可能会被弃用。

WebClient是Spring WebFlux模块供应的一个非壅塞的基于相应式编程的进行Http要求的客户端工具,从Spring5.0开始供应。
WebClient有一个基于Reactor的功能性的、流畅的API,它支持异步逻辑的声明式组合,而无需处理线程或并发性。
它是完备无壅塞的,支持流,并且依赖于同样的编解码器,这些编解码器也用于在做事器端编码和解码要乞降相应内容。

1.为什么RestTemplate被弃用RestTemplate弊端

壅塞性子: RestTemplate 是一个壅塞、同步客户端。
这意味着实行要求的线程会壅塞,直到操作完成,这可能会导致线程池耗尽,并在重负载下导致更高的延迟。
此模型不能很好地扩展,特殊是在运用程序必须有效处理数千个并发要求的微做事环境中。

可扩展性有限: RestTemplate 的同步特性限定了可扩展性。
须要高吞吐量、低延迟能力的当代系统创造这种方法不足。
事宜驱动、反应式编程范式的兴起是对这些需求的回应,导致了 WebClient 等非壅塞 API 的采取。

缺少反应式编程支持: RestTemplate 不支持反应式编程,而反应式编程在基于云的生态系统中日益增长。
相应式编程使系统更具相应性、弹性,但这是 RestTemplate 的壅塞性子无法实现的。

WebClient上风

WebClient许可开拓者通过构建链式的HTTP要乞降相应处理函数来构建异步和非壅塞式的HTTP客户端。
它支持多种HTTP方法、要乞降相应处理、缺点处理、HTTP认证和与RESTful做事交互。

WebClient具有以下优点:

相应式编程模型支持异步、非壅塞式要乞降相应处理。
强类型安全的API,支持Fluent API风格。
支持函数式编程,可以方便地进行流式处理。
支持自定义配置,如连接池、超时时间等。
可以与Spring WebFlux框架集成利用。
WebClient是一种简便、灵巧的办法来构建基于相应式编程模型的HTTP客户端。
它是Spring WebFlux框架的核心组件之一,并促进了Spring与Reactor之间的集成。

2.WebClient基本知识HTTP底层库选择

Spring5的WebClient客户端和WebFlux做事器都依赖于相同的非壅塞编解码器来编码和解码要乞降相应内容。
默认底层利用Netty,内置支持Jetty反应性HttpClient实现。
同时,也可以通过编码的办法实现ClientHttpConnector接口自定义新的底层库;如切换Jetty实现:

WebClient.builder() .clientConnector(new JettyClientHttpConnector()) .build();WEBCLIENT配置

根本配置

WebClient实例布局器可以设置一些根本的全局的web要求配置信息,比如默认的cookie、header、baseUrl等

WebClient.builder() .defaultCookie("kl","kl") .defaultUriVariables(ImmutableMap.of("name","kl")) .defaultHeader("header","kl") .defaultHeaders(httpHeaders -> { httpHeaders.add("header1","kl"); httpHeaders.add("header2","kl"); }) .defaultCookies(cookie ->{ cookie.add("cookie1","kl"); cookie.add("cookie2","kl"); }) .baseUrl("http://www.kailing.pub") .build();编解码配置

针对特定的数据交互格式,可以设置自定义编解码的模式,如下:

ExchangeStrategies strategies = ExchangeStrategies.builder() .codecs(configurer -> { configurer.customCodecs().decoder(new Jackson2JsonDecoder()); configurer.customCodecs().encoder(new Jackson2JsonEncoder()); }) .build(); WebClient.builder() .exchangeStrategies(strategies) .build();Maven依赖

xml复制代码<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId></dependency>WebClient实例创建

WebClient.create()

@Testpublic void testCreate() { Mono<String> mono = WebClient //创建WenClient实例 .create() //方法调用,WebClient中供应了多种方法 .method(HttpMethod.GET) //要求url .uri("http://localhost:8080/hello") //获取相应结果 .retrieve() //将结果转换为指定类型 .bodyToMono(String.class); //block方法返回终极调用结果,block方法是壅塞的 System.out.println("相应结果:" + mono.block());}

WebClient.create(String baseUrl):指定baseUrl,利用该客户端发送要求都是基于baseUrl。

@Testpublic void testCreateBaseUrl() { Mono<String> mono = WebClient //创建WenClient实例,指定根本url,以是下面uri要求的路径都是基于这个路径 .create("http://localhost:8080") //方法调用,WebClient中供应了多种方法 .method(HttpMethod.GET) //要求url .uri("/hello") //获取相应结果 .retrieve() //将结果转换为指定类型 .bodyToMono(String.class); //block方法返回终极调用结果,block方法是壅塞的 System.out.println("相应结果:" + mono.block());}

WebClient.builder():返回一个WebClient.Builder,该工具可以做链式调用,通报更多的参数。

@Testpublic void testBuilder() { Mono<String> mono = WebClient .builder() //配置头信息,或者其他信息 .defaultHeader("token", "123456789") //创建WebClient实例 .build() //方法调用,WebClient中供应了多种方法 .method(HttpMethod.GET) //要求url .uri("http://localhost:8080/hello") //获取相应结果 .retrieve() //将结果转换为指定类型 .bodyToMono(String.class);}支持的可选配置

uriBuilderFactory:自定义UriBuilderFactory灵巧配置利用Url defaultHeader:为HTTP要求设置Headers要求头 defaultCookie:为HTTP要求设置Cookies defaultRequest:自定义HttpRequest filter:为HTTP要求增加客户端过滤器 exchangeStrategies:HTTP读写信息自定义 clientConnector:HTTP客户端连接器设置

3.获取相应结果的办法block()壅塞式获取相应结果

@Testpublic void testMono() { Mono<User> userMono = WebClient .create() .method(HttpMethod.GET) .uri("http://localhost:8080/hello") .retrieve() .bodyToMono(User.class); User user = userMono.block();}@Testpublic void testFlux() { Flux<User> userFlux = WebClient .create() .method(HttpMethod.GET) .uri("http://localhost:8080/hello") .retrieve() .bodyToFlux(User.class); List<User> users = userFlux.collectList().block();}

利用Mono和Flux吸收返回结果,一个Mono工具包含0个或1个元素,而一个Flux工具包含1个或多个元素。

subscribe()非壅塞式获取相应结果

@Testpublic void testSubscribe() { Mono<String> mono = WebClient .create() .method(HttpMethod.GET) .uri("http://localhost:8080/hello") .retrieve() .bodyToMono(String.class); mono.subscribe(WebClientTest::handleMonoResp);}//相应回调private static void handleMonoResp(String monoResp) { System.out.println("要求结果为:" + monoResp);}exchange()获取HTTP相应完全内容

@Testpublic void testExchange() { Mono<ClientResponse> clientResponseMono = WebClient .create() .method(HttpMethod.GET) .uri("http://localhost:8080/hello") .exchange(); ClientResponse clientResponse = clientResponseMono.block(); //相应头 ClientResponse.Headers headers = clientResponse.headers(); //相应状态 HttpStatus httpStatus = clientResponse.statusCode(); //相应状态码 int rawStatusCode = clientResponse.rawStatusCode(); //相应体 Mono<String> mono = clientResponse.bodyToMono(String.class); String body = mono.block();}4.传参办法数字占位符

public static void main(String[] args) throws Exception { List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); String url = "http://localhost:8080/user/{1}/{2}"; Mono<String> mono = WebClient.create() .method(HttpMethod.POST) .uri(url, list.toArray()) .retrieve() .bodyToMono(String.class); String result = mono.block();}参数名占位符

public static void main(String[] args) throws Exception { String url = "http://localhost:8080/user/{id}/{name}"; String id = "123"; String name = "Boss"; Mono<String> mono = WebClient.create() .method(HttpMethod.POST) .uri(url, id, name) .retrieve() .bodyToMono(String.class); String result = mono.block();}map传参

public static void main(String[] args) throws Exception { String url = "http://localhost:8080/user/{id}/{name}"; Map<String, String> params = new HashMap<>(); params.put("id", "123"); params.put("name", "Boss"); Mono<String> mono = WebClient.create() .method(HttpMethod.POST) .uri(url, params) .retrieve() .bodyToMono(String.class); String result = mono.block();}5.Get要求示例

/ Get要求, 带要求头, 带参 @param url @param headers @param params @return / public JSONObject getRequest(String url, MultiValueMap<String, String> headers, Map<String, Object> params) { Mono<JSONObject> mono = webClientBuilder().defaultHeaders(httpHeaders -> { httpHeaders.addAll(headers); }).build() // 方法调用, WebClient中供应了多种方法 .method(HttpMethod.GET) // 要求URL .uri(url, params) // 获取相应结果 .retrieve() // 将结果转换为指定类型 .bodyToMono(JSONObject.class) .doOnError(e -> log.error("Error occurred", e)) .onErrorResume(e -> Mono.just(new JSONObject().fluentPut("message", "Fallback value"))); // 利用subscribe异步订阅结果 mono.subscribe(result -> System.out.println(result)); // 利用block同步壅塞,返回相应结果 return mono.block(); }

在 WebClient 中,统统都是非壅塞的。
该retrieve()方法发起要求,并将bodyToMono相应主体转换为 Reactor Mono。
该subscribe()方法用于订阅结果,一旦可用就会对其进行处理。

如果须要携带繁芜的查询参数,可以通过UriComponentsBuilder布局出uri要求地址,如:

//定义query参数 MultiValueMap params = new LinkedMultiValueMap<>(); params.add("name", "kl"); params.add("age", "19"); //定义url参数 Map uriVariables = new HashMap<>(); uriVariables.put("id", 200); String uri = UriComponentsBuilder.fromUriString("/article/index/arcid/{id}.html") .queryParams(params) .uriVariables(uriVariables)

下载文件时,由于不清楚各种格式文件对应的MIME Type,可以设置accept为MediaType.ALL,然后利用Spring的Resource来吸收数据即可,如:

WebClient.create("https://kk-open-public.oss-cn-shanghai.aliyuncs.com/xxx.xlsx") .get() .accept(MediaType.ALL) .retrieve() .bodyToMono(Resource.class) .subscribe(resource -> { try { File file = new File("E://abcd.xlsx"); FileCopyUtils.copy(StreamUtils.copyToByteArray(resource.getInputStream()), file); }catch (IOException ex){} });6.Post要求示例

post要求示例演示了一个比较繁芜的场景,同时包含表单参数和文件流数据。
如果是普通post要求,直接通过bodyValue设置工具实例即可。
不用FormInserter布局。

WebClient client = WebClient.create("http://www.kailing.pub"); FormInserter formInserter = fromMultipartData("name","kl") .with("age",19) .with("map",ImmutableMap.of("xx","xx")) .with("file",new File("E://xxx.doc")); Mono<String> result = client.post() .uri("/article/index/arcid/{id}.html", 256) .contentType(MediaType.APPLICATION_JSON) .body(formInserter) //.bodyValue(ImmutableMap.of("name","kl")) .retrieve() .bodyToMono(String.class); result.subscribe(System.err::println);json格式要求

/ post要求, 要求头, 要求内容 @param url @param headers @param body @return / public JSONObject postRequest(String url, MultiValueMap<String, String> headers, Map<String, Object> body) { Mono<JSONObject> mono = webClientBuilder().build().post().uri(url).headers(httpHeaders -> { httpHeaders.addAll(headers); }).contentType(MediaType.APPLICATION_JSON) .bodyValue(body) .retrieve() .bodyToMono(JSONObject.class) .doOnError(e -> log.error("Error occurred", e)) .onErrorResume(e -> Mono.just(new JSONObject().fluentPut("message", "Fallback value"))); // 利用subscribe异步订阅结果 mono.subscribe(result -> System.out.println(result)); // 利用block同步壅塞,返回相应结果 return mono.block(); }form格式要求

/ post要求, FormData/Multipart要求办法 @param url @param headers @param params 通报文件还可以利用如下办法进行布局 @param <T> @return / public <T> JSONObject postRequestMultipart(String url, MultiValueMap headers, MultiValueMap params) { Mono<JSONObject> mono = webClient().post().uri(url).headers(httpHeaders -> { httpHeaders.addAll(headers); }).body(fromMultipartData(params)) .retrieve() .bodyToMono(JSONObject.class) .doOnError(e -> log.error("Error occurred", e)) .onErrorResume(e -> Mono.just(new JSONObject().fluentPut("message", "Fallback value"))); // 利用subscribe异步订阅结果 mono.subscribe(result -> System.out.println(result)); // 利用block同步壅塞,返回相应结果 return mono.block(); }同步返回结果

上面演示的都是异步的通过mono的subscribe订阅相应值。
当然,如果你想同步壅塞获取结果,也可以通过.block()壅塞当前哨程获取返回值。

WebClient client = WebClient.create("http://www.kailing.pub"); String result = client .get() .uri("/article/index/arcid/{id}.html", 256) .retrieve() .bodyToMono(String.class) .block(); System.err.println(result);

但是,如果须要进行多个调用,则更高效地方式是避免单独壅塞每个相应,而是等待组合结果,如:

WebClient client = WebClient.create("http://www.kailing.pub"); Mono<String> result1Mono = client .get() .uri("/article/index/arcid/{id}.html", 255) .retrieve() .bodyToMono(String.class); Mono<String> result2Mono = client .get() .uri("/article/index/arcid/{id}.html", 254) .retrieve() .bodyToMono(String.class); Map map = Mono.zip(result1Mono, result2Mono, (result1, result2) -> { Map arrayList = new HashMap<>(); arrayList.put("result1", result1); arrayList.put("result2", result2); return arrayList; }).block(); System.err.println(map.toString());Filter过滤器

可以通过设置filter拦截器,统一修正拦截要求,比如认证的场景,如下示例,filter注册单个拦截器,filters可以注册多个拦截器,basicAuthentication是系统内置的用于basicAuth的拦截器,limitResponseSize是系统内置用于限定响值byte大小的拦截器。

WebClient.builder() .baseUrl("http://www.kailing.pub") .filter((request, next) -> { ClientRequest filtered = ClientRequest.from(request) .header("foo", "bar") .build(); return next.exchange(filtered); }) .filters(filters ->{ filters.add(ExchangeFilterFunctions.basicAuthentication("username","password")); filters.add(ExchangeFilterFunctions.limitResponseSize(800)); }) .build().get() .uri("/article/index/arcid/{id}.html", 254) .retrieve() .bodyToMono(String.class) .subscribe(System.err::println);WEBSOCKET支持

WebClient不支持websocket要求,要求websocket接口时须要利用WebSocketClient,如:

WebSocketClient client = new ReactorNettyWebSocketClient();URI url = new URI("ws://localhost:8080/path");client.execute(url, session -> session.receive() .doOnNext(System.out::println) .then());

链接:https://juejin.cn/post/7305348040209858596