Gateway全局异常处理及请求响应监控

前言

我们在上一篇文章基于压测进行Feign调优完成的服务间调用的性能调优,此时我们也关注到一个问题,如果我们统一从网关调用服务,但是网关因为某些原因报错或者没有找到服务怎么办呢?

如下所示,笔者通过网关调用account服务,但是account服务还没起来。此时请求还没有到达account就报错了,这就意味着我们服务中编写的@RestControllerAdvice对网关没有任何作用。

curl 127.0.0.1:8090/account/getByCode/zsy

响应结果如下,可以看到响应结果如下所示,要知道现如今的开发模式为前后端分离模式,前后端交互完全是基于协商好的格式,如果网关响应格式与我们规定的格式完全不一致,前端就需要特殊处理,这使得代码不仅会变得丑陋,对于后续的功能扩展的交互复杂度也会增加,而gateway默认响应错误如下:

{"timestamp":"2023-02-09T15:22:20.278+0000","path":"/account/getByCode/zsy","status":500,"error":"Internal Server Error","message":"Connection refused: no further information: /192.168.43.73:9000"
}

在这里插入图片描述

网关异常默认处理

所以我们必须了解一下是什么原因导致网关报错会响应这个值。

我们在gateway源码中找到ErrorWebFluxAutoConfiguration这个自动装配类,可以看到下面这段代码,我们从中得知网关报错时默认使用DefaultErrorWebExceptionHandler 来返回结果,所以我们不妨看看这个类做了那些事情。

@Bean@ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT)@Order(-1)public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {//网关默认异常处理的handlerDefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes,this.resourceProperties, this.serverProperties.getError(), this.applicationContext);exceptionHandler.setViewResolvers(this.viewResolvers);exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());return exceptionHandler;}

我们不妨基于debug了解一下这个类,当我们服务没有注册到nacos,并通过网关调用报错时,代码就会走到下方,route 方法第一个参数是RequestPredicate谓词,而后者则是谓词的处理,进行renderErrorViewandRoute同理将报错的请求通过renderErrorResponse返回错误结果

@Override
//route 方法第一个参数是RequestPredicate谓词,而后者则是谓词的处理,进行renderErrorView,然后通过然后通过andRoute将报错的请求通过renderErrorResponse返回错误结果protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse);}

我们不妨看看renderErrorResponse,可以看到一行getErrorAttributes,一旦步入我们就可以看到上文请求错误的结果格式

protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);Map<String, Object> error = getErrorAttributes(request, includeStackTrace);return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON_UTF8).body(BodyInserters.fromObject(error));}

getErrorAttributes源码,可以看到组装的key值就是我们调试时响应的参数:

@Overridepublic Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {Map<String, Object> errorAttributes = new LinkedHashMap<>();errorAttributes.put("timestamp", new Date());errorAttributes.put("path", request.path());Throwable error = getError(request);HttpStatus errorStatus = determineHttpStatus(error);errorAttributes.put("status", errorStatus.value());errorAttributes.put("error", errorStatus.getReasonPhrase());errorAttributes.put("message", determineMessage(error));handleException(errorAttributes, determineException(error), includeStackTrace);return errorAttributes;}

自定义异常处理

了解的默认错误处理,我们就可以改造,返回一个和普通服务一样的格式给前端告知网关报错。从上文我们可知网关默认错误处理时DefaultErrorWebExceptionHandler,通过类图我们可以发现它继承了一个ErrorWebExceptionHandler,所以我们也可以继承这个类重写一个Handler

以笔者的代码如下,可以看到笔者使用Order注解强制获得最高异常处理优先级,然后使用bufferFactory.wrap方法传递自定义错误格式返回给前端。

@Slf4j
@Order(-1)
@Configuration
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {private final ObjectMapper objectMapper;@Overridepublic Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {ServerHttpResponse response = exchange.getResponse();if (response.isCommitted()) {return Mono.error(ex);}// 设置返回值类型为jsonresponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);//设置返回编码if (ex instanceof ResponseStatusException) {response.setStatusCode(((ResponseStatusException) ex).getStatus());}return response.writeWith(Mono.fromSupplier(() -> {DataBufferFactory bufferFactory = response.bufferFactory();try {//writeValueAsBytes 组装错误响应结果return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResultData.fail(500, "网关捕获到异常:" + ex.getMessage())));} catch (JsonProcessingException e) {log.error("Error writing response", ex);return bufferFactory.wrap(new byte[0]);}}));}
}

最终返回的结果如下所示,可以看到结果和一般的服务调用报错格式一模一样,这样一来前端就无需为了网关报错加一个特殊处理的逻辑了

curl 127.0.0.1:8090/account/getByCode/zsy

输出结果

{"status":500,"message":"网关捕获到异常:503 SERVICE_UNAVAILABLE \"Unable to find instance for account-service\"","data":null,"success":false,"timestamp":1675959617386
}

请求响应日志监控

对于微服务架构来说,监控是很重要的,在高并发场景情况下,很多问题我们都可以在网关请求响应中定位到,所以我们希望能有这么一种方式将用户日常请求响应的日志信息记录下来,便于日常运维和性能监控。

查阅了网上的资料发现,基于MongoDB进行网关请求响应数据采集是一种不错的方案,所以笔者本篇文章整理一下笔者如何基于网关过滤器结合MongoDB完成请求日志采集。

本篇文章可能会涉及MongoDB相关的知识,不了解的读者可以参考笔者的这篇文章:

MongoDB快速入门

gateway整合MongoDB采集日志步骤

  1. 添加MongoDB依赖并完成MongoDB配置:

首先在gateway中添加MongoDB依赖,需要注意的是,笔者后续的过滤器某些代码段会用到hutool的工具类,所以这里也添加了hutool的依赖。

 <!--mongodb依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb-reactive</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId></dependency>

然后我们在gateway的配置中添加MongoDB的连接参数配置:

# mongodb的ip地址
spring.data.mongodb.host=ip
# mongodb端口号
spring.data.mongodb.port=27017
# mongodb数据库名称
spring.data.mongodb.database=accesslog
# 用户名
spring.data.mongodb.username=xxxx
# 密码
spring.data.mongodb.password=xxx
  1. 编写MongoDB保存逻辑:

我们希望保存网关响应的内容到mongodb中,所以我们要把我们需要的内容封装成一个对象,如下GatewayLog

@Data
public class GatewayLog {/*** 请求相对路径*/private String requestPath;/***请求方法 :get post*/private String requestMethod;/***请求协议:http rpc*/private String schema;/***请求体内容*/private String requestBody;/***响应内容*/private String responseBody;/***ip地址*/private String ip;/*** 请求时间*/private String requestTime;/***响应时间*/private String responseTime;/***执行时间 单位:毫秒*/private Long executeTime;}

完成对象定义后,我们就可以编写service层接口和实现类的逻辑了:

public interface AccessLogService {/*** 保存AccessLog* @param gatewayLog 请求响应日志* @return 响应日志*/GatewayLog saveAccessLog(GatewayLog gatewayLog);}

实现类代码如下,可以看到笔者完全基于mongoTemplatesave方法将日志数据存到gatewayLog表中。

@Service
public class AccessLogServiceImpl implements AccessLogService {@Autowiredprivate MongoTemplate mongoTemplate;//collection名称private final String collectionName="gatewayLog" ;@Overridepublic GatewayLog saveAccessLog(GatewayLog gatewayLog) {GatewayLog result = mongoTemplate.save(gatewayLog, collectionName);return result;}
}
  1. 基于gateway过滤器完成请求相应日志采集,代码比较长,首先是CachedBodyOutputMessage,由于笔者用的是Spring boot 2.x版本,没有CachedBodyOutputMessage 这个类,所以笔者从网上找了一份。读者可以根据注释进行复制修改即可。
public class CachedBodyOutputMessage implements ReactiveHttpOutputMessage {private final DataBufferFactory bufferFactory;private final HttpHeaders httpHeaders;private Flux<DataBuffer> body = Flux.error(new IllegalStateException("The body is not set. Did handling complete with success? Is a custom \"writeHandler\" configured?"));private Function<Flux<DataBuffer>, Mono<Void>> writeHandler = this.initDefaultWriteHandler();public CachedBodyOutputMessage(ServerWebExchange exchange, HttpHeaders httpHeaders) {this.bufferFactory = exchange.getResponse().bufferFactory();this.httpHeaders = httpHeaders;}public void beforeCommit(Supplier<? extends Mono<Void>> action) {}public boolean isCommitted() {return false;}public HttpHeaders getHeaders() {return this.httpHeaders;}private Function<Flux<DataBuffer>, Mono<Void>> initDefaultWriteHandler() {return (body) -> {this.body = body.cache();return this.body.then();};}public DataBufferFactory bufferFactory() {return this.bufferFactory;}public Flux<DataBuffer> getBody() {return this.body;}public void setWriteHandler(Function<Flux<DataBuffer>, Mono<Void>> writeHandler) {Assert.notNull(writeHandler, "'writeHandler' is required");this.writeHandler = writeHandler;}public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {return Mono.defer(() -> {return (Mono)this.writeHandler.apply(Flux.from(body));});}public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {return this.writeWith(Flux.from(body).flatMap((p) -> {return p;}));}public Mono<Void> setComplete() {return this.writeWith(Flux.empty());}
}

过滤器代码如下,笔者将核心内容都已注释了,读者可以基于此代码进行修改


@Slf4j
@Component
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();//todo 存在线程安全问题,后续需要优化掉SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");@Autowiredprivate AccessLogService accessLogService;@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {GatewayLog gatewayLog = new GatewayLog();ServerHttpRequest request = exchange.getRequest();//获取请求的ip,url,method,bodyString requestPath = request.getPath().pathWithinApplication().value();String clientIp = request.getRemoteAddress().getHostString();String scheme = request.getURI().getScheme();String method = request.getMethodValue();//数据记录到gatwayLog中gatewayLog.setSchema(scheme);gatewayLog.setRequestMethod(method);gatewayLog.setRequestPath(requestPath);gatewayLog.setRequestTime(simpleDateFormat.format(new Date().getTime()));gatewayLog.setIp(clientIp);MediaType contentType = request.getHeaders().getContentType();if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType) || MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {return writeBodyLog(exchange, chain, gatewayLog);} else {//写入日志信息到mongoDbreturn writeBasicLog(exchange, chain, gatewayLog);}}private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {StringBuilder builder = new StringBuilder();MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ","));}//记录响应内容accessLog.setRequestBody(builder.toString());//   获取响应体ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);return chain.filter(exchange.mutate().response(decoratedResponse).build()).then(Mono.fromRunnable(() -> {//打印日志writeAccessLog(accessLog);}));}/*** 解决request body 只能读取一次问题** @param exchange* @param chain* @param gatewayLog* @return*/private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {gatewayLog.setRequestBody(body);return Mono.just(body);});// 通过 BodyInsert 插入 body(支持修改body), 避免 request body 只能获取一次BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);HttpHeaders headers = new HttpHeaders();headers.putAll(exchange.getRequest().getHeaders());headers.remove(HttpHeaders.CONTENT_LENGTH);CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {// 重新封装请求ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);// 记录响应日志ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);// 记录普通的return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> {// 打印日志writeAccessLog(gatewayLog);}));}));}/*** 打印日志并将日志内容写入mongodb** @param gatewayLog*/private void writeAccessLog(GatewayLog gatewayLog) {log.info("写入网关日志,日志内容:" + JSON.toJSONString(gatewayLog));accessLogService.saveAccessLog(gatewayLog);}/*** 请求装饰器,重新计算 headers** @param exchange* @param headers* @param outputMessage* @return*/private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers,CachedBodyOutputMessage outputMessage) {return new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic HttpHeaders getHeaders() {long contentLength = headers.getContentLength();HttpHeaders httpHeaders = new HttpHeaders();httpHeaders.putAll(super.getHeaders());if (contentLength > 0) {httpHeaders.setContentLength(contentLength);} else {httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");}return httpHeaders;}@Overridepublic Flux<DataBuffer> getBody() {return outputMessage.getBody();}};}/*** 记录响应日志** @param exchange* @param gatewayLog* @return*/private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {ServerHttpResponse response = exchange.getResponse();DataBufferFactory bufferFactory = response.bufferFactory();return new ServerHttpResponseDecorator(response) {@SneakyThrows@Overridepublic Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {if (body instanceof Flux) {String responseTime = simpleDateFormat.format(new Date().getTime());gatewayLog.setResponseTime(responseTime);// 计算执行时间long executeTime = (simpleDateFormat.parse(responseTime).getTime() - simpleDateFormat.parse(gatewayLog.getRequestTime()).getTime());gatewayLog.setExecuteTime(executeTime);// 获取响应类型,如果是 json 就打印String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);if (ObjectUtils.equals(this.getStatusCode(), HttpStatus.OK)&& StringUtils.isNotBlank(originalResponseContentType)&& originalResponseContentType.contains("application/json")) {Flux<? extends DataBuffer> fluxBody = Flux.from(body);return super.writeWith(fluxBody.buffer().map(dataBuffers -> {// 合并多个流集合,解决返回体分段传输DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();DataBuffer join = dataBufferFactory.join(dataBuffers);byte[] content = new byte[join.readableByteCount()];join.read(content);// 释放掉内存DataBufferUtils.release(join);String responseResult = new String(content, StandardCharsets.UTF_8);gatewayLog.setResponseBody(responseResult);return bufferFactory.wrap(content);}));}}return super.writeWith(body);}};}/*** 调小优先级使得该过滤器最先执行* @return*/@Overridepublic int getOrder() {return -100;}
}
  1. 测试

以笔者项目为例,通过网关调用order服务:

curl 127.0.0.1:8090/order/getByCode/zsy

可以看到响应成功了,接下来我们就确认一下mongoDb中是否有存储网关请求响应信息


{"status":100,"message":"操作成功","data":{"id":1,"accountCode":"zsy","accountName":"zsy","amount":10000.00},"success":true,"timestamp":1676439102837}

通过数据库连接工具查询,可以看到网关请求响应日志也成功存储到MongoDB中。

在这里插入图片描述

参考文献

SpringCloud Alibaba微服务实战二十四 - SpringCloud Gateway的全局异常处理

软件开发设计中的上游与下游

SpringCloud Alibaba实战二十九 | SpringCloud Gateway 请求响应日志

MongoDB 数据查询操作

实战 | MongoDB的安装配置

spring cloud gateway中实现请求、响应参数日志打印

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/213433.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

超声波清洗机会损伤物品吗?一文明白超声波清洗机有哪些优点

正确使用超声波清洗机且买对超声波清洗机是不会对清洗物品造成伤害的&#xff01; 一、超声波清洗机工作原理是如何的&#xff1f; 超声波清洗机的工作原理是利用超声波产生的空化振动来清洁物体。当超声波在清洗液中传播时&#xff0c;它会产生微小的气泡和振动&#xff0c;这…

论jenkins的使用方法(初步)

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 目录 &#x1f4d1;什么是持续集成&…

1-1、Java概述

语雀原文链接 文章目录 1、Java发展2、Java体系结构3、Java特点 1、Java发展 1990年&#xff0c;Sun公司(Stanford University Network,斯坦福大学网络公司)詹姆斯高斯林推出的一门语言最开始注册的名字oak语言(橡树)&#xff0c;重名了被迫改成Java2009年Sun公司被甲骨文Ora…

logstash插件简单介绍

logstash插件 输入插件(input) Input&#xff1a;输入插件。 Input plugins | Logstash Reference [8.11] | Elastic 所有输入插件都支持的配置选项 SettingInput typeRequiredDefaultDescriptionadd_fieldhashNo{}添加一个字段到一个事件codeccodecNoplain用于输入数据的…

鼠标光标不见了怎么办?速速get这4个方法!

“非常奇怪&#xff0c;我的鼠标光标用着用着就不见了&#xff0c;这是为什么呢&#xff1f;有什么方法可以解决这个问题吗&#xff1f;” 在电脑使用过程中&#xff0c;有时候会遇到鼠标光标突然消失的情况&#xff0c;这无疑会给我们日常操作带来很大的不便。那么&#xff0c…

使用Python提取PDF文件中指定页面的内容

在日常工作和学习中&#xff0c;我们经常需要从PDF文件中提取特定页面的内容。在本篇文章中&#xff0c;我们将介绍如何使用Python编程语言和两个强大的库——pymupdf和wxPython&#xff0c;来实现这个任务。 1. 准备工作 首先&#xff0c;确保你已经安装了以下两个Python库&…

【办公软件】Outlook启动一直显示“正在启动”的解决方法

早上打开电脑Outlook2016以后&#xff0c;半个多小时了&#xff0c;一直显示这个界面&#xff1a; 解决办法 按WIN R键打开“运行”&#xff0c;输入如下命令&#xff1a; outlook.exe /safe 然后点击“确定” 这样就进入了Outlook的安全模式。 点击“文件”->“选项”-…

nbcio-vue下载安装后运行报错,diagram-js没有安装

更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/nbcio-boot 前端代码&#xff1a;https://gitee.com/nbacheng/nbcio-vue.git 在线演示&#xff08;包括H5&#xff09; &#xff1a; http://122.227.135.243:9888 根据…

047:vue加载循环倒计时 示例

第047个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

基于java web的网上书城系统的设计与实现论文

摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff0c;商品交易当然也不能排除在外&#xff0c;随着商品交易管理的不断成熟&#xff0c;它彻底改变了过去传统的经营管理方式&#xff0c;不仅使商品…

统信UOS_麒麟KYLINOS上跨架构下载离线软件包

原文链接&#xff1a;统信UOS/麒麟KYLINOS上跨架构下载离线软件包 hello&#xff0c;大家好啊&#xff0c;今天给大家带来一篇在统信UOS/麒麟KYLINOS上跨架构下载离线软件包的实用教程。在我们的日常工作中&#xff0c;可能会遇到这样的情况&#xff1a;需要为不同架构的设备下…

【总结】机器学习中的15种分类算法

目录 一、机器学习中的分类算法 1.1 基础分类算法 1.2 集成分类算法 1.3 其它分类算法&#xff1a; 二、各种机器学习分类算法的优缺点 分类算法也称为模式识别&#xff0c;是一种机器学习算法&#xff0c;其主要目的是从数据中发现规律并将数据分成不同的类别。分类算法通…

排序算法——桶排序/基数排序/计数排序

桶排序 是计数排序的升级版。它利用了函数的映射关系&#xff0c;高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理&#xff1a; 假设输入数据服从均匀分布&#xff0c;将数据分到有限数量的桶里&#xff0c;每个桶再分别排序&#xff08;有可能再使…

想进阶JAVA高级程序员吗?多线程必学

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星评选TOP 10&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作…

【SpringSecurity】-- 认证、授权

文章目录 SpringSecurity简介快速入门1.准备工作1.2引入SpringSecurity 认证1.登录校验流程2.原理2.1SpringSecurity完整流程2.2认证流程详解 3.解决问题3.1思路分析3.2准备工作3.3.实现3.3.1数据库校验用户3.3.2密码加密存储3.3.3登录接口3.3.4认证过滤器3.3.5退出登录 授权1.…

输入一组数据,以-1结束输入[c]

我们新手写题时总能看到题目中类似这样的输入 没有给固定多少个数据&#xff0c;我们没有办法直接设置数组的元素个数&#xff0c;很纠结&#xff0c;下面我来提供一下本人的方法&#xff08;新手&#xff0c;看到有错误或者不好的地方欢迎大佬指出&#xff0c;纠正&#xff0…

【开源】基于Vue和SpringBoot的衣物搭配系统

项目编号&#xff1a; S 016 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S016&#xff0c;文末获取源码。} 项目编号&#xff1a;S016&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、研究内容2.1 衣物档案模块2.2 衣物搭配模块2.3 衣…

HarmonyOS鸿蒙操作系统架构开发

什么是HarmonyOS鸿蒙操作系统&#xff1f; HarmonyOS是华为公司开发的一种全场景分布式操作系统。它可以在各种智能设备&#xff08;如手机、电视、汽车、智能穿戴设备等&#xff09;上运行&#xff0c;具有高效、安全、低延迟等优势。 目录 HarmonyOS 一、HarmonyOS 与其他操…

CRM系统的这些功能助您高效管理客户

客户管理可以理解为企业收集并利用客户信息&#xff0c;满足客户的需求&#xff0c;从而提升客户价值的过程。CRM系统一直被誉为客户管理的“神器”&#xff0c;下面我们就来说说CRM系统有哪些功能可以管理客户&#xff1f; 1、客户信息管理 CRM可以帮助企业收集客户的基本信…

C++ Qt开发:如何使用信号与槽

在Qt中&#xff0c;信号与槽&#xff08;Signal and Slot&#xff09;是一种用于对象之间通信的机制。是Qt框架引以为傲的一项机制&#xff0c;它带来了许多优势&#xff0c;使得Qt成为一个强大且灵活的开发框架之一。信号与槽的关联通过QObject::connect函数完成。这样的机制使…