Hello, I'm Tomo.
How do you handle the more time-consuming interfaces?
I'm familiar with this question, so I'll just go straight to the asynchronous interface and use theCallable
、WebAsyncTask
respond in singingDeferredResult
、CompletableFuture
etc. can be realized.
However, these methods have the limitation that the result of processing only returns a single value. In some scenarios, if you need the interface to process asynchronously while also continuously responding to the client with the result, these methods will not be sufficient.
The Spring Framework provides a variety of tools to support asynchronous streaming interfaces, such as theResponseBodyEmitter
、SseEmitter
cap (a poem)StreamingResponseBody
. The usage of these tools is simple, with the corresponding object or generic response entity returned directly in the interfaceResponseEntity<xxxx>
If the interface is asynchronous and performs time-consuming operations without blocking, theServlet
request threads that do not affect the responsiveness of the system.
The use of each tool and its application scenarios will be described below one by one.
ResponseBodyEmitter
ResponseBodyEmitter
Adaptation is suitable for scenarios where content needs to be dynamically generated and gradually sent to the client, e.g., file upload progress, real-time logs, etc. Updates can be sent to the client gradually during task execution.
For example, if you use GPT often, you will find that when you ask a question, the answer you get is not presented as a one-time response, but is displayed dynamically step by step. The advantage of doing so is that it makes you feel that it is thinking seriously, and the interactive experience is more vivid and natural than returning the complete answer directly.
utilizationResponseBodyEmitter
To achieve this effect, create a ResponseBodyEmitter sender object and simulate a time-consuming operation by calling the send method step-by-step to send the message.
Note: The ResponseBodyEmitter's timeout, if set to
0
maybe-1
, then the connection will not time out; if not set, the connection will be automatically disconnected after reaching the default timeout.The other two tools are used in the same way and will not be repeated later
@GetMapping("/bodyEmitter")
public ResponseBodyEmitter handle() {
// Create a ResponseBodyEmitter with -1 for no timeout
ResponseBodyEmitter emitter = new ResponseBodyEmitter(-1L); // Create a ResponseBodyEmitter, -1 means no timeout.
// Perform time-consuming operations asynchronously
(() -> {
try {
// Simulate the time-consuming operation
for (int i = 0; i < 10000; i++) {
("bodyEmitter " + i);
// Send the data
("bodyEmitter " + i + " " @ " + new Date() + "\n"); }
(2000);
}
// Finish
(); }
} catch (Exception e) {
// End the interface if an exception occurs
(e); }
}
}); }
return emitter.
}
The implementation code is very simple. By simulating the results of every 2 seconds to respond to the request interface, you can see the page data is dynamically generated. The effect is basically the same as the GPT response.
SseEmitter
SseEmitter
beResponseBodyEmitter
It is also capable of dynamic content generation, but it is mainly used in theserver-to-clientPush real-time data, such as real-time message push, status update, and other scenarios. In one of my previous postsI have 7 solutions for real-time web push messaging The details are described in theServer-Sent Events (SSE)
Technology, for those interested, can be reviewed.
SSE opens a one-way channel between the server and the client, and the server responds no longer with a one-time packet but with atext/event-stream
type of data streaming information that is streamed from the server to the client when there are data changes.
The overall implementation idea is somewhat similar to online video playback, the video stream will be continuously pushed to the browser, you can also understand that the client is completing a long (poor network) download.
The client-side JS implementation, after establishing a connection via a single HTTP request, waits to receive messages. At this point, the server creates aSseEmitter
object that sends a message to the client through this channel.
<body>
<div style="text-align: center;">
<h1>SSE receiving server-side event message data</h1>
<div >Waiting for connection... </div>.
</div>
<script>
let source = null; let userId = 7777
let userId = 7777
function setMessageInnerHTML(message) {
= message.
(newParagraph);
}
if () {
// Establish the connection
source = new EventSource('http://127.0.0.1:9033/subSseEmitter/'+userId);
setMessageInnerHTML("Connecting user = " + userId);
/**
* The open event is triggered once the connection is established.
* Alternatively written: = function (event) {}
*/
('open', function (e) {
setMessageInnerHTML("Connection established..") ;
}, false);
/**
* The client receives the data from the server.
* Alternatively written: = function (event) {}
*/
('message', function (e) {
setMessageInnerHTML();
});
} else {
setMessageInnerHTML("Your browser does not support SSE"); }); }
}
</script>;
</body>
On the server side, we willSseEmitter
The transmitter object is persisted so that the corresponding SseEmitter transmitter can be retrieved directly when a message is generated, and calls thesend
method for pushing.
private static final Map<String, SseEmitter> EMITTER_MAP = new ConcurrentHashMap<>();
@GetMapping("/subSseEmitter/{userId}")
public SseEmitter sseEmitter(@PathVariable String userId) {
("sseEmitter: {}", userId);
SseEmitter emitterTmp = new SseEmitter(-1L);
EMITTER_MAP.put(userId, emitterTmp);
(() -> {
try {
event = ()
.data("sseEmitter" + userId + " @ " + ())
.id((userId))
.name("sseEmitter");
(event);
} catch (Exception ex) {
(ex);
}
});
return emitterTmp;
}
@GetMapping("/sendSseMsg/{userId}")
public void sseEmitter(@PathVariable String userId, String msg) throws IOException {
SseEmitter sseEmitter = EMITTER_MAP.get(userId);
if (sseEmitter == null) {
return;
}
(msg);
}
next touserId=7777
of the user to send a message, 127.0.0.1:9033/sendSseMsg/7777?msg=Welcome to the attention of --> programmer Xiaofu, the message can be displayed on the page in real time.
And SSE is a bit better in that once a connection is established between the client and the server, even if a reboot of the server occurs, it can still be doneautomatic reconnection。
StreamingResponseBody
StreamingResponseBody
Slightly different from other response processing methods, it is mainly used to handle the transmission of large data volumes or continuous data streams, and supports writing data directly to theOutputStream
。
For example, when we need to download a very large file, using StreamingResponseBody avoids loading the file data into memory at once, and instead continuously streams the file to the client, thus solving the common memory overflow problem when downloading large files.
interface implementation directly returns the StreamingResponseBody object, which writes the data to the output stream and refreshes it, calling once theflush
will then write data to the client once.
@GetMapping("/streamingResponse")
public ResponseEntity<StreamingResponseBody> handleRbe() {
StreamingResponseBody stream = out -> {
String message = "streamingResponse";
for (int i = 0; i < 1000; i++) {
try {
(((message + i) + "\r\n").getBytes());
("\r\n".getBytes());
//invoke onceflushIt would be like writing data once to the front end
();
(1);
} catch (InterruptedException e) {
();
}
}
};
return ().contentType(MediaType.TEXT_HTML).body(stream);
}
demo output here is a simple text stream, if it is to download the file then converted to a file stream effect is the same.
summarize
This article introduces three tools to achieve asynchronous streaming interface , Spring knowledge points of literacy. It is relatively simple to use, there is no difficulty, but they are in the actual business application scenarios are still many, through these tools, you can effectively improve the performance and responsiveness of the system.
Demo Github address in the article:/chengxy-nds/Springboot-Notebook/tree/master/springboot101/general-features/springboot-streaming