Spring Cloud Gateway实战WebFlux解析请求体及抛出指定错误代码和信息
概述
基于Spring Cloud开发微服务时,使用Spring Cloud原生自带的Gateway作为网关,所有请求都需要经过网关服务转发。
为了防止恶意请求刷取数据,对于业务请求需要进行拦截,故而可在网关服务增加拦截过滤器。基于此,有如下源码:
@Slf4j
@Component
public class BlockListFilter extends AbstractGatewayFilterFactory {private static final String DIALOG_URI = "/dialog/nextQuestion";@Resourceprivate AssessmentBlockListService assessmentBlockListService;@Lazy@Resourceprivate RemoteUserService remoteUserService;@Lazy@Resourceprivate RemoteRcService remoteRcService;@Lazy@Autowiredprivate RemoteOAuthService remoteOAuthService;@Value("${blockListSwitch:true}")private Boolean blockListSwitch;@Overridepublic GatewayFilter apply(Object config) {return (exchange, chain) -> {ServerHttpResponse response = exchange.getResponse();response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);ServerHttpRequest serverHttpRequest = exchange.getRequest();String uri = serverHttpRequest.getURI().getPath();HttpHeaders httpHeaders = serverHttpRequest.getHeaders();// 只处理相关urlif (blockListSwitch && StringUtils.equalsIgnoreCase(uri, DIALOG_URI)) {String token = httpHeaders.getFirst("Authorization");String pureToken = StringUtils.replaceIgnoreCase(token, "Bearer ", "");BaseUserInfo baseUserInfo = UserUtil.getBaseUserInfo(pureToken);if (baseUserInfo == null) {log.warn("传入的token非法:{}", token);return chain.filter(exchange);}// 从JWT token中解析用户信息(不含mobile等敏感信息)String channel = baseUserInfo.getChannel();String userKey = baseUserInfo.getUserkey();UserParam userParam = new UserParam();userParam.setKey(userKey);userParam.setChannel(channel);// Feign远程请求user服务获取mobile信息Response<UserAccountVO> userAccountResponse = remoteUserService.baseQuery(userParam);if (userAccountResponse.getCode() != 0) {log.warn("未获取到userKey={}的用户信息!", userKey);return chain.filter(exchange);}UserAccountVO userAccountVO = userAccountResponse.getData();log.info("blocklist filter user={}", JsonUtil.beanToJson(userAccountVO));String mobile = userAccountVO.getMobile();if (StringUtils.isNotBlank(userKey)) {// 具体的拦截业务逻辑this.process(uri, userKey, mobile, channel, pureToken);}return chain.filter(exchange);}return chain.filter(exchange);};}private String process(String uri, String userKey, String mobile, String channel, String token) {DetectUserDTO detectUserDTO = new DetectUserDTO();detectUserDTO.setChannel(channel);detectUserDTO.setMobile(mobile);detectUserDTO.setUserKey(userKey);detectUserDTO.setUri(uri);Response<DetectUserVO> detectUserVOResponse = remoteRcService.detectUser(detectUserDTO);if (detectUserVOResponse.getCode() != 0) {log.warn("mobile={} 风控接口返回异常:{}", mobile, JsonUtil.beanToJson(detectUserVOResponse));return null;}DetectUserVO detectUserVO = detectUserVOResponse.getData();if (detectUserVO.getIsInAllowList()) {log.info("在白名单中,放行");} else if (detectUserVO.getIsInBlockList()) {log.info("在黑名单中,拦截处理...");this.logout(token);return "当前手机号问诊次数已达今日上限!";}return null;}private void logout(String token) {// 强制下线,踢出登录态LogoutDto logoutDto = new LogoutDto();logoutDto.setToken(pureToken);remoteOAuthService.logout(logoutDto);}
}
上面的代码只是实现:判断流量请求URL,进而判断用户是否触发风控黑名单机制,如果触发黑名单,则踢出登录态强制用户下线,即用户不能使用App。
需求
异常捕获
上面的踢出登录态做法过于简单粗暴,App有若干个功能模块,某些请求URL触发黑名单机制,就强制用户下线,不能使用App其他功能。应该允许用户使用除了触发黑名单模块的其他模块功能。当然,对于是否强制下线的交互设计,每个人有每个人的看法。
就我遇到的情况来细说,痛点在于,前端(所谓大前端概念,含iOS和安卓App)同学针对此种情况直接返回统一的错误页:【抱歉,请返回再试一次!】,并且这个页面没有任何UI设计,仅仅是前面提到的一句提示文案。用户看到这个文案,会重新登录(因为后端做了踢出登录态逻辑),App成功后经过网关校验,发现用户依旧触发黑名单,然后再次踢出登录态,陷入死循环。
基于此,做如下改造:不踢出登录态,可以继续使用其他模块功能,在使用某个触发黑名单机制的模块时后端返回错误码,前端弹窗提示【无法使用此功能】。
故而对上面的代码进行调整:
String msg = this.process(uri, userKey, mobile, channel, pureToken,);
if (StringUtils.isNotBlank(msg)) {return Mono.error(new CustomException(BlockTypeEnum.getCodeByMsg(msg), msg));
}
本地启动Gateway网关服务,postman模拟请求,得到接口返回数据:
咦?怎么Code不是枚举类里定义的错误文案msg对应的错误码,而是500呢?
看代码,发现有个继承DefaultErrorWebExceptionHandler的JsonErrorWebExceptionHandler类,那这个类也需要调整:
@Slf4j
public class JsonErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {public JsonErrorWebExceptionHandler(ErrorAttributes errorAttributes,ResourceProperties resourceProperties,ErrorProperties errorProperties,ApplicationContext applicationContext) {super(errorAttributes, resourceProperties, errorProperties, applicationContext);}@Overrideprotected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {Map<String, Object> errorAttributes = new HashMap<>(8);Throwable error = super.getError(request);errorAttributes.put("message", error.getMessage());if (error instanceof CustomException) {errorAttributes.put("code", ((CustomException) error).getCode());} else {errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());}errorAttributes.put("method", request.methodName());errorAttributes.put("path", request.path());log.warn("网关异常,path:{},method:{},message:{}", request.path(), request.methodName(), error.getMessage());return errorAttributes;}@Overrideprotected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);}@Overrideprotected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {// 这里其实可以根据errorAttributes里面的属性定制HTTP响应码return HttpStatus.INTERNAL_SERVER_ERROR;}
}
经过调整后,断点调试,万事大吉:
但是!!!
后来在和前端联调时才发现有点不对劲:
如上图,枚举类里有几个code,每个code都有对应的系统功能模块黑名单msg。之前没有注意到右上角的接口状态码是500,这个500表示请求未成功。也就是说,接口未成功,接口响应码是自定义的code也没啥用。status必须得是200、204、202这种才行。
总结一下:虽然使用Mono.error()
返回业务自定义错误信息,但错误代码9996被转为500。
继续排查。找到下面参考2里的文章,做如下调整:
String msg = this.process(uri, userKey, mobile, channel, pureToken,);
if (StringUtils.isNotBlank(msg)) {// 对应200,表明接口请求是成功的,但是触发业务异常错误码exchange.getResponse().setStatusCode(HttpStatus.OK);return exchange.getResponse().writeWith(Flux.just(exchange.getResponse().bufferFactory().wrap(JSON.toJSONString(msg).getBytes())));
}
调试结果,右上角的Status变成200:
注意看,Postman可以返回中文msg。
发布测试环境后,在Chrome浏览器里却发现返回数据乱码!?
IDEA调整截图如上没有乱码问题,console控制台打印也是正常:
乱码不是什么大问题,研发这么多年遇到无数次。继续排查,做如下调整解决问题:
return exchange.getResponse().writeWith(Flux.just(exchange.getResponse().bufferFactory().wrap(JSON.toJSONString(msg).getBytes(StandardCharsets.UTF_8))));
获取请求体
前面提到,我们的App有几个功能模块,其中一个模块的请求全部是dialog/nextQuestion
接口。用户恶意刷数据,多次请求此接口就会触发黑名单。但是在我们的交互设计上,又希望用户从其他功能模块切换到此功能时,可以请求一次此接口。那怎么判断是第一次呢?那就需要解析requestBody。
根据下面的参考1文章,增加如下代码:
public String resolveBodyForDialog(ServerHttpRequest serverHttpRequest) {String uri = serverHttpRequest.getURI().getPath();// 只有某些请求才解析if (!StringUtils.equalsAnyIgnoreCase(uri, "dialog/nextQuestion")) {return "";}StringBuilder sb = new StringBuilder();Flux<DataBuffer> body = serverHttpRequest.getBody();body.subscribe(buffer -> {byte[] bytes = new byte[buffer.readableByteCount()];buffer.read(bytes);DataBufferUtils.release(buffer);String bodyString = new String(bytes, StandardCharsets.UTF_8);sb.append(bodyString);});return sb.toString();
}
本地调试时没有问题,可以获取到requestBody:
但是测试环境却又问题:
详细的错误日志:
500 Server Error for HTTP POST "/api/open/dialog/nextQuestion"
io.netty.handler.codec.EncoderException: io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1
at io.netty.handler.codec.MessageToMessageEncoder.write(MessageToMessageEncoder.java:107)
at io.netty.channel.CombinedChannelDuplexHandler.write(CombinedChannelDuplexHandler.java:348)
at io.netty.channel.AbstractChannelHandlerContext.invokeWrite0(AbstractChannelHandlerContext.java:716)
at io.netty.channel.AbstractChannelHandlerContext.invokeWrite(AbstractChannelHandlerContext.java:708)
at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:791)
at io.netty.channel.AbstractChannelHandlerContext.write(AbstractChannelHandlerContext.java:701)
at reactor.netty.channel.MonoSendMany$SendManyInner.run(MonoSendMany.java:286)
at reactor.netty.channel.MonoSendMany$SendManyInner.trySchedule(MonoSendMany.java:368)
at reactor.netty.channel.MonoSendMany$SendManyInner.onSubscribe(MonoSendMany.java:221)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90)
at reactor.core.publisher.FluxContextStart$ContextStartSubscriber.onSubscribe(FluxContextStart.java:97)
at reactor.core.publisher.MonoJust.subscribe(MonoJust.java:54)
at reactor.core.publisher.MonoSubscriberContext.subscribe(MonoSubscriberContext.java:47)
at reactor.core.publisher.FluxSourceMonoFuseable.subscribe(FluxSourceMonoFuseable.java:38)
at reactor.core.publisher.FluxMapFuseable.subscribe(FluxMapFuseable.java:63)
at reactor.core.publisher.Flux.subscribe(Flux.java:7921)
at reactor.netty.channel.MonoSendMany.subscribe(MonoSendMany.java:81)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.drain(MonoIgnoreThen.java:153)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:56)
at reactor.core.publisher.Mono.subscribe(Mono.java:3848)
at reactor.netty.NettyOutbound.subscribe(NettyOutbound.java:305)
at reactor.core.publisher.MonoSource.subscribe(MonoSource.java:51)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:52)
at reactor.netty.http.client.HttpClientConnect$HttpIOHandlerObserver.onStateChange(HttpClientConnect.java:441)
at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:470)
at reactor.netty.resources.PooledConnectionProvider$DisposableAcquire.onStateChange(PooledConnectionProvider.java:512)
at reactor.netty.resources.PooledConnectionProvider$PooledConnection.onStateChange(PooledConnectionProvider.java:451)
at reactor.netty.channel.ChannelOperationsHandler.channelActive(ChannelOperationsHandler.java:62)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:225)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:211)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelActive(AbstractChannelHandlerContext.java:204)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelActive(CombinedChannelDuplexHandler.java:414)
at io.netty.channel.ChannelInboundHandlerAdapter.channelActive(ChannelInboundHandlerAdapter.java:69)
at io.netty.channel.CombinedChannelDuplexHandler.channelActive(CombinedChannelDuplexHandler.java:213)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:225)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:211)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelActive(AbstractChannelHandlerContext.java:204)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelActive(DefaultChannelPipeline.java:1396)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:225)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelActive(AbstractChannelHandlerContext.java:211)
at io.netty.channel.DefaultChannelPipeline.fireChannelActive(DefaultChannelPipeline.java:906)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.fulfillConnectPromise(AbstractEpollChannel.java:618)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.finishConnect(AbstractEpollChannel.java:651)
at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.epollOutReady(AbstractEpollChannel.java:527)
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:422)
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:333)
at io.netty.util.concurrent.SingleThreadEventExecutor$5.run(SingleThreadEventExecutor.java:906)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
具体研究这篇文章,做如下调整:
String uri = serverHttpRequest.getURI().getPath();
// 只有某些请求才解析
if (!StringUtils.equalsAnyIgnoreCase(uri, DIALOG_URI)) {return "";
}
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());bodyRef.set(charBuffer.toString());
});
return bodyRef.get();
上面的报错消失。问题虽然是解决,但是不明就里。
后面又发现一个乱码问题,针对如下requestBody:
{"stateId": "DASHBOARD","answer": {"transitionId": "GET_HEALTH_ADVICE","label": "开始评估症状"}
}
bodyRef.get()
获取到的中文数据乱码。参考3,解决方法:
String encoding = System.getProperty("file.encoding");
CharBuffer charBuffer = Charset.forName(encoding).decode(buffer.asByteBuffer());
bodyRef.set(charBuffer.toString());
参考
- Spring Cloud Gateway读取RequestBody数据
- 使用Spring Cloud-Gateway WebFlux抛出指定错误代码和信息
- 从String获得ByteBuffer、从ByteBuffer获得CharBuffer的正确姿势
相关文章:

Spring Cloud Gateway实战WebFlux解析请求体及抛出指定错误代码和信息
概述 基于Spring Cloud开发微服务时,使用Spring Cloud原生自带的Gateway作为网关,所有请求都需要经过网关服务转发。 为了防止恶意请求刷取数据,对于业务请求需要进行拦截,故而可在网关服务增加拦截过滤器。基于此,有…...

Servlet开发-通过代码案例熟悉HttpServletRequest类
关于Servlet开发的流程推荐看servlet开发-通过Tomcat部署一个简单的webapp Servlet开发与idea集成的插件安装推荐看idea集成tomcat(Smart Tomcate插件安装) postman(第三方创建HTTP请求工具)的安装推荐看创建HTTP请求的几种方式…...

离线环境harbor 搭建及使用
一 摘要 本文主要介绍harbor 的安装及使用。 二 环境信息及部署图 2.1 环境信息 名称版本备注操作系统centos7.9容器docker 23.0.1harbor2.7代理nginx待补充 2.2 架构图 说明: 1.harbor 核心服务里有个nginx ,也可以用该nginx 做代理 2.proxy-ngin…...

华为杯数学建模比赛经验分享
再过一周左右,第二十届华为杯数学建模比赛就要开赛了,所以今天分享一下个人数学建模比赛的经验。 今天给大家分享一期关于华为杯数学建模比赛的经验分享,我将从以下三个方面展开说明: (1)如何准备数学建模比赛&#x…...

c语言 - 实现每隔1秒向文件中写入当前系统时间
实现思路 主要是通过库函数和结构体获取当前系统时间(年月日和时分秒)保存到变量里,然后通过格式化输出函数将当前系统时间输出到文件中去。 但是需要注意的是题目要求每隔 1 s对系统时间进行输出,所以需要加入 sleep()函数进行调…...

使用cpolar端口映射的方法轻松实现在Linux环境下SVN服务器的搭建与公网访问
文章目录 前言1. Ubuntu安装SVN服务2. 修改配置文件2.1 修改svnserve.conf文件2.2 修改passwd文件2.3 修改authz文件 3. 启动svn服务4. 内网穿透4.1 安装cpolar内网穿透4.2 创建隧道映射本地端口 5. 测试公网访问6. 配置固定公网TCP端口地址6.1 保留一个固定的公网TCP端口地址6…...

前后台分离开发 YAPI平台 前端工程化之Vue-cli
目录 YAPI介绍前端工程化之Vue-cli前端工程化简介前端工程化入门——Vue-cli环境准备Vue项目简介创建Vue项目vue项目目录结构介绍vue项目运行方法 Vue项目开发流程 前后台混合开发这种开发模式有如下缺点: 沟通成本高:后台人员发现前端有问题࿰…...
【js/es6】合集
目录 随机生成字符串每十个字符换行 随机生成字符串 function generateRandomAlphaNum(len) {var rdmString "";for (; rdmString.length < len; rdmString Math.random().toString(36).substr(2));return rdmString.substr(0, len); }每十个字符换行 string.…...
Nginx modules build fail:field ‘pkt6’ has incomplete type
前言 最近升级Nginx 1.24.0,编译第三方module出错: /usr/src/nginx_modules/ngx_json_post_module/src/ngx_json_post_module.c In file included from src/event/ngx_event.h:526,from src/http/ngx_http_upstream.h:14,from src/http/ngx_http.h:34,…...

电脑突然提示mfc140u.dll丢失,缺失mfc140u.dll无法运行程序的解决方法
在当今信息化社会,电脑已经成为我们生活和工作中不可或缺的一部分。然而,随着技术的不断发展,电脑也会出现各种问题。其中,最常见的问题之一就是“mfc140u.dll丢失”。那么,当我们遇到这个问题时,应该如何解…...

国庆day1
消息队列 代码 发送 #include<myhead.h> //声明一个消息结构体 typedef struct {long msgtype; //消息类型char data[1024]; //消息正文 }Msg_s; #define SIZE sizeof(Msg_s)-sizeof(long) //消息正文的大小 int main(int argc, const char *argv[]) {key_t key; /…...

从零开始之了解电机及其控制(11)实现空间矢量调制
广泛地说,空间矢量调制只是将电压矢量以及磁场矢量在空间中调制到任意角度,通常同时最大限度地利用整个电压范围。 其他空间矢量调制模式确实存在,并且根据您最关心的内容,它们可能值得研究。 如何实际执行这种所谓的交替反向序列…...
如何将html转换成markdown
什么是Turndown “Turndown” 是一个用于将 HTML 转换为 Markdown 的 JavaScript 库。它通常用于将富文本内容从网页或其他 HTML 格式转换为纯文本 Markdown 格式,以便在不同平台上显示或存储。 如果在有node环境的情况下要使用 Turndown,首先需要将它…...

【VUE复习·3】@keyup.xxx 键盘事件触发函数(单按键 or 组合按键触发)
总览 1.keyup.xxx or keydown.xxx 单按键触发 2.组合按键触发 一、keyup.xxx or keydown.xxx 1.用法 在我们使用 keyup.enter 时,那么我们可以这样写: <div><input type"text" placeholder"按下回车键以确定..." keyu…...

IntelliJ IDEA学习总结(3)—— IntelliJ IDEA 常用快捷键(带动图演示)
一、构建/编译 Ctrl + F9:构建项目 该快捷键,等同于菜单【Build】—>【Build Project】 执行该命令后,IntelliJ IDEA 会编译项目中所有类,并将编译结果输出到out目录中。IntelliJ IDEA 支持增量构建,会在上次构建的基础上,仅编译修改的类。 Ctrl + Shift + F9:重新编…...

巨人互动|Facebook海外户Facebook的特点优势
Facebook作为全球最大的社交媒体平台之一,同时也是最受欢迎的社交网站之一,Facebook具有许多独特的特点和优势。本文小编将说一些关于Facebook的特点及优势。 1、全球化 Facebook拥有数十亿的全球用户,覆盖了几乎所有国家和地区。这使得人们…...

(vue3)create-vue 组合式API
优势: 更易维护:组合式api,更好的TS支持 之前是选项式api,现在是组合式,把同功能的api集合式管理 复用功能封装成一整个函数 更快的速度 更小的体积 更优的数据响应式:Proxy create-vue 新的脚手架工…...

【如何看待Unity收费】对标中小公司的待就业者的该如何做
👨💻个人主页:元宇宙-秩沅 👨💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨💻 本文由 秩沅 原创 👨💻 收录于专栏:Uni…...

c++迷宫小游戏
一、总结 一句话总结: 显示:根据map数组输出图像 走动:修改map数组的值,每走一步重新刷新一下图像就好 1、如果走函数用z(),出现输入s会向下走多步的情况,原因是什么? 向下走两层循环ij增加&a…...

需求堆积,如何排序产品优先极
面对堆积的产品需求,到底该如何排序优先极呢? 需求排期的目标 在谈具体的排期方法之前,有必要先探讨一下——合理的需求排期应该达到什么的目标呢?如果站在与项目相关的利益人员的角度来看,至少应该使以下四方面的收…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...

地震勘探——干扰波识别、井中地震时距曲线特点
目录 干扰波识别反射波地震勘探的干扰波 井中地震时距曲线特点 干扰波识别 有效波:可以用来解决所提出的地质任务的波;干扰波:所有妨碍辨认、追踪有效波的其他波。 地震勘探中,有效波和干扰波是相对的。例如,在反射波…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
第7篇:中间件全链路监控与 SQL 性能分析实践
7.1 章节导读 在构建数据库中间件的过程中,可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中,必须做到: 🔍 追踪每一条 SQL 的生命周期(从入口到数据库执行)&#…...

从“安全密码”到测试体系:Gitee Test 赋能关键领域软件质量保障
关键领域软件测试的"安全密码":Gitee Test如何破解行业痛点 在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的"神经中枢"。从国防军工到能源电力,从金融交易到交通管控,这些关乎国计民生的关键领域…...
【前端异常】JavaScript错误处理:分析 Uncaught (in promise) error
在前端开发中,JavaScript 异常是不可避免的。随着现代前端应用越来越多地使用异步操作(如 Promise、async/await 等),开发者常常会遇到 Uncaught (in promise) error 错误。这个错误是由于未正确处理 Promise 的拒绝(r…...