当前位置: 首页 > news >正文

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

分析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表达式构建一个新的triggerCronTrigger 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<>();@Overridepublic 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 + 反射 调用目标方法

相关文章:

  • 376. Wiggle Subsequence
  • Windows cmd中文乱码解决方法(Windows控制台中文乱码、CMD乱码、控制台乱码、Command Prompt命令提示符cmd.exe乱码)
  • docker 安装运行mysql8.4.4
  • 多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
  • Loss Margin的原理与推导
  • 一天时间解决期末不挂科
  • 代码解读——ReferenceNet
  • 【位运算】消失的两个数字(hard)
  • STM32 PID控制
  • Hyperlane 框架详解与使用指南
  • shell打印图案
  • 常用的OceanBase调优配置参数
  • Maven 多仓库配置及缓存清理实战分享
  • 【Redis/1-前置知识】分布式系统概论:架构、数据库与微服务
  • vue的created和mounted区别
  • word嵌入图片显示不全-error记
  • Linux下制作Nginx绿色免安装包
  • 介绍一种直流过压保护电路
  • 中和农信创新引领“三农“金融服务新模式
  • vue实现气泡词云图
  • 外贸网站建设流程/如何建立自己的网站?
  • .com免费网站怎么做/手机金融界网站
  • 气动科技东莞网站建设/网络营销师
  • seo网站做推广价格/百度seo排名主要看啥
  • 网络营销方式的对比分析论文/游戏优化大师手机版
  • 做网站一般用什么语言/100种宣传方式