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,一经查实,立即删除!

相关文章

中小企业管理者如何培育团队精神?

某石油工程有限公司总经理曾提问&#xff1a;“作为中小企业的管理者如何才能更好的激发团队精神呢&#xff1f;” 每个企业都向往和号召团队精神&#xff0c;但是往往事与愿违。在各种羡慕嫉妒恨的情绪影响下&#xff0c;难免会产生一些落差&#xff0c;影响到团队精神。 所…

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

正确使用超声波清洗机且买对超声波清洗机是不会对清洗物品造成伤害的&#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…

Docker 容器中使用 Docker - DinD 和 DooD

突然间研究这个来的缘由是正在从 Jenkins 往 Harness 的过度, 而完全用命令来构建 Docker 镜像变得不一样了。在 Jenkins 中 Agent 本身也是一个 Docker Daemon, 所以 Docker 命令执行无障碍&#xff0c;而 Harness 的所谓的 Agent 就是一个个的运行在 Kubernetes 中的 Docker …

error:gmapping

– Could not find the required component ‘gmapping’. The following CMake error indicates that you either need to install the package with the same name or change your environment so that it can be found. CMake Error at /opt/ros/kinetic/share/catkin/cmake…

logstash插件简单介绍

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

【SpringBoot教程】SpringBoot Thymeleaf 基于HTML5的现代模板引擎

作者简介&#xff1a;大家好&#xff0c;我是撸代码的羊驼&#xff0c;前阿里巴巴架构师&#xff0c;现某互联网公司CTO 联系v&#xff1a;sulny_ann&#xff08;17362204968&#xff09;&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗…

error:move_base_msgs

CMake Warning at /opt/ros/kinetic/share/catkin/cmake/catkinConfig.cmake:76 (find_package): Could not find a package configuration file provided by “move_base_msgs” with any of the following names: move_base_msgsConfig.cmake move_base_msgs-config.cmake …

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

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

Linux bin包生成

需求背景&#xff1a; 在实际项目时我们很少把源码用个tar给到客户&#xff0c;这样显得很不专业&#xff0c;且有的时候我们提供补丁&#xff0c;那么这个时候我们提供一个补丁的bin包可以直接安装运行就显得很高大上了。 物料准备 准备一台liunx&#xff0c;虚拟机亦可&am…

自定义插件vue-router简单实现hashRouter设计思路

步骤 1.挂载 vue.prototype.$router 2.声明两个组件 router-view this.$router.current>component > h(component) router-link h(a,{attrs:{href:#this.to}},this.$slots.default) 3.url的监听&#xff1a;window hashchange的改变 4.定义响应式current&#xff0…

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

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

JavaScript深拷贝和浅拷贝

对于原始数据类型&#xff0c;并没有深浅拷贝的区别&#xff0c;深浅拷贝都是对于引用数据类型而言&#xff0c;如果我们要赋值对象的所有属性都是引用类型可以用浅拷贝 浅拷贝&#xff1a;只复制一层对象&#xff0c;当对象的属性是引用类型时&#xff0c;实质复制的是其引用&…

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

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

第6节:Vue3 调用函数

在Vue3中&#xff0c;你可以使用setup函数来调用函数。 <template><button click"handleClick">点击我</button> </template><script> import { ref } from vue;export default {setup() {// 创建一个响应式的引用const count ref(0…

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;不仅使商品…

32、Bean的生产顺序是由什么决定的?

Bean的生产顺序是由什么决定的? BeanDefinition的注册顺序是有什么决定的? Bean在生产之前有个临时状态:BeanDefinition;存储着bean的描述信息。由BeanDefinition决定着Bean的生产顺序。会按照BeanDefinition的注册顺序来决定Bean的生产顺序。因为所有的BeanDefinition存…