Java实现远程调用方案汇总

Java实现远程调用方案汇总

一,前言

在工作中很多时候需要将自身平台与其他平台(系统)进行对接,对接方式不乏为取值和传值,针对自身主动的请求需要用到远程调用,而针对自身被动的请求(第三方系统调用我们的接口)则写好接口就可以了。本节我们主要是针对自身主动的请求,也就是远程调用的实现方式,进行详细的论述,并提供实际源码供大家结合本文学习,或者使用到工作当中去。

仓库源码

gitee.com/Sheng-Sheng...

二,Hutool远程调用方式

号称让Java语言也可以甜甜的的hutool基本提供了工作中所需要使用到的工具集,当然了远程Http请求工具也是有的。

在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的核心集中在两个类:

  • HttpRequest
  • HttpResponse

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

2.Hutool-http优点

  1. 根据URL自动判断是请求HTTP还是HTTPS,不需要单独写多余的代码。
  2. 表单数据中有File对象时自动转为multipart/form-data表单,不必单独做操作。
  3. 默认情况下Cookie自动记录,比如可以实现模拟登录,即第一次访问登录URL后后续请求就是登录状态。
  4. 自动识别304跳转并二次请求
  5. 自动识别页面编码,即根据header信息或者页面中的相关标签信息自动识别编码,最大可能避免乱码。
  6. 自动识别并解压Gzip格式返回内容

3.Get请求使用方式

java 复制代码
// 最简单的HTTP请求,可以自动通过header等信息判断编码,不区分HTTP和HTTPS
String 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> paramMap = new HashMap<>();
paramMap.put("city", "北京");

String result3= HttpUtil.get("https://www.baidu.com", paramMap);

但是在我们日常工作中, 一般的调用都是有请求头和请求参数的, 所以重点代码如下:

java 复制代码
    /**
     * 不带有请求头,带有表单参数的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请求使用方式

java 复制代码
/**
 * @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&amp;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&amp;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&amp;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.文件下载上传

文件上传

java 复制代码
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单独做了封装。文件下载在面对大文件时采用流的方式读写,内存中只是保留一定量的缓存,然后分块写入硬盘,因此大文件情况下不会对内存有压力。

java 复制代码
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);

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

java 复制代码
//带进度显示的文件下载
HttpUtil.downloadFile(fileUrl, FileUtil.file("e:/"), new StreamProgress(){
	
	@Override
	public void start() {
		Console.log("开始下载。。。。");
	}
	
	@Override
	public void progress(long progressSize) {
		Console.log("已下载:{}", FileUtil.readableFileSize(progressSize));
	}
	
	@Override
	public 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部分,看是否可以定位标题。

编写代码

具体代码如下:

java 复制代码
//请求列表页
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请求:

java 复制代码
// 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()方法的源代码如下:

java 复制代码
@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() ,其源代码如下:

java 复制代码
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() 方法里会做一些更为严谨的校验,去掉这些校验后该方法的核心代码如下:

java 复制代码
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;
  }

这段代码可以看成三个步骤:

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

责任链串联

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

递归方法

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

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

markdown 复制代码
														Interceptor 和 Chain 的关系图

6.OkHttp拦截器在项目中的应用

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

  • 添加请求头的拦截器
java 复制代码
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请求方式

java 复制代码
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
@Slf4j
public 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请求方式

java 复制代码
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请求的工具类底座
 */
@Component
public 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 HttpURLConnectionApache HttpComponentsokHttp 等)基础上,封装了更加简单易用的模板方法 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将其做映射)。

java 复制代码
HttpMessageConverterExtractor<T> responseExtractor =
				new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);

HttpMessageConverter.java源码

java 复制代码
public interface HttpMessageConverter<T> {
        //指示此转换器是否可以读取给定的类。
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

        //指示此转换器是否可以写给定的类。
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

        //返回List<MediaType>
	List<MediaType> getSupportedMediaTypes();

        //读取一个inputMessage
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
			throws IOException, HttpMessageNotReadableException;

        //往output message写一个Object
	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException;

}

在内部,RestTemplate默认使用SimpleClientHttpRequestFactoryDefaultResponseErrorHandler来分别处理HTTP的创建和错误,但也可以通过setRequestFactorysetErrorHandler来覆盖。

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请求,看看程序运行是否正常。

java 复制代码
@Test
public 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

java 复制代码
@Configuration
public class RestTemplateConfig {
    /**
     * 没有实例化RestTemplate时,初始化RestTemplate
     * @return
     */
    @ConditionalOnMissingBean(RestTemplate.class)
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }
}

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

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

java 复制代码
@Configuration
public 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的位置,注入并使用即可!

java 复制代码
@Autowired
private RestTemplate restTemplate;

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

java 复制代码
/**
 * 使用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:

java 复制代码
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

java 复制代码
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类:

java 复制代码
@Configuration
public 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():返回的是ResponseEntityResponseEntity是对HTTP响应的封装,除了包含响应体,还包含HTTP状态码、contentType、contentLength、Header等信息

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

不带参get请求

java 复制代码
@RestController
public 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方法 
}
java 复制代码
@Autowired
private RestTemplate restTemplate;
/**
 * 单元测试(不带参的get请求)
 */
@Test
public void testGet(){
    //请求地址
    String url = "http://localhost:8080/testGet";

    //发起请求,直接返回对象
    ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class);
    System.out.println(responseBean.toString());
}

带参的get请求(使用占位符号传参)

java 复制代码
@RestController
public 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;
    }
}

java 复制代码
@Autowired
private RestTemplate restTemplate;
 /**
 * 单元测试(带参的get请求)
 */
@Test
public 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风格)

java 复制代码
@RestController
public 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;
    }
}
java 复制代码
@Autowired
private RestTemplate restTemplate;

 /**
 * 单元测试(带参的get请求)
 */
@Test
public 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()获取响应体。

java 复制代码
 /**
 * 单元测试
 */
@Test
public 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设置参数

java 复制代码
//请求头
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);

工程工具类

java 复制代码
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请求工具类
 */
@Component
public 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请求方法上大同小异,RestTemplatePOST请求也包含两个主要方法:

  • postForObject():返回body对象
  • postForEntity():返回全部的信息

模拟表单请求

java 复制代码
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟表单提交,post请求
 */
@Test
public 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请求

java 复制代码
@Autowired
private RestTemplate restTemplate;
/**
 * 模拟JSON提交,post请求
 */
@Test
public 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());
}

模拟页面重定向

java 复制代码
@Autowired
private RestTemplate restTemplate;
/**
 * 重定向,post请求
 */
@Test
public 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.html

PUT请求

java 复制代码
@Autowired
private RestTemplate restTemplate;
/**
 * 模拟JSON提交,put请求
 */
@Test
public void testPutByJson(){
    //请求地址
    String url = "http://localhost:8080/testPutByJson";
    //入参
    RequestBean request = new RequestBean();
    request.setUserName("唐三藏");
    request.setUserPwd("123456789");

    //模拟JSON提交,put请求
    restTemplate.put(url, request);
}

DELETE请求

java 复制代码
@Autowired
private RestTemplate restTemplate;
/**
 * 模拟JSON提交,delete请求
 */
@Test
public 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.文件操作

文件上传

java 复制代码
@RestController
public 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;
    }
}
java 复制代码
@Autowired
private RestTemplate restTemplate;

/**
 * 文件上传,post请求
 */
@Test
public 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());
}

文件下载

java 复制代码
@RestController
public 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);
        }
    }
}
java 复制代码
@Autowired
private RestTemplate restTemplate;

/**
 * 小文件下载
 * @throws IOException
 */
@Test
public 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(), "未获取到下载文件"));
}

这种下载方法实际上是将下载文件一次性加载到客户端本地内存,然后从内存将文件写入磁盘。这种方式对于小文件的下载还比较适合,如果文件比较大或者文件下载并发量比较大,容易造成内存的大量占用,从而降低应用的运行效率

大文件下载

java 复制代码
@Autowired
private RestTemplate restTemplate;
/**
 * 大文件下载
 * @throws IOException
 */
@Test
public 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实现:

java 复制代码
        WebClient.builder()
                .clientConnector(new JettyClientHttpConnector())
                .build();
WEBCLIENT配置

基础配置

WebClient实例构造器可以设置一些基础的全局的web请求配置信息,比如默认的cookie、header、baseUrl等

java 复制代码
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();
编解码配置

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

java 复制代码
        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()

java 复制代码
@Test
public 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。

java 复制代码
@Test
public 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,该对象可以做链式调用,传递更多的参数。

java 复制代码
@Test
public 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()阻塞式获取响应结果
java 复制代码
@Test
public void testMono() {
    Mono<User> userMono = WebClient
            .create()
            .method(HttpMethod.GET)
            .uri("http://localhost:8080/hello")
            .retrieve()
            .bodyToMono(User.class);
    User user = userMono.block();
}

@Test
public 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()非阻塞式获取响应结果
java 复制代码
@Test
public 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响应完整内容
java 复制代码
@Test
public 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.传参方式

数字占位符
java 复制代码
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();
}
参数名占位符
java 复制代码
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传参
java 复制代码
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请求示例

java 复制代码
   /**
     * 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请求地址,如:

java 复制代码
        //定义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来接收数据即可,如:

java 复制代码
        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构造。

java 复制代码
        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格式请求
java 复制代码
    /**
     * 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格式请求
java 复制代码
/**
     * 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()阻塞当前线程获取返回值。

java 复制代码
      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);

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

java 复制代码
      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大小的拦截器。

java 复制代码
        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,如:

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

参考文章:www.bmabk.com/index.php/p...

blog.csdn.net/A_art_xiang...

cloud.tencent.com/developer/a...

相关推荐
XINGTECODE25 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶31 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺36 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
先天牛马圣体1 小时前
如何提升大型AI模型的智能水平
后端
java亮小白19971 小时前
Spring循环依赖如何解决的?
java·后端·spring
2301_811274311 小时前
大数据基于Spring Boot的化妆品推荐系统的设计与实现
大数据·spring boot·后端
草莓base2 小时前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Ljw...2 小时前
表的增删改查(MySQL)
数据库·后端·mysql·表的增删查改
编程重生之路2 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端