springboot系列(二十一):基于AOP实现自定义注解且记录接口日志|超级超级详细,建议收藏

news/2024/5/19 23:33:08/文章来源:https://blog.csdn.net/weixin_43970743/article/details/125896253

👨‍🎓作者:bug菌

🚫特别声明:原创不易,转载请附上原文出处链接和本文声明,谢谢配合。

🙏版权声明:文章里可能部分文字或者图片来源于互联网或者百度百科,如有侵权请联系bug菌处理。

【开发云】年年都是折扣价,不用四处薅羊毛

         嗨,家人们,我是bug菌呀,我又来啦。今天我们来聊点什么咧,OK,接着为大家更《springboot零基础入门教学》系列文章吧。希望能帮助更多的初学者们快速入门!

小伙伴们在批阅文章的过程中如果觉得文章对您有一丝丝帮助,还请别吝啬您手里的赞呀,大胆的把文章点亮👍吧,您的点赞三连(收藏⭐️+关注👨‍🎓+留言📃)就是对bug菌我创作道路上最好的鼓励与支持😘。时光不弃🏃🏻‍♀️,创作不停💕,加油☘️

一、前言🔥

在Spring体系中,实现自定义注解的方式有很多,比如呢可以写一个公共方法,然后在每个Controller中手动传参调用。

       而我今天则要在这里隆重介绍一下,如何通过AOP切面思想方式来实现用户自定义的注解?大家想不想听听看,感不感兴趣呢?

       其实啊,感不感兴趣都没关系啦,我写了,自然会有愿意看的人看,毕竟我对自己写的东西,还是比较有自信滴。我会做到句句是重点,关键我是要教那些没用过spring aop实现自定义注解的人群,帮助他们,就等于在帮助我自己,我为人人,人人为我!至于大佬级别的,也随时欢迎你们对文章进行批评指正啦。

       好啦,咱们就开始今天的内容吧。

二、正文🔥

1️⃣概念

       这个大家肯定都知道。AOP(Aspect Oriented Programming)是面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。可以说是OOPObject-Oriented Programing,面向对象编程)的补充和完善。

2️⃣应用场景

  • 日志打印及记录
  • 权限认证
  • 全局异常处理拦截
  • 返回值统一处理
  • 多数据源切换
  • 事务处理
  • ... ...

       总之,就是要明白Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。只要符合,那么aop就适用。

3️⃣如何使用AOP?

       原则上来说,我们只要引入 Spring 框架中 AOP 的相应依赖就可以直接使用 Spring 的 AOP 支持了,不过,为了进一步为大家使用 SpringAOP 提供便利,SpringBoot 还是“不厌其烦”地为我们提供了一个 spring-boot-starter-aop 自动配置模块。 

 spring-boot-starter-aop 自动配置行为由两部分内容组成:

  • spring-boot-autoconfigure下的org.springframework.boot.autoconfigure.aop.AopAutoConfiguration 提供 @Configuration 配置类和相应的配置项。 
  • spring-boot-starter-aop 模块自身提供了针对 spring-aopaspectjrt 和 aspectjweaver 的依赖。 

       一般情况下,只要项目依赖中加入了 spring-boot-starter-aop,其实就会自动触发 AOP 的关联行为,包括构建相应的 AutoProxyCreator,将横切关注点织入(Weave)相应的目标对象等。

       不过 AopAutoConfiguration 依然为我们提供了可怜的两个配置项,用来有限地干预 AOP 相关配置:  spring.aop.auto=true ;spring.aop.proxy-target-class=false ,对我们来说,这两个配置项的最大意义在于:允许我们投反对票,比如可以选择关闭自动的 aop 配置spring.aop.auto=false,或者启用针对 class 而不是 interface 级别的 aop 代理(aop proxy)。

4️⃣如何实现AOP?

       实现AOP的技术,主要分为两大类:

  • 一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
  • 二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

然而殊途同归,实现AOP的技术特性却是相同的,分别为:

  • 1、Joinpoint(连接点):是程序执行中的一个精确执行点,例如类中的一个方法。它是一个抽象的概念,在实现AOP时,并不需要去定义一个join point。
  • 2、Pointcut(切入点):本质上是一个捕获连接点的结构。在AOP中,可以定义一个point cut,来捕获相关方法的调用。
  • 3、Advice(通知):是point cut的执行代码,是执行“方面”的具体逻辑。

拓展一下:advice(通知)可分为以下5种通知类型:

  1. 前置通知(Before):在目标方法被调用之前调用通知功能。
  2. 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。
  3. 返回通知(AfterReturning):在目标方法成功执行之后调用通知。
  4. 异常通知(AfterThrowing):在目标方法抛出异常后调用通知。
  5. 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
  • 4、Aspect(方面):point cut和advice结合起来就是aspect,它类似于OOP中定义的一个类,但它代表的更多是对象间横向的关系。
  • 5、Introduce(引入):为对象引入附加的方法或属性,从而达到修改对象结构的目的。有的AOP工具又将其称为mixin。

       上述的技术特性组成了基本的AOP技术,大多数AOP工具均实现了这些技术。它们也可以是研究AOP技术的基本术语。

... ...

       那么,我么接下来就来演示一下,如何具体在项目中进行配置。

三、项目配置🔥

        我们只需要在项目依赖中加入 spring-boot-starter-aop 就可以使用AOP了。

1️⃣pom依赖

        添加aop相关依赖。

<!--AOP-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>

       如上在pom中加入aop starter、等依赖从maven源仓库下载好后,我们就可以正常使用AOP了。

2️⃣application-dev.yaml

       除了引用aop依赖,我们可能还会涉及到一个配置。就像我上述所介绍的那样,允许我们手动关闭自动的aop配置,就可以直接在我们的项目配置文件中加上如下auto 与 proxy-target-class 配置项就行,一般我们是不需要配,但是我们得知道有这么个东西就行。

spring:aop:auto: true #自动代理开启;false:关闭proxy-target-class: false #属性值决定是基于接口的还是基于类的代理被创建.true表示基于类的代理将使用,false表示默认使用Jdk基于接口的代理

四、实战教学🔥

        接下来,我就带着大家代码实现自定义注解记录接口调用日志,并且记录调用接口时的接口返回码、接口耗时、请求时间等参数,具有很好的参考价值。

1️⃣数据库新增一张log_info表

        如下是我从本地数据库导出来的,给大家作为参考哈。

CREATE TABLE `log_info` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',`type` varchar(255) DEFAULT NULL COMMENT '日志类型',`response_code` varchar(255) DEFAULT NULL COMMENT '接口返回状态码',`operate_time` datetime DEFAULT NULL COMMENT '请求时间',`spend_time` bigint(255) DEFAULT NULL COMMENT '消耗时间',`url` varchar(255) DEFAULT NULL COMMENT 'url',`body` varchar(255) DEFAULT NULL COMMENT '请求体',`ip` varchar(255) DEFAULT NULL COMMENT 'ip',`query` varchar(255) DEFAULT NULL COMMENT '查询参数',`exception` varchar(255) DEFAULT NULL COMMENT '异常信息',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2️⃣定义LogInfo实体

/*** 系统日志实体** @Author luoYong* @Date 2021-08-05 15:38*/
@TableName("log_info")
@Data
@ApiModel(value = "系统日志实体", description = "系统日志实体")
public class LogInfo extends BaseEntity {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "主键id 自增列")@TableId(value = "id", type = IdType.AUTO)private Integer id;@ApiModelProperty(value = "操作类型")@TableId(value = "log_type")private String logType;@ApiModelProperty(value = "日志内容")@TableId(value = "content")private String content;@ApiModelProperty(value = "日志类型")@TableId(value = "log_type")private LogTypeEnum logType;@ApiModelProperty(value = "操作")@TableId(value = "operation")private String operation;@ApiModelProperty(value = "ip地址")@TableId(value = "ip")private String ip;
}

3️⃣定义logInfo持久层

/*** 系统日志持久层** @Author luoYong* @Date 2021-08-05 15:27*/
@Component
public interface LogInfoMapper extends BaseMapper<LogInfo> {/*** 清除指定日期之前的日志** @param date 时间*/void clear(Date date);
}

4️⃣定义logInfo业务层接口

public interface ILogInfoService extends IService<LogInfo> {}

5️⃣定义logInfo接口实现类

@Slf4j
@Service
public class LogInfoServiceImpl extends ServiceImpl<LogInfoMapper, LogInfo> implements ILogInfoService {}

6️⃣定义Controller分发器

/*** 用户管理分发器*/
@RestController
@RequestMapping("/user")
@Api(tags = "用户管理模块", description = "用户管理模块")
public class UserController {}

7️⃣实现自定义注解类

package com.example.demo.annotation;import com.example.demo.enums.LogTypeEnum;
import java.lang.annotation.*;/*** 自定义注解类  @SysLog** @Author luoYong* @version 1.0* @Date 2022-01-20 17:29*/
@Target(ElementType.METHOD) //注解放置的目标位置,METHOD是可注解在方法级别上
@Retention(RetentionPolicy.RUNTIME) //注解在哪个阶段执行
@Documented //生成文档
public @interface SysLog {// 声明注解成员String operation() default "";LogTypeEnum logType() default LogTypeEnum.LOG_TYPE_QUERY;   // 日志类型默认是查询类型日志
}

8️⃣实现切面处理类(核心)

       整文的核心就在这儿了,大家在看的过程中,如果有遇到不清楚或者不会的,还请及时提问,下方评论区留言,bug菌会第一时间给予你最有效的解答。

       先定义号一个切面类。

/*** 系统日志:切面处理类** @author luoYong* @version 1.0* @date 2022/1/24 12:48*/
@Slf4j
@Aspect
@Component
public class SysLogAspect {@Autowiredprivate ILogInfoService iLogInfoService;@Around("execution(public * com.example.demo.controller.*.*(..))")public Object postLogAspect(ProceedingJoinPoint pjp) throws Throwable {// 核心逻辑,咱们一步一步拆解}
}

首先咱们先来看下,我们需要获取那些数据。

  1. 查询参数、ip、url、请求体
  2. 返回值(返回code、是否异常)
  3. 请求时间、目标接口消耗时间
  4. 操作类型

一步一步来,我们先来获取把需要处理的数据封装出来。

  • 获取接口消耗时间
private Object proceedController(ProceedingJoinPoint pjp, LogInfo log) throws Throwable {//记录开始接口时间long spendTime = System.currentTimeMillis();//调用目标接口及获取返回结果Object result = pjp.proceed(pjp.getArgs());//调用目标接口结束//计算接口耗时spendTime = (System.currentTimeMillis() - spendTime) / 1000;log.setSpendTime(spendTime);return result;
}
  • 获取接口返回值
private BaseResponse setResponseCode(LogInfo log, Object result) {//判断返回体类型是否为BaseResponseif (result != null && result instanceof BaseResponse) {BaseResponse restResult = (BaseResponse) result;return restResult;}return new BaseResponse();
}
  • 获取请求体
private void setBody(HttpServletRequest request, LogInfo log) {if (request instanceof ContentCachingRequestWrapper) {ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;String body = StringUtils.toEncodedString(wrapper.getContentAsByteArray(),Charset.forName(wrapper.getCharacterEncoding()));log.setBody(body);}
}
  • 获取ip地址
private String getIpAddress() {// 通过RequestContextHolder获取request对象HttpServletRequest request = SpringServletContextUtils.getRequest();if (request != null) {try {return IpUtils.getIpAddr(request);} catch (Exception e) {log.error("unable to get ip address");}}return null;
}
  • 构建日志对象
private LogInfo createOpLog() {HttpServletRequest request = this.getRequest();LogInfo log = new LogInfo();log.setQuery(request.getQueryString());this.setBody(request, log);log.setOperateTime(new Date());log.setUrl(request.getServletPath());log.setIp(getIpAddress());return log;
}
  • 获取request对象
private HttpServletRequest getRequest() {ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = servletRequestAttributes.getRequest();return request;
}
  • 获取操作类型
private void getLogType(ProceedingJoinPoint pjp, LogInfo log) {//从切面织入点处通过反射机制获取织入点处的方法MethodSignature signature = (MethodSignature) pjp.getSignature();//获取切入点所在的方法Method method = signature.getMethod();//获取操作SysLog sysLog = method.getAnnotation(SysLog.class);//获取log.setLogType(sysLog.logType().getValue());
}
  • 核心一步,编写环绕类型通知
/*** 环绕通知 用于拦截指定内容,记录用户的操作* pjp:ProceedingJoinPoint 是切入点对象* com.example.demo.controller.*.*(..)) 解析* 1、第一个*表示是返回任意类型* 2、com.nl.demo.controllers是包路径,针对所有的控制器* 3、第二个*是任意类* 4、第三个*是任意方法* 5、(..)的任意参数** @param pjp 切入点*/
@Around("execution(public * com.example.demo.controller.*.*(..))")
public Object postLogAspect(ProceedingJoinPoint pjp) throws Throwable {//初始化logLogInfo log = this.createOpLog();Object result = this.proceedController(pjp, log);//获取操作类型this.getLogType(pjp, log);//获取返回值编码codeBaseResponse resData = this.setResponseCode(log, result);//赋值返回编码log.setResponseCode(resData.getCode());//记录非成功异常if (log.getResponseCode() != ResultEnum.SUCCESS.getKey()) {//记录异常log.setException(resData.getMsg());} //调用service保存SysLog实体类到数据库iLogInfoService.save(log);return result;
}

        针对以上有任何不清楚的地方,欢迎评论区留言,不懂就问,我也如此。

9️⃣进行swagger测试

/*** 用户管理分发器*/
@RestController
@RequestMapping("/user")
@Api(tags = "用户管理模块", description = "用户管理模块")
public class UserController {@Autowiredprivate UserService userService;/*** 根据用户id查询用户信息*/@SysLog(logType = LogTypeEnum.LOG_TYPE_QUERY)@GetMapping("/getUser-by-id")@ApiOperation(value = "根据用户id查询用户信息", notes = "根据用户id查询用户信息")public ResultResponse<UserEntity> getUserById(@RequestParam(name = "userId") @ApiParam("请输入用户id") String userId) {return new ResultResponse<>(userService.getById(userId));}
}

       写好接口测试,咱们重启下项目,进行swagger调用。

       接口是调用成功了也无报错,可aop切入日志是否插入成功呢?咱们可以打开数据库查看log_info表,日志业务接口是否成功被记录?若成功,肯定会新生成一条记录的。

       如上图,由于我获取ip是获取到的localhost地址,所以这串地址也是127.0.0.1的ipv6地址,需要对获取客户端ip那个方法最后进行一下判断即可。重点是咱们添加自定义注解的业务接口能成功被记录日志并且保存入库,这就很棒哦🤓

五、往期推荐🔥

六、文末🔥

        如果还想要学习更多,小伙伴们可关注bug菌专门为大家创建的专栏《springboot零基础入门教学》,从无到有,从零到一!希望能帮助到更多小伙伴们。

【开发云】年年都是折扣价,不用四处薅羊毛

       我是bug菌,一名想走👣出大山改变命运的程序猿。接下来的路还很长,都等待着我们去突破、去挑战。来吧,小伙伴们,我们一起加油!未来皆可期,fighting!

        最后送大家两句我很喜欢的话,与诸君共勉!


☘️做你想做的人,没有时间限制,只要愿意,什么时候都可以start。

🍀你能从现在开始改变,也可以一成不变,这件事,没有规矩可言,你可以活出最精彩的自己。


​​​

💌如果文章对您有所帮助,就请留下您的吧!(#^.^#);

💝如果喜欢bug菌分享的文章,就请给bug菌点个关注吧!(๑′ᴗ‵๑)づ╭❤~;

💗如果对文章有任何疑问,还请文末留言或者加群吧;

💞鉴于个人经验有限,所有观点及技术研点,如有异议,请直接回复参与讨论(请勿发表攻击言论,谢谢);

💕版权声明:原创不易,转载请附上原文出处链接和本文声明,版权所有,盗版必究!!!谢谢。

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

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

相关文章

Scss--将node-sass切换为sass(原dart-sass)

原文网址&#xff1a;Scss--将node-sass切换为sass(原dart-sass)_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍node-sass与sass&#xff08;原dart-sass&#xff09;&#xff0c;以及如何将node-sass切换为sass&#xff08;原dart-sass&#xff09;。 sass依赖原来是dart-s…

Alibaba之jvm-sandbox初体验

前言 在开始之前&#xff0c;我们先来模拟一下以下的场景&#xff1a; 小李&#xff1a;“小明&#xff0c;你的接口没有返回数据&#xff0c;麻烦帮忙看一下&#xff1f;” 小明&#xff1a;“我这边的数据也是从别人的服务器中拿到的&#xff0c;但是我不确定是因为逻辑处理…

alluxio简单使用

alluxio简单使用 本文是基于alluxio官网和自己实践整理。 Alluxio版本&#xff1a;1.8.1CDH 1.15.2 1、介绍 以内存为中心的分布式虚拟存储系统。Alluxio在上层计算框架和底层存储系统之间架起了桥梁&#xff0c;应用层只需要访问Alluxio即可以访问底层对接了的任意存储系统的…

Oracle索引详解

索引 类似于书的目录&#xff0c;提高查询效率。 创建索引语法&#xff1a; CREATE [UNIQUE] [BITMAP] INDEX 索引名称 ON 表名(字段,[字段,..,..]);名词解释&#xff1a;UNIQUE 唯一索引BITMAP 位图索引默认不写 UNIQUE 和 BITMAP 为普通索引表名后面写多个字段为复合索引在字…

activeMQ、rabbitMQ学习对比心得

一、activemq activemq工作模型比较简单。只有两种模式 queue、topics 。 queue就多对一&#xff0c;producer往queue里发送消息&#xff0c;消费者从queue里取&#xff0c;消费一条&#xff0c;就从queue里移除一条。如果一个消费者消费速度不够快怎么办呢&#xff1f;在act…

About-Flink

About-Flink 一、Flink简介 1.1、flink特点1.2、分层Api1.3、Flink vs Spark Streaming 二、Flink批处理应用 2.1、依赖的引入2.2、准备批处理文件2.3、wordCount编码2.4、自定义类 三、Flink流处理应用 3.1、wordCount编码3.2、设置并行度-默认为43.2、数据来源socket3.3、配…

通过 replace() 和正则实现 将文本中的所有数字颜色高亮

实现的效果&#xff1a; 用到的知识点&#xff1a; replace() 方法用于在字符串中用一些字符替换另一些字符&#xff0c;或替换一个与正则表达式匹配的子串。 repalce&#xff08; a, b &#xff09; 必须传两个值&#xff0c;其中a 是要替换的文本&#xff0c;或者满足条件…

javaweb JAVA JSP球鞋销售系统购物系统ssm购物系统购物商城系统源码(ssm电子商务系统)

JSP球鞋销售系统购物系统ssm购物系统购物商城系统源码&#xff08;ssm电子商务系统&#xff09;

生产和同城存储双活架构下,发生脑裂问题影响数据库读写,如何快速分析问题和解决问题?

数据中心脑裂问题,简单说就是两个数据中心间的网络和存储链路同时发生中断,导致两个数据中心内的应用、数据库或者操作系统同时抢占和利用共享的资源,造成资源的数据不一致,产生重大影响。如何避免脑裂是每个存储双活方案都需要尤为重视的问题,脑裂会带来长时间的存储读写…

linux上redis单机的安装

1. 官网下载 https://github.com/redis/redis/archive/7.0.4.tar.gz 2. 上传到虚拟机/data/目录下、解压 tar -xzvf redis-7.0.4.tar.gz 3. 进入redis-7.0.4此目录 cd redis-7.0.4;ll 4. 安装到指定目录中 a. mkdir /usr/local/redis b. make PREFIX/usr/local/redis inst…

沃尔玛、eBay、wish、新蛋等美系平台对于测评风控点有哪些?怎么解决

很多人把各大平台风控想得过于简单&#xff0c;以为注册一批买家账号配一个IP就能进行下单上评&#xff0c;这也是导致市面上的测评现象杂乱无章。但是一定要明白一点各大电商平台都是一家数据公司他的算法一定是根据市场的变化而不断调整的。 平台检测的方式有很多种 1、平台…

RabbitMQ入门(二)

1.概述 RabbityMQ整体上是一个生产者和消费者模式。生产者生产消息到消息中间件的服务节点&#xff08;Broker&#xff09;,服务节点中包含交换器&#xff08;Exchange&#xff09;和队列&#xff08;Queue&#xff09;&#xff0c;生产的消息首先经过交换器&#xff0c;再由交…

搭建vue3项目

搭建vue3项目搭建准备创建项目选择所需配置运行项目vue3已经被大众所熟悉&#xff0c;很多公司都在做vue2到vue3的升级。 介绍vue3项目的搭建过程 搭建准备 前端开发环境需要node.js&npm node下载地址:http://nodejs.cn/download/ 根据自己电脑环境下载就行 安装vue-cli3…

2022/08/31 day14:企业级解决方案

文章目录目录缓存预热缓存雪崩缓存击穿缓存穿透性能指标监控总结目录 面试问题 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EtBtkGNE-1661933471760)(en-resource://database/5507:1)] [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下…

抖音小程序模板全行业整理合集,抖音小程序制作平台分享

小弟我是来自第三方抖音小程序制作平台的打工人&#xff0c;给大家整合了一些我们平台的抖音小程序模板&#xff0c;大家可以根据需要来获取。 步骤就是点击下方的链接&#xff0c;选好自己的抖音小程序模板&#xff0c;在平台注册账号直接套用到自己的抖音小程序上&#xff0…

深入理解蓝牙BLE之“信道管理”

目录 一.BLE的调制解调&#xff1a; 二.BLE的信道&#xff1a; 三.BLE的广播信道&#xff1a; 四.BLE的数据信道&#xff1a; 五.BLE信道管理&#xff1a; 5.1广播信道的随机延时&#xff1a; 5.2数据信道的调频算法&#xff1a; 跳频算法1&#xff1a; 跳频算法2&…

02.Haoop 虚拟机 桥接与NAT之间区别 及桥接设置

首先说 我的硬件准备&#xff0c;1台windows系统&#xff0c;1台mac pro 。 在 物理机上使用了 VMWARE CENTOS 7 的 方式进行配置。 那么我希望能实现把 这2台机器连在一起&#xff0c;做Hadoop 的集群。 网络问题是首先需要解决的事情&#xff0c;主要不通物理主机之间一直…

02:入门及安装(狂神说RabbitMQ)

RabbitMQ入门及安装 https://www.bilibili.com/video/BV1dX4y1V73Gp27 概述 简单概述&#xff1a; RabbitMQ是一个开源的遵循 AMQP协议实现的基于 Erlang语言编写&#xff0c;支持多种客户端&#xff08;语言&#xff09;&#xff0c;用于在分布式系统中存储消息&#xff0…

Spring Security 入门之自定义表单登录开发实现(三)

文章目录1. 前言2. 自定义认证2.1 自定义登录页面2.2 后端认证逻辑3. 自定义登陆成功处理3.1 登陆成功原理3.2 自定义登陆成功响应处理4. 自定义登陆失败处理4.1 登陆失败原理4.2 自定义登陆失败响应处理5. 注销用户处理5.1 注销原理总结1. 前言 在弄懂HelloWorld案例后&#…

Node.js | 使用内置模块 event 实现发布订阅模式

&#x1f5a5;️ NodeJS专栏&#xff1a;Node.js从入门到精通 &#x1f5a5;️ 蓝桥杯真题解析&#xff1a;蓝桥杯Web国赛真题解析 &#x1f9e7; 加入社区领红包&#xff1a;海底烧烤店ai&#xff08;从前端到全栈&#xff09; &#x1f9d1;‍&#x1f4bc;个人简介&#xff…