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

分析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 + 反射 调用目标方法

相关推荐
板板正8 分钟前
Spring Boot 整合MongoDB
spring boot·后端·mongodb
一心09243 分钟前
tomcat 定时重启
运维·tomcat·定时任务
泉城老铁1 小时前
在高并发场景下,如何优化线程池参数配置
spring boot·后端·架构
泉城老铁1 小时前
Spring Boot中实现多线程6种方式,提高架构性能
spring boot·后端·spring cloud
hrrrrb2 小时前
【Java Web 快速入门】九、事务管理
java·spring boot·后端
布朗克1684 小时前
Spring Boot项目通过RestTemplate调用三方接口详细教程
java·spring boot·后端·resttemplate
IT毕设实战小研5 小时前
基于Spring Boot校园二手交易平台系统设计与实现 二手交易系统 交易平台小程序
java·数据库·vue.js·spring boot·后端·小程序·课程设计
孤狼程序员5 小时前
【Spring Cloud 微服务】1.Hystrix断路器
java·spring boot·spring·微服务
RainbowSea5 小时前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 04
java·spring boot·后端
RainbowSea6 小时前
伙伴匹配系统(移动端 H5 网站(APP 风格)基于Spring Boot 后端 + Vue3 - 03
java·spring boot·后端