1.WebClient之Spring WebFlux
Flux是Reactor项目中的一个核心概念,它代表了一个可响应式、可取消的、元素序列。Flux是异步的,这意味着它不会阻塞当前线程来处理数据。相反,它允许数据项在它们准备好时一个接一个地处理,通常是通过一个非阻塞的方式。
在Spring WebFlux中,Flux通常用于处理HTTP请求和响应,特别是在需要处理流式数据时。例如,一个Flux<String>可以表示一个服务器发送事件(SSE)流,或者一个客户端接收到的流式数据流。
Flux的特点包括:
非阻塞:Flux不会阻塞调用线程,它允许事件在准备好时按顺序处理。
背压:Flux支持背压,这意味着它可以根据下游的处理能力动态调整数据流的大小。
可取消:Flux是可取消的,这意味着可以在任何时候取消它,以释放资源。
元素序列:Flux可以包含0个或多个元素,并且每个元素都可以是一个数据项。
在处理Flux时,通常会使用subscribe方法来注册观察者,观察者可以处理Flux发出的元素。Flux还提供了许多其他操作符,如filter、map、flatMap等,这些操作符可以链式调用,以便在处理数据时应用各种转换。
1.1依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1.2配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Autowired
private WebClient webClient;
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://third-party-api") // 设置基础URL
.build();
}
Public Flux<String> postStreamingData(RequestVO request) {
String requestBody = FastJsonUtil.toJson(request);
return webClient.post()
.uri(url)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(requestBody)
.retrieve()
.bodyToFlux(String.class);
}
1.3.Flux数据处理,Flux属性(常用)
1.map对数据进行格式转化处理等;
2.filter对数据进行过滤,不符合的数据过滤掉;
3.doOnNext获取每一个流出的数据,对一个流出的数据进行处理,例子中用于拼接,当然用subscribe也可以;
4.subscribe方法来监听Flux中的每个字符串。在subscribe方法中,我们提供了三个回调:onNext、onError和onComplete。onNext回调用于处理每个字符串,你可以在这里进行拼接操作。onError回调用于处理错误情况,而onComplete回调用于在所有数据项到达后执行
5.doOnCancel: 这个操作符会在流被取消时执行,例如当客户端断开连接时。取消通常发生在下游取消了对Flux的订阅,或者使用了cancel()方法。doOnCancel保证只会在取消发生时被调用,并且不会被调用多次。
6.doFinally: 这个操作符会在流完成、取消或者发生错误时执行。它用于执行资源清理或者任何需要在流生命周期结束时执行的代码。doFinally保证在流的任何终止状态下至少被调用一次,无论是正常完成、被取消还是发生错误。
在大多数情况下,如果你使用了doOnCancel和doFinally,doFinally会在doOnCancel之后被调用,因为doFinally是在流完全终止之后执行的。但是,这并不是一个绝对的保证,因为doFinally可能会在doOnCancel之前被调用,尤其是在某些错误情况下。
Public Flux<String> completion(RequestVo request) {
StringBuilder sb = new StringBuilder();
return webClientConfig.postStreamingData(request)
.map(this::chat)
.doOnNext(result -> {
sb.append(result);
})
.doFinally(result -> {
save(sb.toString());
});
}
public String chat(String responseLine) {
if (!Object.equals(responseLine, "[DONE]") {
returnresponseLine;
}
}
public void save(String result) {
//保存到数据库
}
1.4.关闭资源
当你完成对WebClient的使用后,你可以简单地让它被垃圾回收器回收。由于WebClient是基于响应式编程的,它不会保持任何持久的资源,除非你在代码中显式地持有资源(例如,通过订阅了一个Flux或Mono)。如果你需要关闭WebClient来释放资源,你可以调用它的close方法。这通常不是必需的,因为WebClient实例本身并不持有
1.5Mono和Flux
Mono和Flux都是Reactor项目中的核心概念,用于表示响应式编程中的数据流,但它们在表示数据流的能力和用法上有所不同。
Mono
Mono代表了一个可能包含0个或1个元素的序列。
Mono是不可变的,一旦创建,就不能改变它包含的元素。
Mono通常用于表示一个单独的数据项,例如一个HTTP响应体。
Mono可以通过just、empty、error等方法创建。
Mono不支持背压(backpressure),它总是假设有足够的处理能力来处理数据。
Flux
Flux代表了一个可能包含0个、1个或多个元素的序列。
Flux是可变的,可以在其生命周期内添加、删除或修改元素。
Flux通常用于表示一个数据流,例如一系列HTTP响应体或事件。
Flux可以通过just、fromIterable、range等方法创建。
Flux支持背压,这意味着它可以根据下游的处理能力动态调整数据流的大小。
1.6.Flux转SseEmitter输出
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
@RequestMapping("/api/events")
public class EventController {
private final WebClient webClient = WebClient.builder().build();
private final ExecutorService nonBlockingService = Executors.newCachedThreadPool();
@GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleEvents() {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
nonBlockingService.execute(() -> {
// 调用第三方流式接口
Flux<String> flux = webClient.get()
.uri("http://third-party-api/stream")
.retrieve()
.bodyToFlux(String.class);
// 订阅流式数据并使用SseEmitter发送
flux.subscribe(
data -> {
try {
emitter.send(data);
} catch (IOException e) {
emitter.completeWithError(e);
}
},
error -> {
emitter.completeWithError(error);
},
() -> {
emitter.complete();
}
);
});
return emitter;
}
}
2.SseEmitter
2.1.对外提供流式接口
2.1.1.Controller
@RestController
@RequestMapping("/sse/websocket")
public class SSEController {
/** 正式使用注意并发量控制 */
@RequestMapping(value = "/sub", method = RequestMethod.GET, produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter subscribe(@RequestParam("questionId") String questionId) {
// 简单异步发消息 ====
new Thread(() -> {
try {
Thread.sleep(1000);
for (int i = 0; i < 10; i++) {
Thread.sleep(500);
SSEUtils.pubMsg(questionId, questionId + " - kingtao come " + i);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 消息发送完关闭订阅
SSEUtils.closeSub(questionId);
}
}).start();
// =================
return SSEUtils.addSub(questionId);
}
}
2.1.2.SSEUtils
package com.example.demo.util.sse;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SSEUtils {
// timeout
private static Long DEFAULT_TIME_OUT = 2*60*1000L;
// 订阅表
private static Map<String, SseEmitter> subscribeMap = new ConcurrentHashMap<>();
/** 添加订阅 */
public static SseEmitter addSub(String questionId) {
if (null == questionId || "".equals(questionId)) {
return null;
}
SseEmitter emitter = subscribeMap.get(questionId);
if (null == emitter) {
emitter = new SseEmitter(DEFAULT_TIME_OUT);
subscribeMap.put(questionId, emitter);
}
return emitter;
}
/** 发消息 */
public static void pubMsg(String questionId, String msg) {
SseEmitter emitter = subscribeMap.get(questionId);
if (null != emitter) {
try {
// 更规范的消息结构看源码
emitter.send(SseEmitter.event().data(msg));
} catch (Exception e) {
// e.printStackTrace();
}
}
}
// 关闭订阅
public static void closeSub(String questionId) {
SseEmitter emitter = subscribeMap.get(questionId);
if (null != emitter) {
try {
emitter.complete();
subscribeMap.remove(questionId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2.1.3.前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>sse</title>
</head>
<body>
<div>
<label>问题id</label>
<input type="text" id="questionId">
<button onclick="subscribe()">订阅</button>
<hr>
<label>F12-console控制台查看消息</label>
</div>
<script>
function subscribe() {
let questionId = document.getElementById('questionId').value;
let url = 'http://localhost:8089/sse/websocket/sub?questionId=' + questionId;
let eventSource = new EventSource(url);
eventSource.onmessage = function (e) {
console.log(e.data);
};
eventSource.onopen = function (e) {
// todo
};
eventSource.onerror = function (e) {
// todo
eventSource.close()
};
}
</script>
</body>
</html>
2.2.调用第三方流式接口
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
public class SseController {
private final RestTemplate restTemplate = new RestTemplate();
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
@GetMapping(value = "/sse/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream() {
SseEmitter emitter = new SseEmitter();
executorService.execute(() -> {
try {
String thirdPartyUrl = "http://third-party-api/stream"; // 第三方流式接口URL
// 调用第三方流式接口
InputStream inputStream = restTemplate.getForObject(thirdPartyUrl, InputStream.class);
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null && !emitter.isComplete()) {
emitter.send(line); // 发送每行数据作为SSE事件
}
emitter.complete(); // 完成SSE事件流
} catch (IOException e) {
emitter.completeWithError(e); // 发生错误时完成SSE事件流
}
});
return emitter;
}
}
package com.example.demo.util.sse;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class SSEClient {
// timeout
private static Integer DEFAULT_TIME_OUT = 2*60*1000;
/** 获取SSE输入流 */
public static InputStream getSseInputStream(String urlPath, int timeoutMill) throws IOException {
HttpURLConnection urlConnection = getHttpURLConnection(urlPath, timeoutMill);
InputStream inputStream = urlConnection.getInputStream();
return new BufferedInputStream(inputStream);
}
/** 读流数据 */
public static void readStream(InputStream is, MsgHandler msgHandler) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
try {
String line = "";
while ((line = reader.readLine()) != null) {
msgHandler.handleMsg(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 服务器端主动关闭时,客户端手动关闭
reader.close();
is.close();
}
}
private static HttpURLConnection getHttpURLConnection(String urlPath, int timeoutMill) throws IOException {
URL url = new URL(urlPath);
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setDoOutput(true);
urlConnection.setDoInput(true);
urlConnection.setUseCaches(false);
urlConnection.setRequestMethod("GET");
urlConnection.setRequestProperty("Connection", "Keep-Alive");
urlConnection.setRequestProperty("Charset", "UTF-8");
// text/plain模式
urlConnection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8");
// 读过期时间
urlConnection.setReadTimeout(timeoutMill);
return urlConnection;
}
/** 消息处理接口 */
interface MsgHandler {
void handleMsg(String line);
}
public static void main(String[] args) throws Exception {
// 单订阅
String urlPath = "http://localhost:8089/sse/websocket/sub?questionId=kingtao3";
InputStream inputStream = getSseInputStream(urlPath, DEFAULT_TIME_OUT);
readStream(inputStream, new MsgHandler() {
@Override
public void handleMsg(String line) {
if (line != null && line.contains("data:")) {
// 注意按约定的消息协议解析消息
String msg = line.split(":")[1];
System.out.println(msg);
}
}
});
// 并发订阅
// ExecutorService executorService = Executors.newFixedThreadPool(100);
// for (int i = 0; i < 100; i++) {
// int finalI = i;
// executorService.submit(() -> {
// try {
// String urlPath = "http://localhost:8089/sse/websocket/subscribe?questionId=kingtao" + finalI;
// InputStream inputStream = getSseInputStream(urlPath, DEFAULT_TIME_OUT);
// readStream(inputStream, new MsgHandler() {
// @Override
// public void handleMsg(String line) {
// if (line != null && line.contains("data:")) {
// // 注意按约定的消息协议解析消息
// String msg = line.split(":")[1];
// System.out.println(msg);
// }
// }
// });
// } catch (Exception e) {
// e.printStackTrace();
// }
// });
// }
// executorService.shutdown();
}
}