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

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

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

createScheduleJob

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

java 复制代码
/**
     * 创建定时任务
     */
    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 实现类
java 复制代码
Class<? extends Job> jobClass = getQuartzJobClass(job);

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

  • QuartzJob.class

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

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

QuartzJob和QuartzDisallowConcurrentExecution的父类:

java 复制代码
/**
 * 抽象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 对象
java 复制代码
JobDetail jobDetail = JobBuilder.newJob(jobClass)
    .withIdentity(getJobKey(jobId, jobGroup))
    .build();
  • JobDetail 是 Quartz 的任务描述对象

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

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

3. 构建 Cron 调度规则
java 复制代码
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(触发器)
java 复制代码
CronTrigger trigger = TriggerBuilder.newTrigger()
    .withIdentity(getTriggerKey(jobId, jobGroup))
    .withSchedule(cronScheduleBuilder)
    .build();
  • Trigger 定义任务 何时触发

  • 与 JobDetail 一起注册到 Scheduler

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

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

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

  • 先删除再重新注册

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

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

8. 如果任务状态是"暂停",注册后立即暂停
java 复制代码
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):

java 复制代码
    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() 方法 ------根据参数调用有参方法还是无参方法:

java 复制代码
    /**
     * 调用任务方法
     *
     * @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 表中,包括执行时间、状态、异常信息等,供前端"任务日志"模块查询查看。

暂停恢复定时任务:

java 复制代码
    /**
     * 任务调度状态修改
     * 
     * @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 + 反射 调用目标方法

相关推荐
十年老菜鸟11 分钟前
spring boot源码和lib分开打包
spring boot·后端·maven
白宇横流学长44 分钟前
基于SpringBoot实现的课程答疑系统设计与实现【源码+文档】
java·spring boot·后端
米奇找不到妙妙屋3 小时前
部分请求报 CROS ERROR
spring boot·vue
保持学习ing3 小时前
SpringBoot 前后台交互 -- CRUD
java·spring boot·后端·ssm·项目实战·页面放行
曲莫终4 小时前
SpEl表达式之强大的集合选择(Collection Selection)和集合投影(Collection Projection)
java·spring boot·spring
kaikaile19955 小时前
解密Spring Boot:深入理解条件装配与条件注解
java·spring boot·spring
广州山泉婚姻5 小时前
解锁高效开发:Spring Boot 3和MyBatis-Flex在智慧零工平台后端的应用实战
人工智能·spring boot·spring
bing_1585 小时前
Spring Boot 中ConditionalOnClass、ConditionalOnMissingBean 注解详解
java·spring boot·后端
小扳7 小时前
Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:智驿AI系统(前后端源码 + 数据库 sql 脚本)
java·数据库·人工智能·spring boot·transformer·课程设计
2401_826097628 小时前
JavaEE-SpringBoot
java·spring boot·java-ee