定时器任务——若依源码分析

news2025/6/12 19:22:54

分析util包下面的工具类schedule utils:

ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。

createScheduleJob

createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobDetail 和 CronTrigger,设置调度策略和参数,然后将任务提交给调度器,并根据任务状态决定是否立即暂停

/**
     * 创建定时任务
     */
    public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException
    {
        Class<? extends Job> jobClass = getQuartzJobClass(job);
        // 构建job信息
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();

        // 表达式调度构建器
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);

        // 按新的cronExpression表达式构建一个新的trigger
        CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
                .withSchedule(cronScheduleBuilder).build();

        // 放入参数,运行时的方法可以获取
        jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);

        // 判断是否存在
        if (scheduler.checkExists(getJobKey(jobId, jobGroup)))
        {
            // 防止创建时存在数据问题 先移除,然后在执行创建操作
            scheduler.deleteJob(getJobKey(jobId, jobGroup));
        }

        // 判断任务是否过期
        if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression())))
        {
            // 执行调度任务
            scheduler.scheduleJob(jobDetail, trigger);
        }

        // 暂停任务
        if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue()))
        {
            scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        }
    }
1. 获取要执行的 Job 实现类
Class<? extends Job> jobClass = getQuartzJobClass(job);

根据任务的并发属性,返回:

  • QuartzJob.class

  • QuartzDisallowConcurrentExecution.class不允许并发执行同一种任务

 用于控制是否允许并发执行同一任务

QuartzJob和QuartzDisallowConcurrentExecution的父类:

/**
 * 抽象quartz调用
 *
 * @author ruoyi
 */
public abstract class AbstractQuartzJob implements Job
{
    private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);

    /**
     * 线程本地变量
     */
    private static ThreadLocal<Date> threadLocal = new ThreadLocal<>();

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException
    {
        SysJob sysJob = new SysJob();
        BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));
        try
        {
            before(context, sysJob);
            if (sysJob != null)
            {
                doExecute(context, sysJob);
            }
            after(context, sysJob, null);
        }
        catch (Exception e)
        {
            log.error("任务执行异常  - :", e);
            after(context, sysJob, e);
        }
    }

    /**
     * 执行前
     *
     * @param context 工作执行上下文对象
     * @param sysJob 系统计划任务
     */
    protected void before(JobExecutionContext context, SysJob sysJob)
    {
        threadLocal.set(new Date());
    }

    /**
     * 执行后
     *
     * @param context 工作执行上下文对象
     * @param sysJob 系统计划任务
     */
    protected void after(JobExecutionContext context, SysJob sysJob, Exception e)
    {
        Date startTime = threadLocal.get();
        threadLocal.remove();

        final SysJobLog sysJobLog = new SysJobLog();
        sysJobLog.setJobName(sysJob.getJobName());
        sysJobLog.setJobGroup(sysJob.getJobGroup());
        sysJobLog.setInvokeTarget(sysJob.getInvokeTarget());
        sysJobLog.setStartTime(startTime);
        sysJobLog.setStopTime(new Date());
        long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime();
        sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒");
        if (e != null)
        {
            sysJobLog.setStatus(Constants.FAIL);
            String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000);
            sysJobLog.setExceptionInfo(errorMsg);
        }
        else
        {
            sysJobLog.setStatus(Constants.SUCCESS);
        }

        // 写入数据库当中
        SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog);
    }

    /**
     * 执行方法,由子类重载
     *
     * @param context 工作执行上下文对象
     * @param sysJob 系统计划任务
     * @throws Exception 执行过程中的异常
     */
    protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception;
}

这是一种 模板方法模式(Template Method)

定义任务执行的整体结构:

  • 执行前(记录开始时间)

  • 执行中(交由子类完成)

  • 执行后(记录日志)

子类只需要实现 doExecute() 方法即可。

允许并发和不允许并发的场景理解

假设你配置了一个任务,每 10 秒执行一次,但这个任务某次执行花了 15 秒才结束。那么 Quartz 到第 10 秒的时候,上一次任务还没执行完,此时 Quartz 会:

  • 如果没有加 @DisallowConcurrentExecution → Quartz 会再启动一个线程,执行下一次任务(出现并发执行)。

  • 如果加了 @DisallowConcurrentExecution → Quartz 会等待上一次任务执行完再执行下一次,不并发。

2. 构建 JobDetail 对象
JobDetail jobDetail = JobBuilder.newJob(jobClass)
    .withIdentity(getJobKey(jobId, jobGroup))
    .build();
  • JobDetail 是 Quartz 的任务描述对象

  • withIdentity 给任务设定唯一 ID(JobKey)

  • 绑定任务类,Quartz 到点时会调用它的 execute 方法

3. 构建 Cron 调度规则
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder
    .cronSchedule(job.getCronExpression());
cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);

根据 cron 表达式 构造调度规则

  • handleCronScheduleMisfirePolicy() 设置“错过触发”的补救策略(MISFIRE)

什么是 Misfire(错过触发)?

当 Quartz 到点想执行一个任务时:

  • 如果线程池没空,或者机器睡眠了,或者调度器重启中……

  • Quartz 就会错过这个触发点(misfire)

此时 Quartz 会根据我们设置的 misfire 策略 来决定是否补救、如何补救。

  • IgnoreMisfires() → 一恢复,就快速执行补回这 3 次(立刻补偿)

  • FireAndProceed() → 一恢复,只执行 1 次补偿,然后按正常节奏

  • DoNothing() → 直接等下一分钟,不补

场景模拟:

你设置了一个定时任务:

  • 每分钟执行一次

  • 比如:09:0009:0109:0209:0309:04……

假设:

  • 程序挂了 3 分钟(从 09:01 ~ 09:03

  • 09:04 程序恢复了!

此时 Quartz 发现:哎,我错过了 09:01、09:02、09:03 的任务,应该怎么办?

策略中文含义发生了什么?
IgnoreMisfires()忽略错过,全部补跑恢复时立刻把 09:01、09:02、09:03 的任务全部都补回来一次,快速连续执行三次。然后继续执行 09:04
FireAndProceed()补跑一次,继续执行恢复时只补一次(比如执行 09:03),然后继续从 09:04 正常调度
DoNothing()错过就错过,不补Quartz 什么都不干,直接等下一次任务,也就是从 09:04 开始,前面三次全当没发生过
4. 构建 Trigger(触发器)
CronTrigger trigger = TriggerBuilder.newTrigger()
    .withIdentity(getTriggerKey(jobId, jobGroup))
    .withSchedule(cronScheduleBuilder)
    .build();
  • Trigger 定义任务 何时触发

  • 与 JobDetail 一起注册到 Scheduler

5. 设置运行时参数
jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
  • JobDataMap 是任务执行时的上下文参数

  • SysJob 对象放进去,任务执行时可以获取

 6. 清除已有同名任务(避免重复)
if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
    scheduler.deleteJob(getJobKey(jobId, jobGroup));
}
  • 防止任务已经存在,创建失败

  • 先删除再重新注册

 7. 判断任务是否过期(没有下一次执行时间就不注册)
if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) {
    scheduler.scheduleJob(jobDetail, trigger);
}
  • 有些 cron 表达式可能已经过时(比如定了过去的时间)

  • 只有有下一次执行时间才注册

8. 如果任务状态是“暂停”,注册后立即暂停
if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
    scheduler.pauseJob(getJobKey(jobId, jobGroup));
}
  • 数据库中任务状态为 PAUSE

  • 即使注册了,也不立刻触发执行

Quartz 执行定时任务的完整过程

  1. 任务初始化

    在 Spring 容器初始化完成后,SysJobServiceImpl 中的 @PostConstruct init() 方法会被自动调用。该方法首先清空调度器中已有的任务,然后从数据库中加载所有配置的定时任务(sys_job 表),并通过循环调用 ScheduleUtils.createScheduleJob(...) 方法将它们逐一注册到 Quartz 的调度器中。

  2. 创建与注册任务

    无论是系统启动时加载任务,还是前端新增任务,都会构建一个 SysJob 实体对象,包含任务名称、cron 表达式、调用方法(invokeTarget)等信息。任务通过 ScheduleUtils.createScheduleJob(...) 方法被包装为 Quartz 的 JobDetailCronTrigger,并使用 scheduler.scheduleJob(...) 注册到调度器中。

  3. 等待触发

    任务注册成功后,Quartz 会将其放入内部的 Trigger 队列。调度线程(QuartzSchedulerThread)会持续轮询所有 Trigger,根据任务的 nextFireTime 判断是否该执行任务。

  4. 任务触发与执行

    当某个任务的 nextFireTime <= 当前系统时间 时,Quartz 会从线程池中分配一个线程,实例化注册时绑定的 Job 类(如 QuartzJobQuartzDisallowConcurrentExecution),并调用其 execute(JobExecutionContext context) 方法。

  5. 调用目标方法

   QuartzJob 会在执行中调用 JobInvokeUtil.invokeMethod(SysJob),通过解析 invokeTarget 字符串提取出 Bean 名(或类的全限定名)、方法名与参数信息。如果是 Spring Bean,则通过 SpringUtils.getBean() 获取对象;如果是类名,则使用 Class.forName() 动态加载并实例化。随后调用重载的 invokeMethod() 方法,根据参数类型和值构建 Method 实例并执行。若无参数,调用 method.invoke(bean);若有参数,则调用 method.invoke(bean, params...),最终动态执行配置的方法逻辑。

obInvokeUtil.invokeMethod(SysJob):

    public static void invokeMethod(SysJob sysJob) throws Exception
    {
        String invokeTarget = sysJob.getInvokeTarget();
        String beanName = getBeanName(invokeTarget);
        String methodName = getMethodName(invokeTarget);
        List<Object[]> methodParams = getMethodParams(invokeTarget);

        if (!isValidClassName(beanName))
        {
            Object bean = SpringUtils.getBean(beanName);
            invokeMethod(bean, methodName, methodParams);
        }
        else
        {
            Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
            invokeMethod(bean, methodName, methodParams);
        }
    }

重载的 invokeMethod() 方法 ——根据参数调用有参方法还是无参方法:

    /**
     * 调用任务方法
     *
     * @param bean 目标对象
     * @param methodName 方法名称
     * @param methodParams 方法参数
     */
    private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
            InvocationTargetException
    {
        if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0)
        {
            Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams));
            method.invoke(bean, getMethodParamsValue(methodParams));
        }
        else
        {
            Method method = bean.getClass().getMethod(methodName);
            method.invoke(bean);
        }
    }

7. 记录执行日志

每次任务执行结束后,无论成功与否,系统都会将执行结果记录到 sys_job_log 表中,包括执行时间、状态、异常信息等,供前端“任务日志”模块查询查看。

暂停恢复定时任务:

    /**
     * 任务调度状态修改
     * 
     * @param job 调度信息
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int changeStatus(SysJob job) throws SchedulerException
    {
        int rows = 0;
        String status = job.getStatus();
        if (ScheduleConstants.Status.NORMAL.getValue().equals(status))
        {
            rows = resumeJob(job);
        }
        else if (ScheduleConstants.Status.PAUSE.getValue().equals(status))
        {
            rows = pauseJob(job);
        }
        return rows;
    }

 /**
     * 暂停任务
     * 功能:
     * 暂停一个正在运行的任务,包括:
     * 更新数据库中的任务状态为 "1"(暂停)
     * 命令 Quartz 调度器暂停该任务,不再触发执行
     * @param job 调度信息
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int pauseJob(SysJob job) throws SchedulerException
    {
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
        int rows = jobMapper.updateJob(job);
        if (rows > 0)
        {
            scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        }
        return rows;
    }

    /**
     * 恢复任务
     * 功能:
     * 恢复一个之前被暂停的任务:
     * 更新数据库中 status = 0(启用)
     * Quartz 恢复调度触发    
     * @param job 调度信息
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int resumeJob(SysJob job) throws SchedulerException
    {
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        job.setStatus(ScheduleConstants.Status.NORMAL.getValue());
        int rows = jobMapper.updateJob(job);
        if (rows > 0)
        {
            scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        }
        return rows;
    }

changeStatus() 是定时任务状态切换的总入口,内部通过调用 pauseJob()resumeJob() 来完成数据库状态更新 + 调度器实际控制的双重操作,确保任务状态从“页面 → 数据库 → 调度器”三方始终一致。

总结

在企业级项目中,Quartz 常与数据库 + SpringBoot + 可视化后台(如若依)结合:

  • 任务信息存在数据库中(如 sys_job 表)

  • 系统启动时从表中加载任务注册到调度器

  • 管理页面可:

    • 动态新增任务(配置 Bean 方法、参数)

    • 启用/暂停任务

    • 查看任务执行日志

🔧 关键代码点:

  • 使用 Scheduler.scheduleJob() 注册任务

  • 使用 pauseJob() / resumeJob() 控制运行状态

  • 使用 JobInvokeUtil + 反射 调用目标方法

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

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

相关文章

STM32标准库-DMA直接存储器存取

文章目录 一、DMA1.1简介1.2存储器映像1.3DMA框图1.4DMA基本结构1.5DMA请求1.6数据宽度与对齐1.7数据转运DMA1.8ADC扫描模式DMA 二、数据转运DMA2.1接线图2.2代码2.3相关API 一、DMA 1.1简介 DMA&#xff08;Direct Memory Access&#xff09;直接存储器存取 DMA可以提供外设…

376. Wiggle Subsequence

376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》

在注意力分散、内容高度同质化的时代&#xff0c;情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现&#xff0c;消费者对内容的“有感”程度&#xff0c;正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中&#xff0…

学校招生小程序源码介绍

基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码&#xff0c;专为学校招生场景量身打造&#xff0c;功能实用且操作便捷。 从技术架构来看&#xff0c;ThinkPHP提供稳定可靠的后台服务&#xff0c;FastAdmin加速开发流程&#xff0c;UniApp则保障小程序在多端有良好的兼…

家政维修平台实战20:权限设计

目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系&#xff0c;主要是分成几个表&#xff0c;用户表我们是记录用户的基础信息&#xff0c;包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题&#xff0c;不同的角色&#xf…

最新SpringBoot+SpringCloud+Nacos微服务框架分享

文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的&#xff0c;根据Excel列的需求预估的工时直接打骨折&#xff0c;不要问我为什么&#xff0c;主要…

基于当前项目通过npm包形式暴露公共组件

1.package.sjon文件配置 其中xh-flowable就是暴露出去的npm包名 2.创建tpyes文件夹&#xff0c;并新增内容 3.创建package文件夹

MMaDA: Multimodal Large Diffusion Language Models

CODE &#xff1a; https://github.com/Gen-Verse/MMaDA Abstract 我们介绍了一种新型的多模态扩散基础模型MMaDA&#xff0c;它被设计用于在文本推理、多模态理解和文本到图像生成等不同领域实现卓越的性能。该方法的特点是三个关键创新:(i) MMaDA采用统一的扩散架构&#xf…

智能在线客服平台:数字化时代企业连接用户的 AI 中枢

随着互联网技术的飞速发展&#xff0c;消费者期望能够随时随地与企业进行交流。在线客服平台作为连接企业与客户的重要桥梁&#xff0c;不仅优化了客户体验&#xff0c;还提升了企业的服务效率和市场竞争力。本文将探讨在线客服平台的重要性、技术进展、实际应用&#xff0c;并…

【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例

文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…

转转集团旗下首家二手多品类循环仓店“超级转转”开业

6月9日&#xff0c;国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解&#xff0c;“超级…

【快手拥抱开源】通过快手团队开源的 KwaiCoder-AutoThink-preview 解锁大语言模型的潜力

引言&#xff1a; 在人工智能快速发展的浪潮中&#xff0c;快手Kwaipilot团队推出的 KwaiCoder-AutoThink-preview 具有里程碑意义——这是首个公开的AutoThink大语言模型&#xff08;LLM&#xff09;。该模型代表着该领域的重大突破&#xff0c;通过独特方式融合思考与非思考…

什么是库存周转?如何用进销存系统提高库存周转率?

你可能听说过这样一句话&#xff1a; “利润不是赚出来的&#xff0c;是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业&#xff0c;很多企业看着销售不错&#xff0c;账上却没钱、利润也不见了&#xff0c;一翻库存才发现&#xff1a; 一堆卖不动的旧货…

el-switch文字内置

el-switch文字内置 效果 vue <div style"color:#ffffff;font-size:14px;float:left;margin-bottom:5px;margin-right:5px;">自动加载</div> <el-switch v-model"value" active-color"#3E99FB" inactive-color"#DCDFE6"…

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …

linux arm系统烧录

1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 &#xff08;忘了有没有这步了 估计有&#xff09; 刷机程序 和 镜像 就不提供了。要刷的时…

Python实现prophet 理论及参数优化

文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候&#xff0c;写过一篇简单实现&#xff0c;后期随着对该模型的深入研究&#xff0c;本次记录涉及到prophet 的公式以及参数调优&#xff0c;从公式可以更直观…

微信小程序 - 手机震动

一、界面 <button type"primary" bindtap"shortVibrate">短震动</button> <button type"primary" bindtap"longVibrate">长震动</button> 二、js逻辑代码 注&#xff1a;文档 https://developers.weixin.qq…

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…