详解Spring event如何优雅实现系统业务解耦、实现原理及使用注意项

1.概述

在我们平时的项目业务系统开发过程中,一个需求功能的业务逻辑经常出现主线业务和副线业务之分。比如,在当下移动端电商app进行注册账号操作,注册成功之后会发送短信、邮箱、站内信等通知,发放红包活动抵用券,推送用户注册信息给大数据系统进行数据分析以便后期个性化推荐等等。由此看出一个注册接口代码逻辑需要干这么多事情,业余逻辑高度耦合,并且串行执行耗时严重,所以我们接下来将围绕如何解决这两个问题进行叙述。串行执行耗时这个问题只需要改成异步,也就是主线逻辑注册成功之后接口就可以返回,而剩下的副线业务逻辑异步执行即可,说到异步解耦我想很多同学就想到了消息队列MQ,因为其一大核心作用就是异步解耦,不过消息队列中间件引入系统相对来说是一个比较重的操作,而我们这里采取的是今天的主角Spring event来实现业务解耦。

Spring事件(Spring Event)是Spring框架的一项功能,它允许不同组件之间通过发布-订阅机制进行解耦的通信。在Spring中,事件是表示应用程序中特定事件的对象,例如用户注册、订单创建、数据更新等。当这些事件发生时,可以通知其他组件来执行相应的操作。

具体来说,Spring事件机制包含以下几个主要的部分:

  1. 事件(Event): 事件是一个普通的POJO类,用于封装与应用程序状态变化相关的信息。通常情况下,事件类继承自ApplicationEvent抽象类,Spring中提供了一些内置的事件,也可以自定义事件。
  2. 事件发布者(ApplicationEventPublisher): 事件发布者是一个接口,用于发布事件。在Spring中,ApplicationContext就是一个事件发布者,可以通过ApplicationContext的publishEvent()方法来发布事件。
  3. 事件监听器(ApplicationListener): 事件监听器是一个接口,用于监听事件并在事件发生时执行相应的逻辑。在Spring中,我们可以通过实现ApplicationListener接口或使用@EventListener注解来定义事件监听器。
  4. 事件监听器注册: 事件监听器需要注册到事件发布者(ApplicationContext)中,以便在事件发生时被正确调用。在Spring中,通常通过XML配置、注解或者编程方式将事件监听器注册到ApplicationContext中。

2.Spring Event使用示例

2.1 用户注册

下面我就基于用户注册成功之后进行短信邮箱、站内信通知,发放红包优惠券,推送用户信息给大数据系统进行示例展示

首先先定义一个用户类信息User:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User {private Long id;private String userNo;private String nickname;private String email;private String phone;private Integer gender;private Date birthday;private Integer isDelete; 
}

自定义一个注册事件

@getter
public class RegisterEvent extends ApplicationEvent {// 携带用户信息private User user;public RegisterEvent(Object source, User user) {super(source);this.user = user;}
}

定义事件监听器

事件监听器有两种实现方式,一种是实现ApplicationListener接口,另一种是使用@EventListener注解。

三个监听器如下所示:消息通知和发送红包监听器通过实现ApplicationListener接口

@Slf4j
@Component // 把监听器注册到spring容器中
public class RegisterMsgNoticeListener implements ApplicationListener<RegisterEvent> {@Overridepublic void onApplicationEvent(RegisterEvent event) {log.info("=========>>>站内信通知了");log.info("=========>>>短信通知了");log.info("=========>>>邮箱通知了");}
}
@Slf4j
@Component
@Order(1)
public class RegisterSendRedPacketListener implements ApplicationListener<RegisterEvent> {@SneakyThrows@Overridepublic void onApplicationEvent(RegisterEvent event) {log.info("======>>>发放红包了");// 睡眠一下,模拟发送优惠券比较复杂TimeUnit.SECONDS.sleep(2);log.info("======>>>发放优惠券了");}
}

使用@EventListener实现推送用户信息监听器

@Slf4j
@Component
public class RegisterPushDataListener{@EventListenerpublic void onApplicationEvent(RegisterEvent event) {log.info("======>>>推送用户信息到大数据系统了,user={}", event.getUser());}
}

事件发布:

@Slf4j
@Service
public class UserServiceImpl implements UserService {@Resourceprivate ApplicationContext applicationContext;@Overridepublic void registerUser(User user) {log.info("=====>>>user注册成功了");applicationContext.publishEvent(new RegisterEvent(this, user));}
}

单元测试用例:

@SpringBootTest
@RunWith(SpringRunner.class)
public class UserServiceImplTest {@Resourceprivate UserService userService;@Testpublic void testEvent() {User user = User.builder().userNo("1111").birthday(new Date()).gender(0).phone("12345677890").email("shepherd@163.com").nickname("芽儿哟").build();userService.registerUser(user);}}

执行结果如下:

=====>>>user注册成功了
=========>>>站内信通知了
=========>>>短信通知了
=========>>>邮箱通知了
======>>>推送用户信息到大数据系统了,user=User(id=null, userNo=1111, nickname=芽儿哟, email=shepherd@163.com, phone=12345677890, gender=0, birthday=Tue Apr 09 17:12:25 CST 2024, isDelete=null)
======>>>发放红包了
======>>>发放优惠券了
=====>>>user注册完成结束了

如果我们要控制监听器的执行顺序,使用@Order即可,注意如果是实现了ApplicationListener,我们把@Order放到bean类上即可,但如果是通过注解@EventListener实现的,就必须写到方法上,下面就是先执行发送红包优惠券监听器,再执行消息通知监听器,最后才执行推送用户数据监听器。注意异步的情况下只保证按顺序将监听器丢入进线程池,具体事件处理执行顺序是不确定的

@Slf4j
@Component
@Order(1)
public class RegisterSendRedPacketListener implements ApplicationListener<RegisterEvent> {@SneakyThrows@Overridepublic void onApplicationEvent(RegisterEvent event) {log.info("======>>>发放红包了");// 睡眠一下,模拟发送优惠券比较复杂TimeUnit.SECONDS.sleep(2);log.info("======>>>发放优惠券了");}
}@Slf4j
@Component // 把监听器注册到spring容器中
@Order(2)
public class RegisterMsgNoticeListener implements ApplicationListener<RegisterEvent> {@Overridepublic void onApplicationEvent(RegisterEvent event) {log.info("=========>>>站内信通知了");log.info("=========>>>短信通知了");log.info("=========>>>邮箱通知了");}
}@Slf4j
@Component
public class RegisterPushDataListener{@EventListener@Order(3)public void onApplicationEvent(RegisterEvent event) {log.info("======>>>推送用户信息到大数据系统了,user={}", event.getUser());}
}

你知道Spring Event发布订阅事件处理默认是同步还是异步的?基于前面示例执行结果知道默认是同步的,很多同学因为基于消息队列MQ异步解耦的思想,自然而然以为是Spring Event的事件处理是异步的,这是一个误区。Spring Boot并不会自动默认维护一个线程池来处理event事件,要想异步处理事件使用 @Async 标记即可,注意前提条件是:使用 @EnableAsync 开启 Spring 异步:

@SpringBootApplication
@EnableAsync
public class BaseDemoApplication {public static void main(String[] args) {SpringApplication.run(BaseDemoApplication.class, args);}
}

使用@Async 的时候,一般都会自定义线程池,因为@Async的默认线程池为 SimpleAsyncTaskExecutor,不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。

@Configuration
public class InitConfig {/*** 初始化一个线程池,放入spring beanFactory* @return*/@Bean(name = "asyncExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10);executor.setMaxPoolSize(20);executor.setQueueCapacity(200);executor.setKeepAliveSeconds(60);executor.setThreadNamePrefix("asyncExecutor-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return executor;}
}

分别在监听器加上@Async注解:

@Slf4j
@Component // 把监听器注册到spring容器中
@Order(2)
@Async("asyncExecutor")
public class RegisterMsgNoticeListener implements ApplicationListener<RegisterEvent> {@Overridepublic void onApplicationEvent(RegisterEvent event) {log.info("=========>>>站内信通知了");log.info("=========>>>短信通知了");log.info("=========>>>邮箱通知了");}
}@Slf4j
@Component
@Order(1)
@Async("asyncExecutor")
public class RegisterSendRedPacketListener implements ApplicationListener<RegisterEvent> {@SneakyThrows@Overridepublic void onApplicationEvent(RegisterEvent event) {log.info("======>>>发放红包了");// 睡眠一下,模拟发送优惠券比较复杂TimeUnit.SECONDS.sleep(2);log.info("======>>>发放优惠券了");}
}@Slf4j
@Component
public class RegisterPushDataListener{@EventListener@Order(3)@Async("asyncExecutor")public void onApplicationEvent(RegisterEvent event) {log.info("======>>>推送用户信息到大数据系统了,user={}", event.getUser());}
}

通过执行结果日志打印可以看到开启多线程异步执行了,并且每次执行结果不确定,验证了上面所说的异步情况下@Order不再能控制监听器的执行顺序了。

2.2 借助事件进行启动初始化

在日常开发中,我们经常碰到需要再项目系统服务启动时进行一些业务上逻辑处理、数据初始化等操作,比如基础数据的写入、缓存的加载、任务的开启等等。实现这个功能的方式有很多,这里我们就用Spring提供的事件ContextRefreshedEvent来实现,当ApplicationContext被初始化或刷新之后触发该事件。

@Slf4j
@Component
public class InitListener implements ApplicationListener<ContextRefreshedEvent> {@Overridepublic void onApplicationEvent(ContextRefreshedEvent event) {log.info("========>>>服务启动了,执行业务初始化操作了");}
}

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

3.Spring Event实现原理

Spring Event是一种基于观察者模式(Observer Pattern)的实现。观察者模式(Observer Design Pattern)也被称为发布订阅模式。其定义是:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。Spring Event发布订阅的流程如下图所示:

直接从入口发布applicationContext.publishEvent()开始分析,会来到AbstractApplicationContext#publishEvent()

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {Assert.notNull(event, "Event must not be null");// Decorate event as an ApplicationEvent if necessaryApplicationEvent applicationEvent;if (event instanceof ApplicationEvent) {applicationEvent = (ApplicationEvent) event;}else {applicationEvent = new PayloadApplicationEvent<>(this, event);if (eventType == null) {eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();}}// Multicast right now if possible - or lazily once the multicaster is initialized// ApplicationEventMulticaster 未初始化完成时先将applicationEvent 暂存if (this.earlyApplicationEvents != null) {this.earlyApplicationEvents.add(applicationEvent);}else {// 获取监听管理器ApplicationEventMulticaster并进行广播,事件处理核心入口所在getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);}// Publish event via parent context as well...if (this.parent != null) {if (this.parent instanceof AbstractApplicationContext) {((AbstractApplicationContext) this.parent).publishEvent(event, eventType);}else {this.parent.publishEvent(event);}}}
ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException {if (this.applicationEventMulticaster == null) {throw new IllegalStateException("ApplicationEventMulticaster not initialized - " +"call 'refresh' before multicasting events via the context: " + this);}return this.applicationEventMulticaster;
}

ApplicationEventMulticaster是在Spring启动时核心方法AbstractApplicationContext#refresh()中进行注入的:

@Overridepublic void refresh() throws BeansException, IllegalStateException {synchronized (this.startupShutdownMonitor) {// Prepare this context for refreshing.prepareRefresh();// Tell the subclass to refresh the internal bean factory.//初始化beanfactorConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();// Prepare the bean factory for use in this context.// beanFactory赋值prepareBeanFactory(beanFactory);try {// Allows post-processing of the bean factory in context subclasses.//空实现,提供子类覆盖的额外处理,即子类处理自定义的beanFactorypostProcesspostProcessBeanFactory(beanFactory);// Invoke factory processors registered as beans in the context.//增强beanFactory功能如自动装配等invokeBeanFactoryPostProcessors(beanFactory);// Register bean processors that intercept bean creation.//创建注册beanPostProcessorregisterBeanPostProcessors(beanFactory);// Initialize message source for this context.//国际化处理initMessageSource();// Initialize event multicaster for this context.//初始化多播器initApplicationEventMulticaster();// Initialize other special beans in specific context subclasses.// 初始化web服务器等beanonRefresh();// Check for listener beans and register them.//将所有的ApplicationListener添加到事件多播器中registerListeners();// Instantiate all remaining (non-lazy-init) singletons.//实例化所有非懒加载的单例beanfinishBeanFactoryInitialization(beanFactory);// Last step: publish corresponding event.//启动servelt服务器等finishRefresh();}
protected void initApplicationEventMulticaster() {ConfigurableListableBeanFactory beanFactory = getBeanFactory();if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {this.applicationEventMulticaster =beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);if (logger.isTraceEnabled()) {logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");}}else {this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);if (logger.isTraceEnabled()) {logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +"[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");}}
}

注册ApplicationEventMulticaster的逻辑很简单,如果Spring容器中有了ApplicationEventMulticaster就使用自定义的,不然就会创建默认的SimpleApplicationEventMulticaster放入容器中。

接下来我们就进入事件处理核心所在:SimpleApplicationEventMulticaster#multicastEvent()

	@Overridepublic void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));// 获取线程池Executor executor = getTaskExecutor();// 循环遍历调用监听器for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {// 是否存在线程池 异步执行逻辑if (executor != null) {executor.execute(() -> invokeListener(listener, event));}else {// 非异步线程处理invokeListener(listener, event);}}}

这里就可以看出在处理事件会先获取线程池,没有的话就同步执行,这也解释上面所说的Spring Event默认是同步处理事件的。接着往下看,执行监听器处理逻辑

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {ErrorHandler errorHandler = getErrorHandler();// 是否存在 ErrorHandler if (errorHandler != null) {try {doInvokeListener(listener, event);}catch (Throwable err) {errorHandler.handleError(err);}}else {doInvokeListener(listener, event);}}private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {try {// 执行监听器的onApplicationEvent()listener.onApplicationEvent(event);}catch (ClassCastException ex) {String msg = ex.getMessage();if (msg == null || matchesClassCastMessage(msg, event.getClass())) {// Possibly a lambda-defined listener which we could not resolve the generic event type for// -> let's suppress the exception and just log a debug message.Log logger = LogFactory.getLog(getClass());if (logger.isTraceEnabled()) {logger.trace("Non-matching event type for listener: " + listener, ex);}}else {throw ex;}}}

从方法#invokeListener()可以看出会先判断是否定义了ErrorHandler,有的话在事件处理过程中出现异常会进行异常捕获并做相应处理,如果没有就是直接报错毫无处理。结论就是:**最终事件的执行是由同一个线程按顺序来完成的,任何一个报错,都会导致后续的监听器执行不了。**这里我就不做演示了,把上面案例改为同步,然后某个监听器的处理逻辑报错就可以验证了,我们可以通过自定义一个事件广播器来解决,从上面Spring启动初始化可以看出只是new了一个SimpleApplicationEventMulticaster对象放入容器中,并没有为其线程池Exector属性进行赋值,这也是为啥默认是单线程同步处理事件的原因所在,所以我们可以自定义一个事件广播器设置好线程池,这样事件处理默认就是异步的了,不需要再在监听器是使用@Async。与此同时我们也可以自定义一个事件异常处理器来对处理事件过程中发生异常进行相应处理,保证不同监听器的事件处理互不干扰,逻辑如下所示

@Slf4j
@Configuration
public class InitConfig {/*** 自定义事件广播器,异步处理事件,这样监听器就不需要使用@Async注解了* @param executor* @return*/@Bean(AbstractApplicationContext.APPLICATION_EVENT_MULTICASTER_BEAN_NAME)public SimpleApplicationEventMulticaster simpleApplicationEventMulticaster(@Qualifier("asyncExecutor") Executor executor,ErrorHandler errorHandler) {SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = new SimpleApplicationEventMulticaster();simpleApplicationEventMulticaster.setTaskExecutor(executor);simpleApplicationEventMulticaster.setErrorHandler(errorHandler);return simpleApplicationEventMulticaster;}/*** 注入一个事件异常处理器* @return*/@Beanpublic ErrorHandler errorHandler() {return (t) -> {log.error("listener handle error: ", t);};}/*** 初始化一个线程池,放入spring beanFactory* @return*/@Bean(name = "asyncExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(10);executor.setMaxPoolSize(20);executor.setQueueCapacity(200);executor.setKeepAliveSeconds(60);executor.setThreadNamePrefix("asyncExecutor-");executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());return executor;}
}

整体类图如下:

4.Spring Event与消息队列MQ的区别

Spring Event和消息队列(MQ)是两种不同的消息传递机制,它们在实现消息通信方面有各自的优缺点。

Spring Event的优缺点: 优点:

简单易用: Spring Event是Spring框架提供的一个内置的事件发布-订阅机制,使用起来非常简单,无需引入额外的依赖。

无中间件依赖: Spring Event不依赖于任何消息中间件,适用于小型项目或者简单的消息通信场景。

模块解耦: Spring Event可以帮助实现模块之间的解耦,提高系统的灵活性和可维护性。

缺点:

单点问题: Spring Event是在单个应用内部的事件通知机制,如果应用崩溃或者重启,事件将会丢失。

不支持分布式: Spring Event只能在单个应用内部传递消息,不支持分布式环境下的消息传递。

性能问题: Spring Event在大规模消息通信场景下可能会存在性能问题,因为它是同步执行的,消息发布者需要等待所有订阅者处理完消息后才能继续执行。

消息队列(MQ)的优缺点: 优点:

异步处理: 消息队列支持异步消息处理,提高系统的并发能力和响应速度。

可靠性: 消息队列通常具有消息持久化、消息重试等特性,能够保证消息传递的可靠性。

分布式支持: 消息队列支持分布式环境下的消息传递,可以实现跨服务、跨应用的消息通信。

缺点:

复杂性: 使用消息队列需要引入额外的消息中间件,并且需要配置和管理这些中间件,增加了系统的复杂性。

维护成本: 消息队列需要维护消息中间件的稳定性和可用性,需要投入一定的维护成本。

一致性问题: 消息队列在消息传递过程中可能会出现一致性问题,需要额外的设计和处理。

综上所述,Spring Event适用于简单的应用内部消息通信场景,操作简单但有一定的局限性;消息队列适用于分布式、高并发的消息通信场景,可以提供更高的可靠性和灵活性,但需要考虑复杂性和维护成本。在选择使用哪种方式时,需要根据具体的业务需求和系统架构来进行权衡和选择。

5.总结

综上所述,Spring Event在业务系统中的实际使用案例包括订单支付成功事件、用户注册事件等,可以带来模块解耦、异步处理、增强扩展性等优点。然而,对于复杂的业务场景、事件失效风险以及调试困难等缺点需要进行注意和权衡。在使用Spring Event时,需要根据具体业务需求和系统特点进行合理的选择和使用。在使用需注意一下几点:

  • 监听器默认同步执行,不要误认为和消息队列MQ一样异步消费消息的,Spring Event是应用内部发布-订阅机制,如果事件处理逻辑过于复杂同步阻塞可能对当前主流程带来影响,建议使用异步的方式。

  • 不要依赖监听器执行顺序:首先我认为监听器之间有依赖关系就说明设计是有问题的,这不就是耦合依赖吗?和我们使用Spring Event的初衷有点背道而驰,如果两个监听器事件处理有前后依赖顺序,就应该想办法合并成一个。虽然我们可以使用 @Order 来控制监听器之间的执行顺序,但是仅在同步执行的场景下有效,监听器异步执行的情况下实际执行顺序仍然是不可控的。

  • 监听器的事件处理并不绝对可靠

    • 多个监听器事件处理的执行是由同一个线程按顺序来完成的,任何一个报错,都会导致后续的监听器执行不了
    • 程序关闭时可能发生监听事件未处理完成。
  • 事务事件:Spring Event同步执行的时候,是和主业务方法事务一起的,可能会出现下面这种异常情况,用户注册成功后发布消息通知事件,但在后续的事务处理中处理异常导致事务回滚,会出现用户收到注册成功短信但实际没有注册成功。所以我们一般认为Spring 事件是子任务,和主业务事务不需要强一致性。

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

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

相关文章

LinuxAndroid: 旋转编码器input输入事件适配(旋转输入)

rk3588s: 旋转编码器input输入事件适配 基于Android 12 kernel-5.10版本 参考文档&#xff1a; https://blog.csdn.net/szembed/article/details/131551950 Linux 输入设备调试详解&#xff08;零基础开发&#xff09;Rotary_Encoder旋转编码器驱动 通用GPIO为例 挂载input输…

日志监控思路分享,只监控日志内容,不存储

有一个这样的需求&#xff0c;就是实时监控日志文件的内容&#xff0c;不需要存储&#xff0c;仅当某行日志内容触发某个规则时调用一段业务逻辑就行了。比如用户触发限流规则&#xff0c;就将其封禁并发送钉钉通知到运维群。 看到这个需求首先想到的就是日志采集工具&#xff…

零售EDI:Princess Auto EDI对接

Princess Auto 是一家加拿大零售连锁店&#xff0c;专门从事农场、工业、车库、液压和剩余物品的销售。 Princess Auto 总部位于马尼托巴省温尼伯&#xff0c;截至 2024 年 1 月在 10 个省份拥有并经营 55 家商店以及三个配送中心。各种商品均以其“Powerfist”和“Pro.Point”…

Node.js 入门

一、什么是 Node.js&#xff1f; 定义&#xff1a; 作用&#xff1a;使用 Node.js 编写服务器端程序 编写数据接口&#xff0c;提供网页资源浏览功能等等 前端工程化&#xff1a;为后续学习 Vue 和 React 等框架做铺垫 二、什么是前端工程化&#xff1f; 前端工程化&#xf…

学浪视频怎么下载到手机相册

学浪视频如何下载到手机相册&#xff0c;很简单&#xff0c;首先将学浪课程下载到电脑&#xff0c;然后再由电脑传输到手机中&#xff0c;这样你就可以在手机相册里面观看。 但是将学浪课程下载到本地是不可以使用录屏的方式&#xff0c;所以这里要借助一款工具&#xff0c;小…

自动化测试-web(弹窗/滚动条/鼠标/等待等操作)

一、弹窗 为什么要处理弹窗&#xff1f; 如果页面操作过程中&#xff0c;有弹窗出现&#xff0c;不处理&#xff0c;无法继续对页面操作。 弹窗类型&#xff1a; js原生弹窗&#xff1a; 警告框、输入框、提示框&#xff0c;这些必须处理 如何处理&#xff1a; 1&#xff0…

Node.js cnpm的安装

百度搜索 cnpm,进入npmmirror 镜像站https://npmmirror.com/ cmd窗口输入 npm install -g cnpm --registryhttps://registry.npmmirror.com

dyld: Library not loaded: @rpath/SDK.framework/SDK错误问题

关于导入三方SDK.framework之后&#xff0c;启动崩溃之后如下报错的解决方式: 截屏2020-10-14 上午9.55.09.png 在正常导入framework之后&#xff0c;做如图示操作&#xff0c; image.png 以上步骤之后&#xff0c;重新启动运行xcode&#xff0c;即可成功运行。

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之八 简单视频素描效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之八 简单视频素描效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之八 简单视频素描效果 一、简单介绍 二、简单指定视频某片段快放效果实现原理 三、简单指定视频某…

[leetcode]remove-duplicates-from-sorted-list

. - 力扣&#xff08;LeetCode&#xff09; 给定一个已排序的链表的头 head &#xff0c; 删除所有重复的元素&#xff0c;使每个元素只出现一次 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,1,2] 输出&#xff1a;[1,2]示例 2&#xff1a; 输入&…

安全风险攻击面管理如何提升企业网络弹性?

从研究人员近些年的调查结果来看&#xff0c;威胁攻击者目前非常善于识别和利用最具有成本效益的网络入侵方法&#xff0c;这就凸显出了企业实施资产识别并了解其资产与整个资产相关的安全态势的迫切需要。 目前来看&#xff0c;为了在如此复杂的网络环境中受到最小程度上的网络…

Unity Pro 2019 for Mac:专业级游戏引擎,助力创意无限延伸!

Unity Pro 2019是一款功能强大的游戏开发引擎&#xff0c;其特点主要体现在以下几个方面&#xff1a; 强大的渲染技术&#xff1a;Unity Pro 2019采用了新的渲染技术&#xff0c;包括脚本化渲染流水线&#xff0c;能够轻松自定义渲染管线&#xff0c;通过C#代码和材料材质&…

uniapp 上传视频到阿里云之后回显视频获取视频封面

uniapp 上传视频到阿里云之后回显视频获取视频封面 官网的解决方案 1.initial-time Number 指定视频初始播放位置&#xff0c;单位为秒&#xff08;s&#xff09;。 没什么卵用 2.使用 uni.createVideoContext(“myVideo”, this).seek(number)。 没什么卵用 <video :id&quo…

云计算重要概念之:虚拟机、网卡、交换机、路由器、防火墙

一、虚拟机 (Virtual Machine, VM) 1.主流的虚拟化软件&#xff1a; 虚拟化软件通过在单个物理硬件上创建和管理多个虚拟环境&#xff08;虚拟机&#xff09;&#xff0c;实现资源的高效利用、灵活部署、隔离安全以及便捷管理&#xff0c;是构建云计算和现代化数据中心的核心…

C语言面试题之返回倒数第 k 个节点

返回倒数第 k 个节点 实例要求 1、实现一种算法&#xff0c;找出单向链表中倒数第 k 个节点&#xff1b;2、返回该节点的值&#xff1b; 示例&#xff1a;输入&#xff1a; 1->2->3->4->5 和 k 2 输出&#xff1a; 4 说明&#xff1a;给定的 k 保证是有效的。实…

【机器学习300问】65、为什么Sigmoid和Tanh激活函数会导致梯度消失?

一、梯度消失现象 当神经网络的输入值较大或较小时&#xff0c;其导数&#xff08;梯度&#xff09;都会接近于0。在反向传播过程中&#xff0c;这些微小的梯度经过多层网络逐层传递时&#xff0c;会不断被乘以权重矩阵&#xff08;权重通常小于1&#xff09;&#xff0c;进一步…

智能时代中的工业应用中前所未有的灵活桥接和I/O扩展功能解决方案MachXO2系列LCMXO2-1200HC-4TG100I FPGA可编程逻辑IC

lattice莱迪斯 MachXO2系列LCMXO2-1200HC-4TG100I超低密度FPGA现场可编程门阵列&#xff0c;适用于低成本的复杂系统控制和视频接口设计开发&#xff0c;满足了通信、计算、工业、消费电子和医疗市场所需的系统控制和接口应用。 瞬时启动&#xff0c;迅速实现控制——启动时间…

Java项目:基于Springboot+vue实现的中国陕西民俗前后台管理系统设计与实现(源码+数据库+毕业论文)

一、项目简介 本项目是一套基于Springbootvue实现的中国陕西民俗管理系统设计与实现设 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界…

安全大脑与盲人摸象

21世纪是数字科技和数字经济爆发的时代&#xff0c;互联网正从网状结构向类脑模型进行进化&#xff0c;出现了结构和覆盖范围庞大&#xff0c;能够适应不同技术环境、经济场景&#xff0c;跨地域、跨行业的类脑复杂巨型系统。如腾讯、Facebook等社交网络具备的神经网络特征&…

Web漏洞-文件上传之内容逻辑数组

图片一句话制作方法&#xff1a; copy 1.png /b shell.php /a webshell.jpg 具体示例见upload-labs 的14-17 二次渲染----见Pass-18 用/.或者%00绕过&#xff1a;Pass-20----Pass-21 CVE-2017-12615复现 创好环境后打开环境&#xff0c;再访问ip8080 抓包发送数据 Shell的…