【基于springboot分析Quartz(v2.3.2)的启动流程】

news2024/11/24 16:54:54

基于springboot分析Quartz(v2.3.2)的启动流程

最近公司的定时任务使用了Quartz框架,在开发中经常出现定任务不执行了的问题,但是我又找不到原因所在,可把我愁坏了。于是我决定看看Quartz框架是怎么调度任务的。(ps:适合用过Quart框架的同学阅读,如果从来没有用过Quartz框架的同学,可以看看我之前的文章【Quartz入门】)

如何定位到关键代码

1.通过控制台打印的关键日志入手

在程序启动时候,可以看到控制台会输出很多quartz相关的日志,从这些日志我们可以定位到quartz框架的初始化关键代码,下面是我本地启动时候打印的日志

2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'quartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.springframework.scheduling.quartz.LocalDataSourceJobStore' - which supports persistence. and is not clustered.

2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler 'quartzScheduler' initialized from an externally provided properties instance.
2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
2024-03-29T22:14:00.779+08:00  INFO 10044 --- [           main] org.quartz.core.QuartzScheduler          : JobFactory set to: org.springframework.scheduling.quartz.SpringBeanJobFactory@70a898b0
2024-03-29T22:14:01.496+08:00  INFO 10044 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8087 (http) with context path ''
2024-03-29T22:14:01.497+08:00  INFO 10044 --- [           main] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now

我这儿就通过最后一行的打印(o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now)定位到具体的代码中如下,并在此debug

image.png

  1. 可以看到scheduler.start()这行代码肯定是我们一个重要的突破口,从字面意思可以得知,Quartz框架在这个地方就启动了。
  2. 从左下角的堆栈信息可以看到quart启动流程是在context.refresh()阶段调用。

从日志定位到了关键方法,接下来我们就深入到start方法,深入到start方法,下面就看看start的核心逻辑到底在干嘛把

2.在job任务中debug分析上下文

image.png
可以看到第一个栈是SimpleThreadPool的WorkerThread内部类的一个线程,顺腾摸瓜最后定位到关键代码入口
QuartzSchedulerThread.run

分析代码

1.SchedulerFactoryBean.start

通过打印的日志定位到,代码入口SchedulerFactoryBean.start

public void start() throws SchedulerException {
      //首先,检查调度器的状态,如果已经在关闭中(shuttingDown)或已经关闭(closed),则抛出 SchedulerException 异常,表示调度器无法在关闭后重新启动
    if (shuttingDown|| closed) {
        throw new SchedulerException(
                "The Scheduler cannot be restarted after shutdown() has been called.");
    }

    // QTZ-212 : calling new schedulerStarting() method on the listeners
    // right after entering start()
    //调用 notifySchedulerListenersStarting() 方法通知调度器监听器,表示调度器即将启动
    notifySchedulerListenersStarting();
    //如果 initialStart 为 null,说明调度器是第一次启动:
    //设置 initialStart 为当前日期和时间。
    //调用作业存储器的 schedulerStarted() 方法,通知作业存储器调度器已经启动。
    //调用 startPlugins() 方法,启动插件。
    if (initialStart == null) {
        initialStart = new Date();
        this.resources.getJobStore().schedulerStarted();            
        startPlugins();
    } else {
    //如果 initialStart 不为 null,说明调度器已经启动过:
    //调用作业存储器的 schedulerResumed() 方法,通知作业存储器调度器已经恢复运行。
        resources.getJobStore().schedulerResumed();
    }
     //将调度器线程的暂停状态设置为 false,以确保调度器不处于暂停状态。
    schedThread.togglePause(false);

    getLog().info(
            "Scheduler " + resources.getUniqueIdentifier() + " started.");
    //通知调度器监听器调度器已经完全启动。
    notifySchedulerListenersStarted();
}

看到这儿,嘿嘿关键代码又来咯,核心代码this.resources.getJobStore().schedulerStarted();那我们接着分析吧

public void schedulerStarted() throws SchedulerException {
//首先,检查是否为集群模式(调用 isClustered() 方法)。
//如果是集群模式,创建并初始化集群管理线程(ClusterManager)。

//如果指定了 initializersLoader,将其设置为集群管理线程的上下文类加载器。
//调用集群管理线程的 initialize() 方法进行初始化。
    if (isClustered()) {
        clusterManagementThread = new ClusterManager();
        if(initializersLoader != null)
            clusterManagementThread.setContextClassLoader(initializersLoader);
        clusterManagementThread.initialize();
    } else {
        try {
            recoverJobs();
        } catch (SchedulerException se) {
            throw new SchedulerConfigException(
                    "Failure occured during job recovery.", se);
        }
    }
    //初始化触发器
    misfireHandler = new MisfireHandler();
    if(initializersLoader != null)
        misfireHandler.setContextClassLoader(initializersLoader);
    misfireHandler.initialize();
    schedulerRunning = true;
    
    getLog().debug("JobStore background threads started (as scheduler was started).");
}
  1. clusterManagementThread.initialize 判断当前节点是否是集群中目前执行任务节点,是则发送任务调度通知signalSchedulingChangeImmediately

public void run() {
    while (!shutdown) {

        if (!shutdown) {
            long timeToSleep = getClusterCheckinInterval();
            long transpiredTime = (System.currentTimeMillis() - lastCheckin);
            timeToSleep = timeToSleep - transpiredTime;
            if (timeToSleep <= 0) {
                timeToSleep = 100L;
            }

            if(numFails > 0) {
                timeToSleep = Math.max(getDbRetryInterval(), timeToSleep);
            }
            
            try {
                Thread.sleep(timeToSleep);
            } catch (Exception ignore) {
            }
        }

        if (!shutdown && this.manage()) {
            signalSchedulingChangeImmediately(0L);
        }

    }//while !shutdown
}
  1. misfireHandler.initialize主要就是启动一个线程,去查询错过执行的任务,立即发出调度变更的信号signalSchedulingChangeImmediately,并传递最早的新时间(earliestNewTime)。
@Override
public void run() {
    
    while (!shutdown) {

        long sTime = System.currentTimeMillis();

        RecoverMisfiredJobsResult recoverMisfiredJobsResult = manage();

        if (recoverMisfiredJobsResult.getProcessedMisfiredTriggerCount() > 0) {
            signalSchedulingChangeImmediately(recoverMisfiredJobsResult.getEarliestNewTime());
        }

        if (!shutdown) {
            long timeToSleep = 50l;  // At least a short pause to help balance threads
            if (!recoverMisfiredJobsResult.hasMoreMisfiredTriggers()) {
                timeToSleep = getMisfireThreshold() - (System.currentTimeMillis() - sTime);
                if (timeToSleep <= 0) {
                    timeToSleep = 50l;
                }

                if(numFails > 0) {
                    timeToSleep = Math.max(getDbRetryInterval(), timeToSleep);
                }
            }
            
            try {
                Thread.sleep(timeToSleep);
            } catch (Exception ignore) {
            }
        }//while !shutdown
    }
}

signalSchedulingChangeImmediately具体实现:QuartzSchedulerThread.signalSchedulingChange
到这儿,start方法执行已经到底了,维护了QuartzSchedulerThread类变量

public void signalSchedulingChange(long candidateNewNextFireTime) {
    synchronized(sigLock) {
        signaled = true;
        signaledNextFireTime = candidateNewNextFireTime;
        sigLock.notifyAll();
    }
}
总结一下scheduler.start()方法底层核心逻辑
  1. 器群模式实现启动集群线程,检查目前节点状态,如果目前节点可执行任务则标记立即执行任务调度(JobStoreSupport.signalSchedulingChangeImmediately
  2. 启动查询错过的任务线程MisFireHandler,去监听是否有错过的执行任务,有则发送任务调度通知(JobStoreSupport.signalSchedulingChangeImmediately)

上面两个线程都没真正的去调度我们的任务,主要就是维护集群,发送是否要执行任务调度的信号,执行signalSchedulingChangeImmediately方法,此方法修改的就是QuartzSchedulerThread类变量,以及唤醒sigLock锁,说明有其他线程在获取sigLock,做一些事儿,估计就是真正的在做任务调度的事儿了。
接下来就可以分析QuartzSchedulerThread谁在使用sigLock,但是我没有继续分析哈哈,我是转头去job任务debug一下,看一下上下文方法栈找到调度任务的线程

2.QuartzSchedulerThread.run

通过在job任务中debug,定位到核心的run方法,接下来就是分析它在干嘛了

(SchedulerFactoryBean.afterPropertiesSet()中会进行QuartzScheduler的初始化,初始化过程有个重要的成员变量QuartzSchedulerThread这个线程的run方法就是核心所在)

@Override
public void run() {
    int acquiresFailed = 0;

    while (!halted.get()) {
        try {
            // check if we're supposed to pause...
            synchronized (sigLock) {
                while (paused && !halted.get()) {
                    try {
                        // wait until togglePause(false) is called...
                        sigLock.wait(1000L);
                    } catch (InterruptedException ignore) {
                    }

                    // reset failure counter when paused, so that we don't
                    // wait again after unpausing
                    acquiresFailed = 0;
                }

                if (halted.get()) {
                    break;
                }
            }

            // wait a bit, if reading from job store is consistently
            // failing (e.g. DB is down or restarting)..
            if (acquiresFailed > 1) {
                try {
                    long delay = computeDelayForRepeatedErrors(qsRsrcs.getJobStore(), acquiresFailed);
                    Thread.sleep(delay);
                } catch (Exception ignore) {
                }
            }

            int availThreadCount = qsRsrcs.getThreadPool().blockForAvailableThreads();
            if(availThreadCount > 0) { // will always be true, due to semantics of blockForAvailableThreads...

                List<OperableTrigger> triggers;

                long now = System.currentTimeMillis();

                clearSignaledSchedulingChange();
                try {
                    triggers = qsRsrcs.getJobStore().acquireNextTriggers(
                            now + idleWaitTime, Math.min(availThreadCount, qsRsrcs.getMaxBatchSize()), qsRsrcs.getBatchTimeWindow());
                    acquiresFailed = 0;
                    if (log.isDebugEnabled())
                        log.debug("batch acquisition of " + (triggers == null ? 0 : triggers.size()) + " triggers");
                } catch (JobPersistenceException jpe) {
                    if (acquiresFailed == 0) {
                        qs.notifySchedulerListenersError(
                            "An error occurred while scanning for the next triggers to fire.",
                            jpe);
                    }
                    if (acquiresFailed < Integer.MAX_VALUE)
                        acquiresFailed++;
                    continue;
                } catch (RuntimeException e) {
                    if (acquiresFailed == 0) {
                        getLog().error("quartzSchedulerThreadLoop: RuntimeException "
                                +e.getMessage(), e);
                    }
                    if (acquiresFailed < Integer.MAX_VALUE)
                        acquiresFailed++;
                    continue;
                }

                if (triggers != null && !triggers.isEmpty()) {

                    now = System.currentTimeMillis();
                    long triggerTime = triggers.get(0).getNextFireTime().getTime();
                    long timeUntilTrigger = triggerTime - now;
                    while(timeUntilTrigger > 2) {
                        synchronized (sigLock) {
                            if (halted.get()) {
                                break;
                            }
                            if (!isCandidateNewTimeEarlierWithinReason(triggerTime, false)) {
                                try {
                                    // we could have blocked a long while
                                    // on 'synchronize', so we must recompute
                                    now = System.currentTimeMillis();
                                    timeUntilTrigger = triggerTime - now;
                                    if(timeUntilTrigger >= 1)
                                        sigLock.wait(timeUntilTrigger);
                                } catch (InterruptedException ignore) {
                                }
                            }
                        }
                        if(releaseIfScheduleChangedSignificantly(triggers, triggerTime)) {
                            break;
                        }
                        now = System.currentTimeMillis();
                        timeUntilTrigger = triggerTime - now;
                    }

                    // this happens if releaseIfScheduleChangedSignificantly decided to release triggers
                    if(triggers.isEmpty())
                        continue;

                    // set triggers to 'executing'
                    List<TriggerFiredResult> bndles = new ArrayList<TriggerFiredResult>();

                    boolean goAhead = true;
                    synchronized(sigLock) {
                        goAhead = !halted.get();
                    }
                    if(goAhead) {
                        try {
                            List<TriggerFiredResult> res = qsRsrcs.getJobStore().triggersFired(triggers);
                            if(res != null)
                                bndles = res;
                        } catch (SchedulerException se) {
                            qs.notifySchedulerListenersError(
                                    "An error occurred while firing triggers '"
                                            + triggers + "'", se);
                            //QTZ-179 : a problem occurred interacting with the triggers from the db
                            //we release them and loop again
                            for (int i = 0; i < triggers.size(); i++) {
                                qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                            }
                            continue;
                        }

                    }

                    for (int i = 0; i < bndles.size(); i++) {
                        TriggerFiredResult result =  bndles.get(i);
                        TriggerFiredBundle bndle =  result.getTriggerFiredBundle();
                        Exception exception = result.getException();

                        if (exception instanceof RuntimeException) {
                            getLog().error("RuntimeException while firing trigger " + triggers.get(i), exception);
                            qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                            continue;
                        }

                        // it's possible to get 'null' if the triggers was paused,
                        // blocked, or other similar occurrences that prevent it being
                        // fired at this time...  or if the scheduler was shutdown (halted)
                        if (bndle == null) {
                            qsRsrcs.getJobStore().releaseAcquiredTrigger(triggers.get(i));
                            continue;
                        }

                        JobRunShell shell = null;
                        try {
                            shell = qsRsrcs.getJobRunShellFactory().createJobRunShell(bndle);
                            shell.initialize(qs);
                        } catch (SchedulerException se) {
                            qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
                            continue;
                        }

                        if (qsRsrcs.getThreadPool().runInThread(shell) == false) {
                            // this case should never happen, as it is indicative of the
                            // scheduler being shutdown or a bug in the thread pool or
                            // a thread pool being used concurrently - which the docs
                            // say not to do...
                            getLog().error("ThreadPool.runInThread() return false!");
                            qsRsrcs.getJobStore().triggeredJobComplete(triggers.get(i), bndle.getJobDetail(), CompletedExecutionInstruction.SET_ALL_JOB_TRIGGERS_ERROR);
                        }

                    }

                    continue; // while (!halted)
                }
            } else { // if(availThreadCount > 0)
                // should never happen, if threadPool.blockForAvailableThreads() follows contract
                continue; // while (!halted)
            }

            long now = System.currentTimeMillis();
            long waitTime = now + getRandomizedIdleWaitTime();
            long timeUntilContinue = waitTime - now;
            synchronized(sigLock) {
                try {
                  if(!halted.get()) {
                    // QTZ-336 A job might have been completed in the mean time and we might have
                    // missed the scheduled changed signal by not waiting for the notify() yet
                    // Check that before waiting for too long in case this very job needs to be
                    // scheduled very soon
                    if (!isScheduleChanged()) {
                      sigLock.wait(timeUntilContinue);
                    }
                  }
                } catch (InterruptedException ignore) {
                }
            }

        } catch(RuntimeException re) {
            getLog().error("Runtime error occurred in main trigger firing loop.", re);
        }
    } // while (!halted)

    // drop references to scheduler stuff to aid garbage collection...
    qs = null;
    qsRsrcs = null;
}

上面是 Quartz 中 QuartzSchedulerThread 类的 run() 方法的具体代码。该方法是线程运行的主要逻辑,负责获取触发器并执行作业。

以下是 run() 方法的大致流程:

  1. 定义一个变量 acquiresFailed,用于记录连续获取触发器失败的次数。
  2. 进入一个循环,只要 halted 标志为 false,就会一直执行。
  3. 检查是否需要暂停调度器。
    • 如果需要暂停,进入等待状态,直到调用 togglePause(false) 方法来恢复调度器。
    • 如果 halted 标志为 true,跳出循环。
  4. 如果获取触发器的连续失败次数大于 1,等待一段时间。
    • 等待时间由 computeDelayForRepeatedErrors() 方法计算。
  5. 获取可用的线程数。
  6. 如果有可用线程,则获取下一批触发器并执行作业。
    • 获取触发器时,指定了最大批处理大小和时间窗口。
    • 如果获取触发器过程中发生异常,根据失败次数进行错误处理。
  7. 如果获取到触发器且触发器列表不为空,等待触发器的执行时间到来。
    • 如果期间发生调度器关闭、时间变化等情况,跳出循环。
    • 如果触发器执行时间到达或发生了显著的调度变化,跳出循环。
  8. 如果触发器列表为空,跳过本次循环。
  9. 设置触发器为 “executing” 状态。
  10. 创建 JobRunShell 对象,并初始化。
  • 如果发生异常,标记作业触发指令为 “SET_ALL_JOB_TRIGGERS_ERROR”。
  1. 在线程池中运行 JobRunShell
  • 如果返回值为 false,表示调度器已关闭或存在线程池的问题,进行相应的错误处理。
  1. 继续下一次循环,获取并执行下一批触发器。
  2. 如果没有可用线程,继续下一次循环。
  3. 计算随机的空闲等待时间,并等待一段时间。
  • 如果调度计划发生变化,提前结束等待。
  1. 在循环中捕获并处理 RuntimeException 异常。
  2. halted 标志为 true,跳出循环。
  3. 清除对调度器资源的引用,以便垃圾回收。

总结

通过启动日志、以及在任务中debug,反向推理出Quartz在springboot中的启动流程,以及Quartz框架调度任务的核心逻辑。授人以鱼不如授人以渔,希望本篇文章不仅仅能帮助大家理解Quartz,还能帮助大家学会去阅读框架源码。

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

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

相关文章

STM32 软件I2C方式读取AS5600磁编码器获取角度例程

STM32 软件I2C方式读取AS5600磁编码器获取角度例程 &#x1f516;本例程使用正点原子例程作为工程模板创建。 &#x1f4d8; 硬件电路部分 &#x1f33f;原理图部分&#xff1a; &#x1f33f;PCB布线和电路 &#x1f4d9;驱动代码部分 int main(void) {u16 i 0;u16 ra…

手动实现一个扩散模型DDPM

扩散模型是目前大部分AIGC生图模型的基座&#xff0c;其本质是用神经网络学习从高斯噪声逐步恢复图像的过程&#xff0c;本文用python代码从零开始构建了一个简单的扩散模型。 理论部分 DDPM(Denoising Diffusion Probabilistic Models) 是一种在生成对抗网络等技术的基础上发展…

【优选算法】双指针 -- 快乐数 和 盛最多水的容器

前言&#xff1a; &#x1f3af;个人博客&#xff1a;Dream_Chaser &#x1f388;刷题专栏&#xff1a;优选算法篇 &#x1f4da;本篇内容&#xff1a;03快乐数 和 04盛最多水的容器 目录 一、快乐数&#xff08;medium&#xff09; 1. 题⽬链接&#xff1a;202. 快乐数 2. …

c++初阶篇----string的底层模拟

string类的模拟 目录 string类的模拟功能介绍各功能的实现类的构造函数&#xff0c;拷贝构造函数&#xff0c;析构函数迭代器的实现string的内部容量访问成员函数string的修改成员函数string类的相关联函数string类的输入输出友元 汇总string功能的实现汇总测试代码 功能介绍 …

鸿蒙实战开发-使用属性动画实现自定义刷新组件

介绍 本篇Codelab主要介绍组件动画animation属性设置。当组件的某些通用属性变化时&#xff0c;可以通过属性动画实现渐变效果&#xff0c;提升用户体验。效果如图所示&#xff1a; 说明&#xff1a; 本Codelab使用的display接口处于mock阶段&#xff0c;在预览器上使用会显示…

element-ui switch 组件源码分享

今日简单分享 switch 组件源码&#xff0c;主要从以下四个方面来分享&#xff1a; 1、switch 组件的页面结构 2、switch 组件的属性 3、switch 组件的事件 4、switch 组件的方法 一、switch 组件的页面结构 二、switch 组件的属性 2.1 value / v-model 属性&#xff0c;绑…

基于Hive大数据分析springboot为后端以及vue为前端的的民宿系

标题基于Hive大数据分析springboot为后端以及vue为前端的的民宿系 本文介绍了如何利用Hive进行大数据分析,并结合Spring Boot和Vue构建了一个民宿管理系统。该民民宿管理系统包含用户和管理员登陆注册的功能,发布下架酒店信息,模糊搜索,酒店详情信息展示,收藏以及对收藏的…

SpringMVC源码分析(七)--数据绑定工厂

1.数据绑定工厂的使用 数据绑定工厂能够创建数据绑定器,将数据绑定到对象中,比如说当接收到请求时,经过http协议解析后数据刚开始都是字符串,此时我们希望将这些属性进行类型转换,并为对象赋值,示例如下: 1)先创建两个实体类Student和Teacher @Getter @Setter @ToSt…

封装性练习

练习 1 &#xff1a; 创建程序&#xff1a;在其中定义两个类&#xff1a; Person 和 PersonTest 类。定义如下&#xff1a; 用 setAge() 设置人的合法年龄 (0~130) &#xff0c;用 getAge() 返回人的年龄。在 PersonTest 类中实例化 Person 类的对象 b &#xff0c;调用 set…

Java八股文(算法)

Java八股文の算法 算法 算法 逆序输出字符串 题目描述&#xff1a;输入一个字符串&#xff0c;要求逆序输出。 public static String reverseString(String s) {StringBuilder sb new StringBuilder();for (int i s.length() - 1;i > 0;i--) {sb.append(s.charAt(i));}r…

基于多模态信息的语音处理(misp) 2023挑战:视听目标说话人提取

THE MULTIMODAL INFORMATION BASED SPEECH PROCESSING (MISP) 2023 CHALLENGE: AUDIO-VISUAL TARGET SPEAKER EXTRACTION 第二章 目标说话人提取之《基于多模态信息的语音处理(misp) 2023挑战:视听目标说话人提取》 文章目录 THE MULTIMODAL INFORMATION BASED SPEECH PROCESS…

MCU或者SOC常见驱动3:USART通信

MCU或者SOC常见驱动3&#xff1a;USART通信 本文目的前置知识点UART简介主要特性通信流程数据帧格式所有数据帧满足的格式特殊的帧和用处&#xff08;不是很清楚对不对&#xff09; 参考文献 本文目的 简单的介绍USART通信一下是什么&#xff0c;有什么用&#xff0c;以及相关…

鸿蒙OS开发实例:【窥探网络请求】

HarmonyOS 平台中使用网络请求&#xff0c;需要引入 "ohos.net.http", 并且需要在 module.json5 文件中申请网络权限, 即 “ohos.permission.INTERNET” 本篇文章将尝试使用 ohos.net.http 来实现网络请求 场景设定 WeiBo UniDemo HuaWei : 请求顺序WeiBo1 UniDem…

同元软控专业模型库系列——液压气动篇

01 引言 近年来&#xff0c;数字液压技术在工业领域的应用逐渐推广&#xff0c;为提升生产效率、降低能源消耗、实现智能化制造提供了新的可能性。数字液压技术的应用已经覆盖了工程机械、航空航天、能源设备等众多领域&#xff0c;具有巨大的发展潜力。 行业技术的发展融合在…

机器人码垛机:智能仓储系统的重要组成部分

随着科技的飞速进步&#xff0c;机器人技术已经渗透到了许多行业领域&#xff0c;其中&#xff0c;仓储业尤为显著。机器人码垛机作为智能仓储系统的重要组成部分&#xff0c;不仅提高了码垛效率&#xff0c;还降低了人工成本和安全风险。然而&#xff0c;在其广泛应用的同时&a…

C# OpenCvSharp-HoughCircles(霍夫圆检测) 简单计数

目录 效果 项目 代码 下载 效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using OpenCvSharp; using O…

pycharm复习

1.字面量 2.注释&#xff1a; 单行注释# 多行注释" " " " " " 3.变量&#xff1a; 变量名 变量值 print&#xff1a;输出多个结果&#xff0c;用逗号隔开 4.数据类型&#xff1a; string字符串int整数float浮点数 t…

WebSocket 详解-小案例展示

简介&#xff1a;Websocket是一种用于H5浏览器的实时通讯协议&#xff0c;可以做到数据的实时推送&#xff0c;可适用于广泛的工作环境&#xff0c;例如客服系统、物联网数据传输系统&#xff0c;该测试工具可用于websocket开发初期的测试工作。 文章末尾有此案例的完整源代码。…

Arcgis中使用NDVI阈值法提取农田shape

首先有一幅NDVI影像TIFF&#xff0c;对其查看农田上的NDVI范围&#xff0c;大概是0.1以上&#xff0c;因为是12月份&#xff0c;小麦播种完1-2个月&#xff0c;此时NDVI并不是很高&#xff0c;但是树林基本叶子掉落了&#xff0c;所以比较好提取农田。 打开地图代数-栅格计算器…

【漏洞分析】浅析android手游lua脚本的加密与解密(一)

主要用到的工具和环境&#xff1a; 1 win7系统一枚 2 quick-cocos2d-x的开发环境&#xff08;弄一个开发环境方便学习&#xff0c;而且大部分lua手游都是用的cocos2d-x框架&#xff0c;还有一个好处&#xff0c;可以查看源码关键函数中的特征字符串&#xff0c;然后在IDA定位到…