使用 SSE + WebFlux 推送日志信息到前端
为什么使用 SSE 而不使用 WebSocket, 请看 SEE 对比 Websocket 的优缺点。
特性 | SSE | WebSocket |
---|---|---|
通信方向 | 单向(服务器→客户端) | 双向(全双工) |
协议 | 基于 HTTP | 独立协议(需 ws:// 前缀) |
兼容性 | 现代浏览器(IE 不支持) | 广泛支持 |
复杂度 | 简单(服务器只需返回流数据) | 较复杂(需处理握手、帧协议等) |
适用场景 | 实时推送(日志、通知、新闻) | 双向交互(聊天、实时协作) |
我的架构如下:
前端
后端中间件
后端服务
[前端] → 打开模态框 → 发起SSE连接 → [后端中间件] → 转发请求 → [后端服务] → 查询日志信息
← 实时日志推送 ← (SSE流) ← 捕获进程输出流 ← 返回实时日志流
前端html代码:
前端代码使用 bootstrap + jquery
<a href='#' data-bs-toggle='modal' data-bs-target='#logModal'>日志</a><!-- Modal -->
<div class="modal fade" id="logModal" tabindex="-1" aria-labelledby="logModalLabel" aria-hidden="true"><div class="modal-dialog modal-fullscreen"><div class="modal-content"><div class="modal-header"><h1 class="modal-title fs-5" id="logModalLabel">Modal title</h1><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div><div class="modal-body" id="logContent">...</div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button><button type="button" class="btn btn-primary">Save changes</button></div></div></div>
</div>
前端js代码:
$("#logModal").on('shown.bs.modal', function (){const eventSource = new EventSource("http://localhost:9998/middle/logs");const logContent = $('#logContent');eventSource.onmessage = function (event){const logLine = $('<div>').text(event.data)logContent.append(logLine);logContent[0].scrollTop = logContent[0].scrollHeight; // 自动滚动到底部};eventSource.onerror = function (error ) {console.log('日志连接失败: ', error);eventSource.close();logContent.text('日志获取失败,请检查容器常或网络连接')}$("#logModal").on('hide.bs.modal', function (){eventSource.close();logContent.empty(); // 清空日志})
})
后端中间件需要引入 webflux 的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
后端中间件代码:
controller:
@GetMapping(value = "/middle/logs", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter getDockerLogs(){return publishServerService.getDockerLogs();
}
service:
private final WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())).build();
private final ExecutorService executorService = Executors.newSingleThreadExecutor();public SseEmitter getDockerLogs() {String url = "http://127.0.0.1:9999/service/logs";SseEmitter emitter = new SseEmitter();executorService.submit(() -> {try {webClient.get().uri(url).retrieve().bodyToFlux(String.class).subscribe(log -> {try {emitter.send(SseEmitter.event().data(log)); // 发送消息到前端} catch (IOException e) {emitter.completeWithError(e);}},error -> emitter.completeWithError(error),emitter::complete);} catch (Exception e) {emitter.completeWithError(e);}});return emitter;
}
后端服务代码:
controller:
@GetMapping(value = "/service/logs", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public SseEmitter getDockerLogs() {return publishServerService.getDockerLogs();}
service:
private final ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();public SseEmitter getDockerLogs() {SseEmitter emitter = new SseEmitter();executorService.submit(() -> {try {int i = 0;while (i < 100000000) {try {emitter.send(SseEmitter.event().data(i)); // 发送消息到后端中间件Thread.sleep(1000);i++;} catch (InterruptedException e) {throw new RuntimeException(e);}}emitter.complete();} catch (IOException e) {throw new RuntimeException(e);}});return emitter;
}
提示:你可以把 i 换成真实的应用程序日志,如下:
public SseEmitter getDockerLogs() {SseEmitteremitter = new SseEmitter();executorService.submit(() -> {try {Process process = Runtime.getRuntime().exec("docker logs -f nginx");InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream());BufferedReader reader = new BufferedReader(inputStreamReader);String line;while ((line = reader.readLine()) != null) {emitter.send(SseEmitter.event().data(line));}emitter.complete();} catch (IOException e) {throw new RuntimeException(e);}});return emitter;
}
最终前端显示效果如下:
到此前端就可以实时的获取后端日志在页面中显示了。
但是你会发现后端控制台会时不时报错,错误信息如下:
2025-04-24T11:01:12.488Z WARN 26616 --- [nio-9990-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed. : org.springframework.web.context.request.async.AsyncRequestTimeoutException
2025-04-24T11:01:12.489Z WARN 26616 --- [nio-9990-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.context.request.async.AsyncRequestTimeoutException]org.springframework.web.context.request.async.AsyncRequestTimeoutExceptionat org.springframework.web.context.request.async.TimeoutDeferredResultProcessingInterceptor.handleTimeout(TimeoutDeferredResultProcessingInterceptor.java:42)at org.springframework.web.context.request.async.DeferredResultInterceptorChain.triggerAfterTimeout(DeferredResultInterceptorChain.java:81)at org.springframework.web.context.request.async.WebAsyncManager.lambda$startDeferredResultProcessing$5(WebAsyncManager.java:442)at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)at org.springframework.web.context.request.async.StandardServletAsyncWebRequest.onTimeout(StandardServletAsyncWebRequest.java:154)at org.apache.catalina.core.AsyncListenerWrapper.fireOnTimeout(AsyncListenerWrapper.java:44)at org.apache.catalina.core.AsyncContextImpl.timeout(AsyncContextImpl.java:135)at org.apache.catalina.connector.CoyoteAdapter.asyncDispatch(CoyoteAdapter.java:135)at org.apache.coyote.AbstractProcessor.dispatch(AbstractProcessor.java:243)at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:57)at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896)at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1744)at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)javaweb 报错如下:
2025-04-24T11:02:41.538Z WARN 26653 --- [nio-9991-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Ignoring exception, response committed. : org.springframework.web.context.request.async.AsyncRequestTimeoutException
2025-04-24T11:02:41.538Z WARN 26653 --- [nio-9991-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.context.request.async.AsyncRequestTimeoutException]
2025-04-24T11:02:42.480Z ERROR 26653 --- [or-http-epoll-2] reactor.core.publisher.Operators : Operator called default onErrorDroppedjava.lang.IllegalStateException: The response object has been recycled and is no longer associated with this facadeat org.apache.catalina.connector.ResponseFacade.checkFacade(ResponseFacade.java:478) ~[tomcat-embed-core-10.1.16.jar!/:na]at org.apache.catalina.connector.ResponseFacade.isFinished(ResponseFacade.java:154) ~[tomcat-embed-core-10.1.16.jar!/:na]at org.apache.catalina.connector.ResponseFacade.flushBuffer(ResponseFacade.java:240) ~[tomcat-embed-core-10.1.16.jar!/:na]at org.springframework.http.server.ServletServerHttpResponse.flush(ServletServerHttpResponse.java:104) ~[spring-web-6.0.14.jar!/:6.0.14]at org.springframework.http.server.DelegatingServerHttpResponse.flush(DelegatingServerHttpResponse.java:61) ~[spring-web-6.0.14.jar!/:6.0.14]at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler$HttpMessageConvertingHandler.complete(ResponseBodyEmitterReturnValueHandler.java:231) ~[spring-webmvc-6.0.14.jar!/:6.0.14]at org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter.complete(ResponseBodyEmitter.java:266) ~[spring-webmvc-6.0.14.jar!/:6.0.14]at reactor.core.publisher.LambdaSubscriber.onComplete(LambdaSubscriber.java:132) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.MonoFlatMapMany$FlatMapManyInner.onComplete(MonoFlatMapMany.java:260) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onComplete(Operators.java:2205) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:549) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onComplete(FluxConcatMapNoPrefetch.java:240) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.DrainUtils.postComplete(DrainUtils.java:183) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxBufferPredicate$BufferPredicateSubscriber.onComplete(FluxBufferPredicate.java:356) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxPeekFuseable$PeekFuseableConditionalSubscriber.onComplete(FluxPeekFuseable.java:595) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxMapFuseable$MapFuseableConditionalSubscriber.onComplete(FluxMapFuseable.java:350) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxContextWrite$ContextWriteSubscriber.onComplete(FluxContextWrite.java:126) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxDoFinally$DoFinallySubscriber.onComplete(FluxDoFinally.java:128) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.onComplete(FluxConcatArray.java:230) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drainAsync(FluxFlattenIterable.java:371) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.drain(FluxFlattenIterable.java:724) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxFlattenIterable$FlattenIterableSubscriber.onComplete(FluxFlattenIterable.java:273) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:144) ~[reactor-core-3.5.12.jar!/:3.5.12]at reactor.netty.channel.FluxReceive.terminateReceiver(FluxReceive.java:483) ~[reactor-netty-core-1.1.13.jar!/:1.1.13]at reactor.netty.channel.FluxReceive.drainReceiver(FluxReceive.java:275) ~[reactor-netty-core-1.1.13.jar!/:1.1.13]at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:419) ~[reactor-netty-core-1.1.13.jar!/:1.1.13]at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:446) ~[reactor-netty-core-1.1.13.jar!/:1.1.13]at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:500) ~[reactor-netty-core-1.1.13.jar!/:1.1.13]at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:768) ~[reactor-netty-http-1.1.13.jar!/:1.1.13]at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:114) ~[reactor-netty-core-1.1.13.jar!/:1.1.13]at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346) ~[netty-codec-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318) ~[netty-codec-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800) ~[netty-transport-classes-epoll-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:509) ~[netty-transport-classes-epoll-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:407) ~[netty-transport-classes-epoll-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.101.Final.jar!/:4.1.101.Final]at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.101.Final.jar!/:4.1.101.Final]at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
这是因为 SpringMVC 默认设置了 30 秒超时时间,你需要修改的长一点,具体多少由你自己定义。后端中间件和后端服务都需要配置。
错误分析
1. AsyncRequestTimeoutException
此错误表明异步请求超时。在 Spring 里,当一个异步请求在规定的时间内没有完成时,就会抛出该异常。默认情况下,Spring 的异步请求超时时间是 30 秒。在 SSE 场景中,因为要保持长连接以实现实时数据推送,所以很容易超出这个时间限制。
2. IllegalStateException
该错误显示响应对象已被回收,不再和当前的请求关联。这通常是在异步请求超时后,响应对象被关闭或者回收,而代码还尝试对其进行操作时发生的。
解决办法
1. 增加异步请求超时时间
你可以通过配置 WebMvcConfigurer 来增加异步请求的超时时间,避免因超时引发异常。
在 后端中间件 和 后端服务 中添加如下配置类:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
class LogWebConfig implements WebMvcConfigurer {@Overridepublic void configureAsyncSupport(AsyncSupportConfigurer configurer) {// 设置异步请求超时时间为 3600 秒(1 小时)configurer.setDefaultTimeout(3600 * 1000); }
}
扩展:
我通过前端传递参数(容器id)给后端执行,我发现了一个奇怪的问题。
我第一次打开模态框,可以正常的实时显示日志。但是我点击 close 的时候,这段代码确实执行了。
$("#logModal").on('hide.bs.modal', function (){eventSource.close();logContent.empty(); // 清空日志
})
但是我再次打开模态框,发现没有日志显示了,我甚至刷新页面,然后再次打开模态框依然没有日志显示,只有我把后端服务重启之后,再次打开模态框,才有日志显示,这是为什么?
这个问题可能是由于EventSource 实例未正确释放或重复创建导致的资源冲突。以下是具体分析和解决方案:
问题原因分析
1. EventSource 作用域问题:
在当前代码中,eventSource变量是在shown.bs.modal事件处理函数的闭包中创建的。当模态框关闭时,虽然通过hide.bs.modal事件调用了eventSource.close(),但闭包中的变量可能未被正确释放,导致再次打开模态框时,旧的eventSource实例残留或引用错误。
2. 事件绑定重复问题:
每次打开模态框时,shown.bs.modal事件处理函数会重复绑定hide.bs.modal事件,导致多个处理函数共存,可能混淆不同实例的eventSource。
3. 后端连接未正确断开:
若后端未正确处理EventSource的断开(如未关闭响应流),可能导致端口或连接资源被占用,后续请求无法建立新连接。
解决方案:
使用模态框数据存储 EventSource 实例
修改代码,将eventSource实例存储在模态框元素的data属性中,确保每次关闭时正确释放当前实例:
// 修改后的模态框显示事件处理函数
$("#logModal").on('shown.bs.modal', function (event) {const trigger = $(event.relatedTarget);const itemStr = trigger.data('item');if (!itemStr) return;const item = JSON.parse(itemStr);const modal = $(this); // 获取当前模态框实例// 先关闭可能存在的旧连接const oldEventSource = modal.data('eventSource');if (oldEventSource) {oldEventSource.close();modal.removeData('eventSource');}// 创建新的EventSource并存储到模态框数据中const eventSource = new EventSource(`http://localhost:9998/middle/logs?containerId=${item.containerId}&ip=${item.ip}`);modal.data('eventSource', eventSource);const logContent = $('#logContent');logContent.empty();eventSource.onmessage = function (event) {const logLine = $('<div>').text(event.data);logContent.append(logLine);logContent[0].scrollTop = logContent[0].scrollHeight;};eventSource.onerror = function (error) {console.log('日志连接失败: ', error);eventSource.close();logContent.text('日志获取失败,请检查容器状态或网络连接');modal.removeData('eventSource'); // 清除错误状态的实例};
});// 修改后的模态框隐藏事件处理函数
$("#logModal").on('hide.bs.modal', function () {const modal = $(this);const eventSource = modal.data('eventSource');if (eventSource) {eventSource.close(); // 关闭当前实例modal.removeData('eventSource'); // 清除数据引用}$('#logContent').empty(); // 清空日志
});
其实不是上面这个问题,经过再次排查,我发现了问题所在,当我的后端服务的代码这样写的时候,就会出现前面说的问题。
public SseEmitter getDockerLogs(PublishServerDto publishServerDto) {SseEmitter emitter = new SseEmitter();executorService.submit(() -> {try {Process process = Runtime.getRuntime().exec("docker logs -f " + publishServerDto.getContainerId());InputStreamReader inputStreamReader = new InputStreamReader(process.getInputStream());BufferedReader reader = new BufferedReader(inputStreamReader);String line;while ((line = reader.readLine()) != null) {emitter.send(SseEmitter.event().data(line));}emitter.complete();} catch (IOException e) {emitter.completeWithError(e);}});return emitter;}
当我的后端服务的代码修改成这样的时候。
public SseEmitter getDockerLogs(PublishServerDto publishServerDto) {SseEmitter emitter = new SseEmitter();executorService.submit(() -> {try {int i = 0;while (i < 100000000) {try {emitter.send(SseEmitter.event().data(i)); // 发送消息到后端中间件Thread.sleep(1000);i++;} catch (InterruptedException e) {throw new RuntimeException(e);}}emitter.complete();} catch (IOException e) {emitter.completeWithError(e);}});return emitter;}
前端关闭再打开模态框也能正常显示数据。为什么?
这是因为 docker logs -f 命令会持续阻塞直到进程被终止,而我的后端代码在 EventSource 关闭时未正确终止正在运行的 Process 对象,导致资源占用和连接冲突。
docker logs -f 命令导致线程阻塞,无法响应 SseEmitter 的关闭事件。当前端关闭 EventSource 时,后端的 reader.readLine() 处于阻塞状态,无法感知 SseEmitter 的异常或完成信号,导致 finally 块和异常处理代码无法执行。
另外,
以下是详细分析和解决方案:
问题根源:未终止后台进程
1. docker logs -f 的阻塞特性
-
docker logs -f 会持续读取容器日志并阻塞当前线程,直到容器停止或命令被中断(如按下 Ctrl+C)。
-
当前端关闭 EventSource 时,后端的 SseEmitter 会触发 completeWithError,但 Process 对象(docker logs 进程)仍在后台运行,其输入流被占用,导致:
- 再次打开模态框时,新的 Process 无法创建(端口 / 资源被占用)。
- 旧的 Process 残留数据可能干扰新连接。
2. 模拟数据与真实命令的差异
模拟数据场景: 模拟数据的循环中使用了 Thread.sleep(1000),该方法会响应线程中断(InterruptedException)。当 SseEmitter 关闭时,会抛出异常并中断线程,而 docker logs -f 的阻塞式读取无法响应中断,导致清理逻辑失效。
真实命令场景: docker logs -f 是外部进程,不受 Java 线程控制,EventSource 关闭时未终止该进程,导致资源泄漏。
3. 阻塞式读取的局限性
BufferedReader.readLine() 是阻塞式方法,当 docker logs -f 没有新日志时,线程会一直阻塞在此处,无法处理 SseEmitter 的关闭事件(如 emitter.completeWithError() 或客户端断开连接引发的异常)。可以使用非阻塞式读取或中断机制 解决这个问题。
解决方案:
分离读取逻辑
将日志读取放到独立线程中,避免主线程被 docker logs -f 阻塞,确保能响应 SseEmitter 的关闭事件。
监听 SseEmitter 事件
使用 emitter.onCompletion() 和 emitter.onError() 回调,在连接关闭或出错时执行清理逻辑。
修改后的代码:
service :
// 使用原子引用存储进程对象,确保多线程环境下的可见性和原子性private final AtomicReference<Process> processHolder = new AtomicReference<>();// 使用原子布尔控制日志读取循环,确保线程安全的状态变更private final AtomicBoolean isRunning = new AtomicBoolean(true);public SseEmitter getDockerLogs(PublishServerDto publishServerDto) {SseEmitter emitter = new SseEmitter();// 提交异步任务处理日志流executorService.submit(() -> {try {// 1. 执行docker logs命令,获取容器实时日志String containerId = publishServerDto.getContainerId();Process process = Runtime.getRuntime().exec("docker logs -f " + containerId);processHolder.set(process); // 保存进程引用到原子容器中// 2. 启动独立线程读取日志输入流(避免主线程阻塞)new Thread(() -> {try ( // 使用try-with-resources自动关闭流BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {String line;// 循环读取日志,直到isRunning为false或读取到null(流结束)while (isRunning.get() && (line = reader.readLine()) != null) {// 通过SSE发送日志数据到前端emitter.send(SseEmitter.event().data(line));}} catch (IOException e) {// 读取流时发生异常(如管道关闭),终止进程Process p = processHolder.get();if (p != null) {p.destroy(); // 尝试正常终止进程System.out.println("taskagent - 读取日志流异常: " + e.getMessage());}}}).start();// 3. 监听SSE连接的生命周期事件// 当前端正常关闭连接时触发(如关闭模态框)emitter.onCompletion(() -> {handleShutdown("正常关闭", processHolder.get()); // 统一处理关闭逻辑});// 当SSE连接发生错误时触发(如网络中断)emitter.onError(ex -> {handleShutdown("错误关闭: " + ex.getMessage(), processHolder.get());});// 当SSE连接超时时触发(需配置超时时间,默认30秒)emitter.onTimeout(() -> {handleShutdown("超时关闭", processHolder.get());});} catch (Exception e) {// 初始化进程时发生异常(如命令格式错误)isRunning.set(false);Process p = processHolder.get();if (p != null) p.destroy();System.out.println("taskagent - 初始化docker进程失败: " + e.getMessage());emitter.completeWithError(e); // 通知前端连接失败}});return emitter;}/*** 统一处理进程关闭逻辑* @param reason 关闭原因(用于日志输出)* @param process 待关闭的进程*/private void handleShutdown(String reason, Process process) {isRunning.set(false); // 停止日志读取循环if (process != null) {process.destroy(); // 发送中断信号(等效于Ctrl+C)System.out.println("taskagent - " + reason + ", 已终止docker进程"); // 关键日志输出点}}
现在我发现另一个问题。我的页面有很多容器,首先我打开nginx容器的日志,模态框被打开,实时日志能正常显示在页面上,当我关闭模态框,然后再次打开 nginx的日志,模态框被打开,依然能正常显示实时日志。 关键问题来了,然后我关闭模态框,接着打开 redis 的容器日志,日志没有显示在页面上了。
问题分析:
1. isRunning 标志的重置问题 - 可能
在 handleShutdown 方法里,isRunning 被设置为 false,不过在开启新的日志流时,没有将其重置为 true。这会让后续的日志读取循环无法正常运行。
解决办法:
在开启新的日志流之前,把 isRunning 重置为 true。
2. processHolder 未正确更新 - 可能
当切换容器查看日志时,processHolder 可能还保存着之前的 Process 对象,这会对新的日志读取产生影响。
解决办法:
在开启新的日志流之前,确保 processHolder 被清空。
3. webClient 连接问题(taskweb 端) - 可能
taskweb 端的 webClient 在处理多个连接时,可能会出现连接复用或者资源未正确释放的情况。
解决办法:
确保 webClient 在每次请求结束后都能正确释放资源。可以考虑为每个请求创建独立的 webClient 实例。
4. 前端 EventSource 管理问题 - 可能
前端在切换容器时,EventSource 可能没有正确关闭或者重新创建。
解决办法:
确保在切换容器时,EventSource 被正确关闭并重新创建。你的前端代码已经有了关闭逻辑,不过可以添加一些调试日志来确认是否正常执行。
最终修改的代码如下:
前端js代码:
// 显示实时日志$("#logModal").on('shown.bs.modal', function (event){// 从触发模态框的元素中获取 containerIdconst trigger = $(event.relatedTarget);const itemStr = trigger.data('item');if (!itemStr) {return;}const modal = $(this);let oldEventSource = modal.data('eventSource');if (oldEventSource) {oldEventSource.close();oldEventSource = null;modal.removeData('eventSource');}const item = JSON.parse(itemStr);let eventSource = new EventSource(`http://localhost:9998/middle/logs?containerId=${item.containerId}&ip=${item.ip}`);modal.data('eventSource', eventSource);const logContent = $('#logContent');logContent.empty(); // 清空日志eventSource.onmessage = function (event){const logLine = $('<div>').text(event.data)logContent.append(logLine);logContent[0].scrollTop = logContent[0].scrollHeight;};eventSource.onerror = function (error ) {console.log('日志连接失败: ', error);eventSource.close();eventSource = null; // 确保引用被清除logContent.text('日志获取失败,请检查容器常或网络连接')}})// 监听模态框关闭逻辑$("#logModal").on('hide.bs.modal', function (){const modal = $(this);let eventSource = modal.data('eventSource');if (eventSource) {eventSource.close(); // 关闭当前实例eventSource = null;modal.removeData('eventSource'); // 清除数据引用}$("#logContent").empty(); // 清空日志})
后端中间件代码:
service:
public SseEmitter getDockerLogs(PublishServer publishServer) {String url = taskagentConfig.getPrefixAddress(publishServer.getIp(), taskagentConfig.getGetDockerLogsUrl());SseEmitter emitter = new SseEmitter();executorService.submit(() -> {try {WebClient webClient = WebClient.builder().clientConnector(new ReactorClientHttpConnector(HttpClient.create())).build();webClient.get().uri(url, publishServer.getContainerId()).retrieve().bodyToFlux(String.class).subscribe(log -> {try {emitter.send(SseEmitter.event().data(log));} catch (IOException e) {emitter.completeWithError(e);}},emitter::completeWithError,emitter::complete);} catch (Exception e) {emitter.completeWithError(e);}});return emitter;}
后端服务代码:
// 使用原子应用存储进程对象,确保多线程环境下的可见性和原子性private final AtomicReference<Process> processHolder = new AtomicReference<>();// 使用原子布尔值控制日志读取循环,确保线程安全的状态变更private final AtomicBoolean isRunning = new AtomicBoolean(true);public SseEmitter getDockerLogs(PublishServerDto publishServerDto) {SseEmitter emitter = new SseEmitter();isRunning.set(true);processHolder.set(null); // 清空进程引用// 提交异步任务处理日志流executorService.submit(() -> {try {// 1. 执行 docker logs 命令,获取容器实时日志Process process = Runtime.getRuntime().exec("docker logs -f "+publishServerDto.getContainerId());processHolder.set(process); // 保存进程引用到原子容器中// 2. 启动独立线程读取日志输入流(避免主线程阻塞)new Thread(() -> {// 使用 try-with-resources 自动关闭流资源try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))){String line;// 循环读取日志,直到isRunning为false或读取到null(流结束)while (isRunning.get() && (line = reader.readLine()) != null) {// 通过 SSE 发送日志数据到taskwebemitter.send(SseEmitter.event().data(line));}} catch (IOException e) {// 读取流时发生异常(如管道关闭),终止进程Process p = processHolder.get();if (p != null) {p.destroy(); // 尝试正常终止进程log.error("taskagent - 读取流时发生异常", e.getMessage());}}}).start();// 3. 监听SSE连接的生命周期事件// 当前端正常关闭连接时触发emitter.onCompletion(() -> {// 统一处理关闭逻辑handleShutdown("正常关闭", processHolder.get());});// 当SSE连接发生错误时触发(如网络中断)emitter.onError(ex -> {handleShutdown("错误关闭: "+ex.getMessage(), processHolder.get());});// 当SSE连接超时时触发emitter.onTimeout(() -> {handleShutdown("超时关闭", processHolder.get());});} catch (IOException e) {// 初始化进程时发生异常isRunning.set(false);Process p = processHolder.get();if (p != null) {p.destroy();}log.error("taskagent - 初始化 docker 进程失败:", e.getMessage());emitter.completeWithError(e); // 通知taskweb连接失败}});return emitter;}/*** 统一处理进程关闭逻辑* @param reason 关闭原因(用于日志输出)* @param process 待关闭的进程*/private void handleShutdown(String reason, Process process) {isRunning.set(false); // 停止日志读取循环if (process != null) {process.destroy(); // 发送中断信号(等效于Ctrl+C)log.info("taskagent - " + reason + ", 已终止docker进程");}}
修改成上面之后,前端实时显示日志的效果就全部正常了。
相关文章:
使用 SSE + WebFlux 推送日志信息到前端
为什么使用 SSE 而不使用 WebSocket, 请看 SEE 对比 Websocket 的优缺点。 特性SSEWebSocket通信方向单向(服务器→客户端)双向(全双工)协议基于 HTTP独立协议(需 ws:// 前缀)兼容性现代浏览器(…...
Java多线程同步有哪些方法?
大家好,我是锋哥。今天分享关于【Java多线程同步有哪些方法?】面试题。希望对大家有帮助; Java多线程同步有哪些方法? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在 Java 中,多线程同步是确保多个线程在访问共享资源时不会…...
Java—数 组
数组就是一个容器,用来存一批同种类型的数据。 一、静态初始化数组 1.1 定义方式 语法: 完整格式:数据类型 [ ] 数组名 new 数据类型 []{ 元素 1 ,元素 2 ,元素3… };简化格式:数据类型 [ ] 数组名 {…...
iOS/Android 使用 C++ 跨平台模块时的内存与生命周期管理
在移动应用开发领域,跨平台开发已经成为一种不可忽视的趋势。随着智能手机市场的持续扩张,开发者需要同时满足iOS和Android两大主流平台的需求,而这往往意味着重复的工作量和高昂的维护成本。跨平台开发的目标在于通过一套代码库实现多平台的支持,从而降低开发成本、加速产…...
为什么vue的key值,不用index?
在 Vue 中,key 的作用是帮助框架高效地识别和复用 DOM 节点或组件实例。使用数组索引 (index) 作为 key 值可能会导致以下问题,因此通常不建议这样做: 1. 列表数据变化时,可能导致错误的 DOM 复用 问题:当列表的顺序…...
Hi3516CV608 超高清智慧视觉 SoC 芯片 可提供开发资料
Hi3516CV608 超高清智慧视觉SoC 产品简介 总体介绍 Hi3516CV608是一颗面向消费类市场的IPC SoC,在新一代视频编解码标准、网络安全、隐私保护和人工智能方面引领行业发展。主要应用于室内外场景下的云台机、枪机、球机、枪球一体机、双目长短焦机等产品形态&#…...
Flink部署与应用——部署方式介绍
引入 我们通过Flink相关论文的介绍,对于Flink已经有了初步理解,这里简单的梳理一下Flink常见的部署方式。 Flink 的部署方式 StandAlone模式 介绍 StandAlone模式是Flink框架自带的分布式部署模式,不依赖其他的资源调度框架,…...
数据挖掘技术与应用课程论文——数据挖掘中的聚类分析方法及其应用研究
数据挖掘中的聚类分析方法及其应用研究 摘要 聚类分析是数据挖掘技术中的一个重要组成部分,它通过将数据集中的对象划分为多个组或簇,使得同一簇内的对象具有较高的相似性,而不同簇之间的对象具有较低的相似性。 本文系统地研究了数据挖掘中的多种聚类分析方法及其应用。首先…...
SIEMENS PLC程序解读 ST 语言 车型识别
1、ST程序代码 IF #Type1_MIX < #CFG_Type.Type.CT AND #CFG_Type.Type.CT < #Type1_MAX AND #CFG_Type.Type.CT<>0 THEN#Type[1] : 1;FOR #I : 0 TO 39 DOIF #CFG_Type.Type.CT/10 (#Type1_MIX 10 * #I)/10 THEN#Sub_Type."1"[#I 1] : 1;END_IF; E…...
神经网络基础[损失函数,bp算法,梯度下降算法 ]
关于神经网络的基础的概念可以看我前面的文章 损失函数 在深度学习中, 损失函数是用来衡量模型参数的质量的函数, 衡量的方式是比较网络输出和真实输出的差异 作用:指导模型的训练过程,通过反向传播算法计算梯度,从而更新网络的参数,最终使…...
python打印颜色(python颜色、python print颜色、python打印彩色文字、python print彩色、python彩色文字)
文章目录 python怎么打印彩色文字1. 使用ANSI转义码:2. 使用colorama库(更好的跨平台支持):3. 使用termcolor库: python怎么打印彩色文字 在Python中打印彩色文字有几种方法: 1. 使用ANSI转义码ÿ…...
数字域残留频偏的补偿原理
模拟域的频谱搬移一般通过混频器实现。一般情况下模拟域调整完频偏后数字域还会存在一部分残留频偏这部分就需要在数字域补偿。原理比较简单本文进行下粗略总结。首先我们需要了解下采样具体可参考下信号与系统笔记(六):采样 - 知乎。 采样前和采样后,角…...
Linux文件管理2
Linux 文件管理是系统操作的核心内容之一,涉及文件和目录的创建、删除、移动、查看、权限管理等操作。以下是 Linux 文件管理的核心知识点和常用操作总结: 一、文件系统结构 Linux 文件系统采用 树形结构,以 /(根目录࿰…...
C++----模拟实现string
模拟实现string,首先我们要知道成员变量有哪些: class _string{private:char* _str;size_t capacity;//空间有多大size_t size;//有效字符多少const static size_t npos;};const size_t _string::npos-1;//static在外面定义不需要带static,np…...
Python torch.optim.lr_scheduler 常用学习率调度器使用方法
在看学习率调度器之前,我们先看一下学习率的相关知识: 学习率 学习率的定义 学习率(Learning Rate)是深度学习中一个关键的超参数,它决定了在优化算法(如梯度下降法)更新模型参数时࿰…...
从零开始学Python游戏编程39-碰撞处理1
在《从零开始学Python游戏编程38-精灵5》代码的基础上,添加两个敌人的防御塔,玩家的坦克无法移动到防御塔所在的空格中,如图1所示。 图1 游戏中的碰撞处理 1 游戏中空格的坐标 在《从零开始学Python游戏编程36-精灵3》中提到,可…...
同步定时器的用户数要和线程组保持一致,否则jmeter会出现接口不执行’stop‘和‘×’的情况
调试压测时发现了一个问题就是线程计划总是出现‘stop’的按钮无法执行完毕 发现时同步定时器导致的,就是有接口使用了同步定时器,但是这个同步定时器的用户数量设置的<线程组用户数量时,会出现执行无法结束的情况,如下…...
如何在Linux用libevent写一个聊天服务器
废话少说,先看看思路 因为libevent的回调机制,我们可以借助这个机制来创建bufferevent来实现用户和用户进行通信 如果成功连接后我们可以直接在listener回调函数里创建一个bufferevent缓冲区,并为每个缓冲区设置相应的读回调和事件回调&…...
Virtuoso ADE采用Spectre仿真中出现MOS管最小长宽比满足要求依然报错的情况解决方法
在ADE仿真中错误问题如下: ERROR (CMI-2440): "xxx.scs" 46338: I2.M1: The length, width, or area of the instance does not fit the given lmax-lmin, wmax-wmin, or areamax-areamin range for any model in the I2.M3.nch_hvt group. The channel w…...
防火墙原理与应用总结
防火墙介绍: 防火墙(Firewall)是一种网络安全设备,其核心目标是通过分析数据包的源地址、端口、协议等内容,保护一个网络区域免受来自另一个网络区域的网络攻击和网络入侵行为,同时允许合法流量自由通行。…...
Graph Database Self-Managed Neo4j 知识图谱存储实践2:通过官方新手例子入门(未完成)
官方入门例子:neo4j-graph-examples/get-started: An introduction to graph databases and Neo4j for new users 官方例子仓库:https://github.com/neo4j-graph-examples 下载数据 git clone https://github.com/neo4j-graph-examples/get-started …...
GIT下载步骤
git官方链接: 添加链接描述...
C++中的vector和list的区别与适用场景
区别 特性vectorlist底层实现动态数组双向链表内存分配连续内存块非连续内存块随机访问支持,通过索引访问,时间复杂度O(1)不支持,需遍历,时间复杂度O(n)插入/删除末尾操作效率高,时间复杂度O(1)任意位置操作效率高&am…...
软件测试入门学习笔记
今天学习新知识,软件测试。 什么是软件测试? 使用人工和自动手段来运行或测试某个系统的过程,目的在于检验它是否满足规定的需求或弄清实际结果与预期结果之间的差别。 软件测试的目的? 1)为了发现程序࿰…...
2025年深度学习模型发展全景透视(基于前沿技术突破与开源生态演进的交叉分析)
2025年深度学习模型发展全景透视 (基于前沿技术突破与开源生态演进的交叉分析) 一、技术突破与能力边界拓展 智能水平跃升 2025年开源模型如Meta Llama-4、阿里Qwen2.5-VL参数规模突破1300亿,在常识推理能力测试中首次超越人类基准线7.2%谷歌…...
时间复杂度分析
复杂度分析的必要性: 当给我们一段代码时,我们是以什么准则来判断代码效率的高低呢?每一段代码都会消耗一段时间,或占据一段数据空间,那么自然是在实现相同功能的情况下,代码所耗时间最少,所占…...
BGE-m3 和 BCE-Embedding 模型对比分析
以下是对 BGE-m3 和 BCE-Embedding 模型在 embedding 领域的多维度对比分析,基于公开的技术文档和实验数据: 1. 基础信息对比 维度BGE-m3 (智源研究院)BCE-Embedding (网易)发布时间2024 年 1 月2023 年 9 月模型架构Transformer-basedTransformer-base…...
题目 3320: 蓝桥杯2025年第十六届省赛真题-产值调整
题目 3320: 蓝桥杯2025年第十六届省赛真题-产值调整 时间限制: 2s 内存限制: 192MB 提交: 549 解决: 122 题目描述 偏远的小镇上,三兄弟共同经营着一家小型矿业公司 “兄弟矿业”。公司旗下有三座矿山:金矿、银矿和铜矿,它们的初始产值分别用…...
计算机组成原理第二章 数据的表示和运算——2.1数制与编码
计算机组成原理第二章 数据的表示和运算——数制与编码 一、基本概念与核心知识点 1.1 数制系统基础 1.1.1 进位计数制 定义:以固定基数(如2、8、10、16)表示数值的系统核心要素: 基数(R):允…...
基于归纳共形预测的大型视觉-语言模型中预测集的**数据驱动校准**
摘要 本研究通过分离共形预测(SCP)框架,解决了大型视觉语言模型(LVLMs)在视觉问答(VQA)任务中幻觉缓解的关键挑战。虽然LVLMs在多模态推理方面表现出色,但它们的输出常常表现出具有…...
Golang | 自行实现并发安全的Map
核心思路,读写map之前加锁!哈希思路,大map化分为很多个小map...
【Python数据库编程实战】从SQL到ORM的完整指南
目录 前言技术背景与价值当前技术痛点解决方案概述目标读者说明 一、技术原理剖析核心概念图解核心作用讲解关键技术模块说明技术选型对比 二、实战演示环境配置要求核心代码实现案例1:SQLite基础操作案例2:MySQL连接池案例3:SQLAlchemy ORM …...
深入剖析扣子智能体的工作流与实战案例
前面我们已经初步带大家体验过扣子工作流,工作流程是 Coze 最为强大的功能之一,它如同扣子中蕴含的奇妙魔法工具,赋予我们的机器人处理极其复杂问题逻辑的能力。 这篇文章会带你更加深入地去理解并运用工作流解决实际问题 目录 一、工作流…...
【计算机网络】IP地址
IPv4 五类地址 1.0.0.0 ~ 126.255.255.255A类子网8位,主机24位128.0.0.0 ~ 191.255.255.255B类子网16位,主机16位192.0.0.0 ~ 223.255.255.255C类子网24位,主机8位224.0.0.0 ~ 239.255.255.255D类不分网络地址和主机地址,作为组播…...
基于CATIA参数化管道建模的自动化插件开发实践——NX建模之管道命令的参考与移植
引言 在机械设计领域,CATIA作为行业领先的CAD软件,其强大的参数化建模能力备受青睐。本文介绍如何利用Python的PySide6框架与CATIA二次开发技术,开发一款智能管状体生成工具。该工具借鉴了同类工业软件NX的建模的管道命令,通过Py…...
运维之SSD硬盘(SSD hard Drive for Operation and Maintenance)
背景 SSD的产生背景是计算技术发展和市场需求驱动的结果。早期计算机使用磁芯存储器,后来被半导体存储器取代,提高了速度和可靠性。随着电子设备小型化,对轻便、低功耗存储器的需求增长,SSD因无机械部件、速度快、耗电少而受到关…...
基于javaweb的SSM+Maven红酒朔源管理系统设计与实现(源码+文档+部署讲解)
技术范围:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…...
HTML 地理定位(Geolocation)教程
HTML 地理定位(Geolocation)教程 简介 HTML5 的 Geolocation API 允许网页应用获取用户的地理位置信息。这个功能可用于提供基于位置的服务,如导航、本地搜索、天气预报等。本教程将详细介绍如何在网页中实现地理定位功能。 工作原理 浏览器可以通过多种方式确定…...
RHEL与CentOS:从同源到分流的开源操作系统演进
RHEL与CentOS:从同源到分流的开源操作系统演进 一、核心关系:源代码的重构与社区化 RHEL(Red Hat Enterprise Linux)与CentOS(Community ENTerprise Operating System)的关系可以概括为“同源异构”。RHE…...
架构师面试(三十六):广播消息
题目 在像 IM、短视频、游戏等实时在线类的业务系统中,一般会有【广播消息】业务,这类业务具有瞬时高流量的特点。 在对【广播消息】业务实现时通常需要同时写 “系统消息库” 和更新用户的 “联系人库” 的操作,用户的联系人表中会有未读数…...
Spine 动画教程:皮肤制作
一、前言 搁了很久的抖音直播小玩法开发,最近又让我想起来了。由于是初次尝试,所以我将开发费用的预算降到为零。不但不买服务器采用 UnitySDK 的指令直推,而且游戏的资产也用 AI 生成,主打省时又省钱。 但是图片有了࿰…...
Rust 学习笔记:函数和控制流
Rust 学习笔记:函数和控制流 Rust 学习笔记:函数和控制流函数(Function)语句和表达式带返回值的函数注释控制流if 表达式使用 else if 处理多个条件在 let 语句中使用 if循环loop从循环中返回值循环标签消除多个循环之间的歧义带 …...
探秘LLM推理模型:hidden states中藏着的self verification的“钥匙”
推理模型在数学和逻辑推理等任务中表现出色,但常出现过度推理的情况。本文研究发现,推理模型的隐藏状态编码了答案正确性信息,利用这一信息可提升推理效率。想知道具体如何实现吗?快来一起来了解吧! 论文标题 Reasoni…...
《Learning Langchain》阅读笔记8-RAG(4)在vector store中存储embbdings
什么是 vector store? 与专门用于存储结构化数据(如 JSON 文档或符合关系型数据库模式的数据)的传统数据库不同,vector stores处理的是非结构化数据,包括文本和图像。像传统数据库一样,vector stores也能执…...
【C/C++】深入理解指针(五)
文章目录 深入理解指针(五)1.回调函数是什么?2.qsort使用举例2.1 使用qsort函数排序整型数据强调 2.2 使用qsort排序结构数据 3.qsort函数的模拟实现 深入理解指针(五) 1.回调函数是什么? 回调函数就是⼀个通过函数指针调⽤的函数。 如果你把函数的指…...
【vue】【element-plus】 el-date-picker使用cell-class-name进行标记,type=year不生效解决方法
typedete,自定义cell-class-name打标记效果如下: 相关代码: <el-date-pickerv-model"date":clearable"false":editable"false":cell-class-name"cellClassName"type"date"format&quo…...
RocketMQ 主题与队列的协同作用解析(既然队列存储在不同的集群中,那要主题有什么用呢?)---管理命令、配置安装
学习之前呢需要会使用linux的基础命令 一.RocketMQ 主题与队列的协同作用解析 在 RocketMQ 中,主题(Topic)与队列(Queue)的协同设计实现了消息系统的逻辑抽象与物理存储分离。虽然队列实际存储在不同集群的 B…...
解决视频处理中的 HEVC 解码错误:Could not find ref with POC xxx【已解决】
问题描述 今天在使用 Python 处理视频时遇到了以下错误: [hevc 0x7f8a1d02b7c0] Could not find ref with POC 33之前没接触过视频处理,查了一下,这个错误通常发生在处理 HEVC(H.265)编码 的视频时,原因…...
NEGATIVE LABEL GUIDED OOD DETECTION WITH PRETRAINED VISION-LANGUAGE MODELS
1. 介绍: 这篇论文也是基于CLIP通过后处理的方法实现的OOD的检测,但是设计点在于,之前的方法是使用的ID的类别,这篇工作是通过添加一些在语义上非常不同于ID的类别的外分布类来做的OOD检测。 CLIP做OOD检测的这个系列里面我看的以及记录的第一篇就是MCM的方法,这也是确实是…...
Appium自动化 -- 环境安装
1.安装Appium-Python-Clientpip install Appium-Python-Client 2.AndroidSdk安装和环境配置 AndroidSdk下载地址:https://www.androiddevtools.cn/# 下载后解压 SDK Manager.exe 安装sdk tools、sdk plaform-tools、sdk build-tools AndroidSDK 环境变量配…...