Spring - 事件监听机制 源码解析

news2024/9/29 23:39:21

文章目录

  • Pre
  • 概述
    • ApplicationEvent ------ 事件
    • ApplicationListener ------ 事件监听器
    • ApplicationEventPublisher ------ 事件发布者
    • ApplicationEventMulticaster ------ 事件广播器
  • spring主要的内置事件
    • ContextRefreshedEvent
    • ContextStartedEvent
    • ContextStoppedEvent
    • ContextClosedEvent
    • RequestHandledEvent
  • org.springframework.context.ApplicationListener
  • 源码分析
    • 初始化事件广播器
    • 注册事件监听器
    • 事件的发布和消费
      • 根据事件获取事件监听器
      • 唤醒监听器处理事件

在这里插入图片描述


Pre

Spring Boot - 扩展接口一览

在这里插入图片描述

众所周知,Spring Framework在 BeanFactory的基础容器之上扩展为了ApplicationContext上下文。 ApplicationContext处理包含了BeanFactory的全部基础功能之外,还额外提供了大量的扩展功能。

在这里插入图片描述

今天我们就来看看 扩展的 事件监听接口


概述

我们都知道 实现事件监听机制至少四个组成部分:

  • 事件
  • 事件生产者
  • 事件消费者
  • 控制器 (管理生产者、消费者和事件之间的注册监听关系)

在Spring中,事件监听机制主要实现是通过事件、事件监听器、事件发布者和事件广播器来实现。

ApplicationEvent ------ 事件

在这里插入图片描述

public abstract class ApplicationContextEvent extends ApplicationEvent {

	/**
	 * Create a new ContextStartedEvent.
	 * @param source the {@code ApplicationContext} that the event is raised for
	 * (must not be {@code null})
	 */
	public ApplicationContextEvent(ApplicationContext source) {
		super(source);
	}

	/**
	 * Get the {@code ApplicationContext} that the event was raised for.
	 */
	public final ApplicationContext getApplicationContext() {
		return (ApplicationContext) getSource();
	}

}

抽象父类ApplicationEvent,它的子抽象类ApplicationContextEvent 包含有当前ApplicationContext的引用,这样就可以确认每个事件是从哪一个Spring容器中发生的。


ApplicationListener ------ 事件监听器

顶级接口ApplicationListener,只有一个void onApplicationEvent(E event); ,当该监听器所监听的事件发生时,就会执行该方法


ApplicationEventPublisher ------ 事件发布者

顶级接口ApplicationEventPublisher,只有一个方法 void publishEvent(Object event); ,调用该方法就可以发生spring中的事件


ApplicationEventMulticaster ------ 事件广播器

spring中的事件核心控制器叫做事件广播器,两个作用

  • 将事件监听器注册到广播器中

    这样广播器就知道了每个事件监听器分别监听什么事件,且知道了每个事件对应哪些事件监听器在监听。

  • 将事件广播给事件监听器

    当有事件发生时,需要通过广播器来广播给所有的事件监听器,因为生产者只需要关心事件的生产,而不需要关心该事件都被哪些监听器消费。


spring主要的内置事件

ContextRefreshedEvent

ApplicationContext 被初始化或刷新时,该事件被发布。

也可以在ConfigurableApplicationContext接口中使用 refresh()方法来发生。

此处的初始化是指:所有的Bean被成功装载,后处理Bean被检测并激活,所有Singleton Bean 被预实例化,ApplicationContext容器已就绪可用。


ContextStartedEvent

当使用 ConfigurableApplicationContext 接口中的 start() 方法启动 ApplicationContext时,该事件被发布。

可以在接受到这个事件后重启任何停止的应用程序。


ContextStoppedEvent

当使用 ConfigurableApplicationContext接口中的 stop()停止ApplicationContext 时,发布这个事件。

可以在接受到这个事件后做必要的清理的工作


ContextClosedEvent

当使用 ConfigurableApplicationContext接口中的 close()方法关闭 ApplicationContext 时,该事件被发布。一个已关闭的上下文到达生命周期末端;它不能被刷新或重启


RequestHandledEvent

这是一个 web-specific 事件,告诉所有 bean HTTP 请求已经被服务。只能应用于使用DispatcherServlet的Web应用。在使用Spring作为前端的MVC控制器时,当Spring处理用户请求结束后,系统会自动触发该事件


org.springframework.context.ApplicationListener

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {

	/**
	 * Handle an application event.
	 * @param event the event to respond to
	 */
	void onApplicationEvent(E event);


	/**
	 * Create a new {@code ApplicationListener} for the given payload consumer.
	 * @param consumer the event payload consumer
	 * @param <T> the type of the event payload
	 * @return a corresponding {@code ApplicationListener} instance
	 * @since 5.3
	 * @see PayloadApplicationEvent
	 */
	static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
		return event -> consumer.accept(event.getPayload());
	}

}

在这里插入图片描述

ApplicationListener可以监听某个事件的event,触发时机可以穿插在业务方法执行过程中,用户可以自定义某个业务事件。


源码分析

首先看看Spring在初始化的时候,有两个核心步骤和事件监听器有关,一个是初始化事件广播器,一个是注册所有的事件监听器

org.springframework.context.support.AbstractApplicationContext#refresh
@Override
	public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			 		
				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();
 

				// Check for listener beans and register them.
				registerListeners();

				 
		 
	}

初始化事件广播器

	/** Spring容器的事件广播器对象*/
    private ApplicationEventMulticaster applicationEventMulticaster;

    /** 事件广播器对应的beanName*/
    public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";

    /** 初始化事件广播器*/
    protected void initApplicationEventMulticaster() {
        //1.获取Spring容器BeanFactory对象
        ConfigurableListableBeanFactory beanFactory = getBeanFactory();
        //2.从BeanFactory获取事件广播器的bean,如果存在说明是用户自定义的事件广播器
        if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
            //2.1.给容器的事件广播器赋值
            this.applicationEventMulticaster =
                    beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
            if (logger.isTraceEnabled()) {
                logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
            }
        }
        else {
            //3.如果没有自定义的,则初始化默认的事件广播器SimpleApplicationEventMulticaster对象
            this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
            //4.注册该bean
            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() + "]");
            }
        }
    }

如果beanFactory中存在用于自定义的就使用自定义的,如果没有自定义的就创建新的默认的事件广播器SimpleApplicationEventMulticaster对象,然后赋值给applicationEventMulticaster对象。


注册事件监听器

	/** 注册事件监听器*/
    protected void registerListeners() {
        //1.遍历将通过编码方式创建的事件监听器加入到事件广播器中
        for (ApplicationListener<?> listener : getApplicationListeners()) {
            //2.获取到当前事件广播器,添加事件监听器
            getApplicationEventMulticaster().addApplicationListener(listener);
        }

        //3.从BeanFactory中获取所有实现了ApplicationListener接口的bean,遍历加入到事件广播器中
        String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
        for (String listenerBeanName : listenerBeanNames) {
            getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
        }

        //3.获取需要提前发布的事件
        Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
        this.earlyApplicationEvents = null;
        if (earlyEventsToProcess != null) {
            for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
                //5.遍历将提前发布的事件广播出去
                getApplicationEventMulticaster().multicastEvent(earlyEvent);
            }
        }

从容器中找到所有的事件监听器,然后调用事件广播器的addApplicationListener方法将事件监听器添加到事件广播器中。

在这里插入图片描述


事件的发布和消费

在这里插入图片描述

事件的发布是通过ApplicationEventPublisher的实现类实现的publishEvent方法实现的,ApplicationContext就实现了该接口,所以使用Spring时就可以直接使用ApplicationContext实例来调用publishEvent方法来发布事件

	/** 发布事件
     * @param event:事件对象
     *  */
    @Override
    public void publishEvent(Object event) {
        publishEvent(event, null);
    }

    /** 发布事件
     * @param event:事件对象
     * @param eventType:事件类型
     * */
    protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
        Assert.notNull(event, "Event must not be null");

        /** 1.将发布的事件封装成ApplicationEvent对象(因为传入的参数是Object类型,有可能没有继承ApplicationEvent) */
        ApplicationEvent applicationEvent;
        if (event instanceof ApplicationEvent) {
            applicationEvent = (ApplicationEvent) event;
        }
        else {
            applicationEvent = new PayloadApplicationEvent<>(this, event);
            if (eventType == null) {
                eventType = ((PayloadApplicationEvent<?>) applicationEvent).getResolvableType();
            }
        }

        if (this.earlyApplicationEvents != null) {
            /** 2.1.如果需要提前发布的事件还没有发布完,则不是立即发布,而是将事件加入到待发布集合中*/
            this.earlyApplicationEvents.add(applicationEvent);
        }
        else {
            /** 2.2.获取当前的事件广播器,调用multicasterEvent方法广播事件*/
            getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
        }

        /** 3.如果当前applicationContext有父类,则再调用父类的publishEvent方法*/
        if (this.parent != null) {
            if (this.parent instanceof AbstractApplicationContext) {
                ((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
            }
            else {
                this.parent.publishEvent(event);
            }
        }
    }

首先是将发布的事件转化成ApplicationEvent对象,然后获取到事件广播器,调用事件广播器的multicastEvent方法来广播事件,所以核心逻辑又回到了事件广播器那里

	/** 广播事件
     * @param event:事件
     * @param eventType:事件类型
     * */
    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
        Executor executor = getTaskExecutor(); // (如果有Executor,则广播事件就是通过异步来处理的)
        /**
         * 1.根据事件和类型调用getApplicationListeners方法获取所有监听该事件的监听器
         * */
        for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
            if (executor != null) {
                /** 2. 异步遍历执行invokeListener方法来唤醒监听器处理事件 */
                executor.execute(() -> invokeListener(listener, event));
            }
            else {
                invokeListener(listener, event);
            }
        }
    }

这里主要有两个核心步骤,

  • 首先是根据事件和类型找到监听了该事件的所有事件监听器
  • 然后遍历来执行监听器的处理逻辑.另外如果配置了执行器Executor,就会通过Executor来异步发布事件给监听器

根据事件获取事件监听器

protected Collection<ApplicationListener<?>> getApplicationListeners(
            ApplicationEvent event, ResolvableType eventType) {

        Object source = event.getSource();
        Class<?> sourceType = (source != null ? source.getClass() : null);
        ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

        // Quick check for existing entry on ConcurrentHashMap...
        ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
        if (retriever != null) {
            return retriever.getApplicationListeners();
        }

        if (this.beanClassLoader == null ||
                (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
                        (sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
            // Fully synchronized building and caching of a ListenerRetriever
            synchronized (this.retrievalMutex) {
                retriever = this.retrieverCache.get(cacheKey);
                if (retriever != null) {
                    return retriever.getApplicationListeners();
                }
                retriever = new ListenerRetriever(true);
                Collection<ApplicationListener<?>> listeners =
                        retrieveApplicationListeners(eventType, sourceType, retriever);
                this.retrieverCache.put(cacheKey, retriever);
                return listeners;
            }
        }
        else {
            // No ListenerRetriever caching -> no synchronization necessary
            return retrieveApplicationListeners(eventType, sourceType, null);
        }
    }

核心方法是retrieveApplicationListeners(eventType, sourceType, retriever)方法,源码如下:

private Collection<ApplicationListener<?>> retrieveApplicationListeners(
            ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable ListenerRetriever retriever) {

        List<ApplicationListener<?>> allListeners = new ArrayList<>();
        Set<ApplicationListener<?>> listeners;
        Set<String> listenerBeans;
         /** 初始化所有事件监听器,存入集合中*/
        synchronized (this.retrievalMutex) {              
            listeners = new LinkedHashSet<>(this.defaultRetriever.applicationListeners);
            listenerBeans = new LinkedHashSet<>(this.defaultRetriever.applicationListenerBeans);
        }

        // Add programmatically registered listeners, including ones coming
        // 遍历所有监听器,调用supportsEvent判断是否监听该事件
        for (ApplicationListener<?> listener : listeners) {
            if (supportsEvent(listener, eventType, sourceType)) {
                if (retriever != null) {
                    retriever.applicationListeners.add(listener);
                }                   /** 如果监听器监听当前事件,则加入到监听器集合中*/
                allListeners.add(listener);
            }
        }

        // Add listeners by bean name, potentially overlapping with programmatically
        // registered listeners above - but here potentially with additional metadata.
        if (!listenerBeans.isEmpty()) {
            ConfigurableBeanFactory beanFactory = getBeanFactory();
                       //
            for (String listenerBeanName : listenerBeans) {
                try {
                    if (supportsEvent(beanFactory, listenerBeanName, eventType)) {
                        ApplicationListener<?> listener =
                                beanFactory.getBean(listenerBeanName, ApplicationListener.class);
                        if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType)) {
                            if (retriever != null) {
                                if (beanFactory.isSingleton(listenerBeanName)) {
                                    retriever.applicationListeners.add(listener);
                                }
                                else {
                                    retriever.applicationListenerBeans.add(listenerBeanName);
                                }
                            }
                            allListeners.add(listener);
                        }
                    }
                    else {
                        // Remove non-matching listeners that originally came from
                        // ApplicationListenerDetector, possibly ruled out by additional
                        // BeanDefinition metadata (e.g. factory method generics) above.
                        Object listener = beanFactory.getSingleton(listenerBeanName);
                        if (retriever != null) {
                            retriever.applicationListeners.remove(listener);
                        }
                        allListeners.remove(listener);
                    }
                }
                catch (NoSuchBeanDefinitionException ex) {
                    // Singleton listener instance (without backing bean definition) disappeared -
                    // probably in the middle of the destruction phase
                }
            }
        }

        /** 将所有监听器根据Order进行排序*/
        AnnotationAwareOrderComparator.sort(allListeners);
        if (retriever != null && retriever.applicationListenerBeans.isEmpty()) {
            retriever.applicationListeners.clear();
            retriever.applicationListeners.addAll(allListeners);
        }
        return allListeners;
    }

核心步骤:

  • 1:获取事件广播器中所有的事件监听器

  • 2:遍历事件监听器,判断该监听器是否监听当前事件

  • 3:将所有监听当前事件的监听器进行排序

第二步判断监听器是否监听事件的判断,主要是通过反射获取该监听器实现的接口泛型类,如果包含当前事件的类则表示监听,否则就表示不监听


唤醒监听器处理事件

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
        ErrorHandler errorHandler = getErrorHandler();
        if (errorHandler != null) {
            try {
                /** 调用doInvokeListener方法*/
                doInvokeListener(listener, event);
            }
            catch (Throwable err) {
                errorHandler.handleError(err);
            }
        }
        else {
            /** 调用doInvokeListener方法*/
            doInvokeListener(listener, event);
        }
    }

继续

private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
        try {
		    /** 直接调用ApplicationListener的onApplicationEvent(event)方法*/
            listener.onApplicationEvent(event);
        }
        catch (ClassCastException ex) {
              
        }
    }

直接调用监听器的onApplicationEvent方法

在这里插入图片描述

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

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

相关文章

设计模式概述之单例模式(四)

很多小伙伴&#xff0c;不知道设计模式是什么&#xff1f; 通常我们所说的设计模式是一种设计方案&#xff0c;是前人留下的经验及最佳实践。 想要学习设计模式&#xff0c;至少要把面向对象的基本结构全部了解。 设计模式&#xff0c;是建立在一定基础上的思维训练。 学习…

喜报 | 中关村发来贺电

2022年12月14日&#xff0c;由中关村金融科技产业发展联盟、中关村互联网金融研究院举办的“中关村金融科技系列活动——2023第十届中关村金融科技论坛年会暨2022“光大杯”中关村番钛客金融科技国际创新大赛颁奖典礼”已圆满落幕。本次会议为建设金融科技中心&#xff0c;共建…

【Pintos】实现自定义 UserProg 系统调用

&#x1f4ad; 写在前面&#xff1a;本文讲解的内容不属于 Pintos 的 Project 项目&#xff0c;而是关于 userprog 如何添加系统调用的&#xff0c;学习如何额外实现一些功能到系统调用中以供用户使用。因为涉及到 src/example 下的Makefile 的修改、lib 目录下 syscall-nr 系统…

搜索二叉树详解

&#x1f9f8;&#x1f9f8;&#x1f9f8;各位大佬大家好&#xff0c;我是猪皮兄弟&#x1f9f8;&#x1f9f8;&#x1f9f8; 文章目录一、搜索二叉树框架二、搜索二叉树概念三、搜索二叉树操作①Erase②Find递归③Insert递归④Erase递归&#xff0c;比Erase更简洁⑤析构函数⑥…

unity学习笔记--day01

今天学习制作了一个简单的抽卡功能&#xff0c;学习结束后目录结构如下&#xff1a; .mate文件是unity生成的配置文件&#xff0c;不用管 制作第一张卡片 创建一个空物体&#xff0c;改名为Card。 在Card下挂载以下UI组件&#xff1a; 编写数据脚本并挂载&#xff0c;unity采用…

Nginx教程(4)—Keepalived

文章目录4.1 高可用集群架构Keepalived双机主备原理4.2 安装Keepalived4.3 Keepalived核心配置文件4.4 Keepalived实现双主机主备高可用测试4.5 高可用集群架构Keepalived双主热备原理Nginx教程一 Nginx教程二 Nginx教程三 4.1 高可用集群架构Keepalived双机主备原理 我们知道…

【计算机毕业设计】78.汽车租赁系统源码

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 目 录 摘 要 前 言 第1章 概述 1.1 研究背景 1.2 研究目的 1.3 研究内容 第二章 开发技术介绍 2.1 Java技术 2.2 Mysql数据库 2.3 B/S结构 2.4 SSM框架 第三章 系统分析 3.1 可行性分析 3.1.1 技术…

UnrealUBlueprintAsyncActionBase的使用

实现异步调用&#xff0c;之前我们介绍过一种FLatentActionInfo的方法&#xff0c;还有另外一种UBlueprintAsyncActionBase方法&#xff0c;可以实现异步节点&#xff0c;用于异步监听然后进行回调。按照如下步骤进行使用&#xff0c;我们同样以Delay一定帧数&#xff0c;并每帧…

面对新技术,必须找到与其发展相辅相成的长期主义的方法

从Meta股价的一路走低到扎克伯格发布的头显并不被用户买账&#xff0c;Facebook全力拥抱Meta正在经历一场过山车。   扎克伯格和他所带领下的Meta遭遇到的如此多的困境和难题&#xff0c;越来越多地让我们开始相信&#xff1a;所谓的元宇宙并非是一蹴而就的&#xff0c;它是一…

【云原生 Kubernetes】基于 Minikube 搭建第一个k8s集群

一、前言 对于k8s来说&#xff0c;搭建方式有多种&#xff0c;如果是生产环境&#xff0c;一般来说&#xff0c;至少需要3台节点确保服务的高可用性&#xff0c;常用的搭建方式列举如下&#xff08;提供参考&#xff09;&#xff1a; kubeadm搭建&#xff08;推荐&#xff09; …

postman测试环境的创建及发送请求方式

目录 一、创建工作环境 1、打开postman&#xff0c;点击工作区 2、点击新建 3、添加名字&#xff0c;点击创建 4、工作区可以自由切换工作区 5、点击创建发送请求 6、更换请求方式 7、保存测试 二、测试发送请求&#xff0c;使用的时候服务一定要启动 1、普通传参&…

C++ 类型转换

目录 C语言中的类型转换 为什么C需要四种类型转换 C&#xff1a;命名的强制类型转换 static_cast reinterpret_cast const_cast dynamic_cast C语言中的类型转换 在C语言中&#xff0c;如果赋值运算符左右两侧类型不同&#xff0c;或者形参与实参类型不匹配&#xff0c…

信息学奥赛一本通——1163:阿克曼(Ackmann)函数

文章目录1163&#xff1a;阿克曼(Ackmann)函数【题目描述】【输入】【输出】【输入样例】【输出样例】分析代码1163&#xff1a;阿克曼(Ackmann)函数 时间限制:1000ms内存限制:65536KB提交数:24804通过数:20247时间限制: 1000 ms 内存限制: 65536 KB 提交数: 24804 通过数: 202…

第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现

第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现 文章目录第三十章 linux-模块的文件格式与EXPORT_SYMBOL的实现模块的文件格式EXPORT_SYMBOL的实现模块的文件格式 以内核模块形式存在的驱动程序&#xff0c;比如demodev.ko&#xff0c;其在文件的数据组织形式上是ELF&am…

数据结构---快速排序

快速排序分治法思想基准元素的选择元素交换双边循环法JAVA实现单边循环法JAVA实现快速排序也是从冒泡排序演化而来使用了 分治法&#xff08;快的原因&#xff09;快速排序和冒泡排序共同点&#xff1a;通过元素之间的比较和交换位置来达到排序的目的。 快速排序和冒泡排序不同…

JavaWeb核心:HTTPTomcatServlet

HTTP 概念: Hyper Text Transfer Protocol&#xff0c;超文本传输协议&#xff0c;规定了浏览器和服务器之间数据传输的规则。 HTTP-请求数据格式 HTTP-响应数据格式 响应状态码的大的分类 常见的响应状态码 Tomcat 简介 概念: Tomcat是Apache 软件基金会一个核心项目&#…

【云原生】Prometheus 自定义告警规则

文章目录一、概述二、告警实现流程三、告警规则1&#xff09;告警规则配置1&#xff09;监控服务器是否在线3&#xff09;告警数据的状态四、实战操作1&#xff09;下载 node_exporter2&#xff09;启动 node_exporter3&#xff09;配置Prometheus加载node_exporter4&#xff0…

这样也可以让图像正向扩散

🍿*★,*:.☆欢迎您/$:*.★* 🍿 怎样的扩散取决于b是不是随机噪声 是随机噪声 则是扩散模型 如stable diffision 如果是非噪声则是方向模型 方向模型是指 在已知几个连续的输入 后可以通过模型的辅助预测扩散的方向 而 stable diffision 是通过预测反扩散方向 本质就…

VS2017中OpenCV编程插件Image Watch安装和使用介绍

安装 下载适合vs2017最新版本的Image Watch(ImageWatch.vsix)&#xff0c;下载地址 安装ImageWatch&#xff0c;双击ImageWatch.vsix进行安装即可&#xff1b; 使用 打开一个OpenCV工程&#xff0c;在Debug下设置断点&#xff0c;通过view -> other windows -> Image W…

基于51单片机宠物自动投料喂食器控制系统仿真设计( proteus仿真+程序+讲解视频)

基于51单片机宠物自动投料喂食器控制系统仿真设计( proteus仿真程序讲解视频&#xff09; 仿真图proteus 7.8及以上 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;S0029 视频讲解 基于51单片机的宠物自动投料喂食器控制系统proteu…