一、前置知识 Quartz

1.1核心定位

Quartz 是 Java 生态中成熟、开源的定时任务调度框架(简单说就是专门帮你实现 “在指定时间做某件事”“每隔一段时间重复做某件事” 的工具),几乎是 Java 定时任务的标配方案。

1.2. 核心价值

  • 支持复杂的定时规则(不仅是固定间隔,还支持类似 Cron 表达式的 “每月 1 号凌晨 3 点执行” 等灵活配置)
  • 支持任务的持久化(任务信息可存到数据库,避免应用重启后定时任务丢失)
  • 支持任务的暂停、恢复、删除、动态添加等精细化管控
  • 支持集群部署(避免多实例部署时任务重复执行)

1.3  三个核心概念

1.3.1 Job(任务)

  • 定义:你要执行的具体业务逻辑载体,比如发送邮件、数据备份、清理日志。
  • 关键类与接口
    • org.quartz.Job:所有任务类必须实现的接口,只需重写 execute(JobExecutionContext context) 方法,业务逻辑就写在这里。
    • org.quartz.JobExecutionContext:任务执行时的上下文对象,可获取 JobDetailTriggerScheduler 等信息,还能通过 getMergedJobDataMap() 读取任务参数。
    • org.quartz.JobDetail:任务的元数据描述,包含任务名称、分组、持久化配置等,由 JobBuilder 创建。
    • org.quartz.JobBuilder:构建 JobDetail 的工具类,通过链式调用设置任务标识、分组、描述等。
    • org.quartz.JobDataMap:任务参数的存储容器,可在创建 JobDetail 时存入参数,任务执行时通过 JobExecutionContext 取出。

1.3.2  Trigger(触发器)

  • 定义:定义任务的执行时间规则,决定 Job 什么时候执行、执行多少次。
  • 关键类与接口
    • org.quartz.Trigger:所有触发器的顶级接口,核心实现是 CronTrigger 和 SimpleTrigger
    • org.quartz.CronTrigger:基于 Cron 表达式的触发器,支持复杂时间规则,是最常用的类型。
    • org.quartz.SimpleTrigger:支持固定间隔、固定延迟的简单触发器,适合周期性重复的场景。
    • org.quartz.TriggerBuilder:构建 Trigger 的工具类,用于设置触发器标识、分组、调度规则。
    • org.quartz.CronScheduleBuilder:构建 Cron 调度规则的工具类,通过 cronSchedule(String cronExpression) 生成 Cron 表达式对应的调度规则。
    • org.quartz.SimpleScheduleBuilder:构建简单调度规则的工具类,支持 withIntervalInSeconds(int interval) 等方法。
    • org.quartz.impl.matchers.GroupMatcher:用于按分组匹配任务或触发器,常用于批量操作。

1.3.3  Scheduler(调度器)

  • 定义:Quartz 的核心大脑,负责将 Job 和 Trigger 绑定,统一管理和调度所有任务。
  • 关键类与接口
    • org.quartz.Scheduler:调度器的核心接口,提供任务注册、启动、暂停、删除等所有调度操作。
    • org.quartz.SchedulerFactory:用于创建 Scheduler 实例,常用实现是 StdSchedulerFactory
    • org.quartz.impl.StdSchedulerFactory:默认的调度器工厂,通过读取配置文件(quartz.properties)初始化调度器。
    • org.quartz.JobKey / org.quartz.TriggerKey:任务和触发器的唯一标识,由名称(name)和分组(group)组成,用于精准定位任务或触发器。

1.4 简单工作流程

  • 初始化调度器:通过 SchedulerFactory 创建 Scheduler 实例,并启动。
  • 构建 JobDetail:用 JobBuilder 创建 JobDetail,指定任务实现类和唯一标识。
  • 构建 Trigger:用 TriggerBuilder + CronScheduleBuilder/SimpleScheduleBuilder 创建触发器,设置时间规则和唯一标识。
  • 绑定并调度:调用 scheduler.scheduleJob(jobDetail, trigger) 将任务与触发器绑定到调度器。
  • 触发执行:当触发器的时间规则满足时,调度器会触发 Job 的 execute() 方法执行业务逻辑。

1.5 和Spring Task对比

对比维度 Quartz(独立定时任务框架) Spring Task(Spring 内置定时功能)
核心定位 成熟、功能全面的独立开源定时任务调度框架 Spring 生态内置的轻量级定时工具
学习成本 较高(有专属概念、配置复杂,支持集群 / 持久化) 极低(注解式开发,配置简单,适合初学者)
功能丰富度 非常全面(复杂 Cron、任务暂停 / 恢复 / 动态添加、失败重试、集群、持久化) 基础够用(简单 Cron、固定间隔,无原生集群 / 持久化 / 动态任务功能)
依赖情况 需额外引入 Quartz 相关依赖(与 Spring 整合需额外配置) 无额外依赖(Spring Core 内置,引入 Spring 即可使用)
与 Spring 生态整合度 支持整合,但需手动配置(或使用 Spring Boot 起步依赖简化) 无缝整合(Spring/Spring Boot 原生支持,注解一键启用)
持久化支持 支持(可将任务信息存入数据库,应用重启任务不丢失) 不支持(任务仅存于内存,应用重启任务失效)
集群部署支持 支持(原生解决集群下任务重复执行问题) 不支持(原生无集群能力,需借助外部工具实现)
适用场景 复杂定时需求、生产环境核心任务、集群部署、需要任务精细化管控 简单定时需求、非核心任务、单机部署、快速开发验证
性能表现 性能稳定,针对复杂任务做了优化,开销略高 轻量高效,开销极小,适合简单任务

二、若依定时任务初始化

定位到quartz模块

2.1 初始化总流程

打开SysJobServiceImpl

    /**
     * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据)
     */
    @PostConstruct
    public void init() throws SchedulerException, TaskException
    {
        scheduler.clear();
        List<SysJob> jobList = jobMapper.selectJobAll();
        for (SysJob job : jobList)
        {
            ScheduleUtils.createScheduleJob(scheduler, job);
        }
    }
  • @PostConstruct 注解,作用是在当前类的依赖注入完成后、项目启动阶段自动执行被标记的方法。

  • 清空旧任务首先调用 scheduler.clear() 清除调度器中已存在的所有任务,避免项目重启后出现任务重复或残留的问题。

  • 加载数据库任务通过 jobMapper.selectJobAll() 从数据库中查询所有已配置的定时任务(SysJob)列表,确保任务配置与数据库保持一致。

  • 重建所有任务遍历查询到的任务列表,调用工具类 ScheduleUtils.createScheduleJob() 将每个任务重新注册到 Quartz 调度器中,完成任务的初始化。

三、任务创建

3.1 总流程

打开ScheduleUtils

    /**
     * 创建定时任务
     */
    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));
        }
    }
  • 获取任务实现类通过 getQuartzJobClass() 获取与当前任务对应的 Job 实现类,这是具体业务逻辑的载体。

  • 构建 JobDetail用 JobBuilder 创建 JobDetail 对象,并为其设置唯一标识(JobKey),这是任务的元数据载体。

  • 构建 Cron 触发器用 CronScheduleBuilder 基于任务的 Cron 表达式创建调度规则,并通过 handleCronScheduleMisfirePolicy() 配置任务错过触发后的处理策略。再用 TriggerBuilder 生成 CronTrigger,并设置唯一标识(TriggerKey)。

  • 传递任务参数将 SysJob 对象存入 JobDetail 的 JobDataMap,让任务执行时可以直接获取配置信息。

  • 避免任务重复检查调度器中是否已存在该任务,如果存在则先删除旧任务,避免重复注册。

  • 校验并注册任务通过 CronUtils 校验 Cron 表达式的有效性,确认有合法的下次执行时间后,才将 JobDetail 和 Trigger 绑定到调度器。

  • 处理暂停状态如果任务状态为 “暂停”,则在注册后立即暂停该任务,确保任务状态与数据库配置一致。

 3.2 Job的实现类级获取

/**
 * 抽象quartz调用
 *
 * @author ruoyi
 */
public abstract class AbstractQuartzJob implements Job

而这个类有两个子类,因为两个类都需要记录日志,有一些公共的方法,所以都继承AbstractQuartzJob

/**
 * 定时任务处理(禁止并发执行)
 * 
 * @author ruoyi
 *
 */
@DisallowConcurrentExecution
public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob
/**
 * 定时任务处理(允许并发执行)
 * 
 * @author ruoyi
 *
 */
public class QuartzJobExecution extends AbstractQuartzJob

我们在ScheduleUtils中通过getQuartzJobClass()获取对应的Job实现类

    /**
     * 得到quartz任务类
     *
     * @param sysJob 执行计划
     * @return 具体执行任务类
     */
    private static Class<? extends Job> getQuartzJobClass(SysJob sysJob)
    {
        boolean isConcurrent = "0".equals(sysJob.getConcurrent());
        return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
    }

3.3 定时任务的策略

这里指的是如果服务器发生了一些故障,没执行的任务在恢复后该进行什么操作

    /** 默认 不执行*/
    public static final String MISFIRE_DEFAULT = "0";

    /** 立即触发执行 */
    public static final String MISFIRE_IGNORE_MISFIRES = "1";

    /** 触发一次执行 */
    public static final String MISFIRE_FIRE_AND_PROCEED = "2";
    /**
     * 设置定时任务策略
     */
    public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb)
            throws TaskException
    {
        switch (job.getMisfirePolicy())
        {
            case ScheduleConstants.MISFIRE_DEFAULT:
                return cb;
            case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
                return cb.withMisfireHandlingInstructionIgnoreMisfires();
            case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
                return cb.withMisfireHandlingInstructionFireAndProceed();
            case ScheduleConstants.MISFIRE_DO_NOTHING:
                return cb.withMisfireHandlingInstructionDoNothing();
            default:
                throw new TaskException("The task misfire policy '" + job.getMisfirePolicy()
                        + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR);
        }
    }

四、任务的执行

任务执行会调用job实现的execute方法

打开AbstractQuartzJob

    @Override
    public void execute(JobExecutionContext context)
    {
        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);
        }
    }
  • 从任务上下文取出预设参数,封装成业务任务对象 sysJob
  • 按「前置处理 → 核心业务执行 → 后置收尾」的流程运行任务;
  • 捕获执行异常,保证异常场景下收尾工作也能正常执行,同时记录错误信息。

4.1 取出JobDataMap中的job实例

  • JobDataMap:Quartz 定时任务框架提供的任务专属数据容器,用于在任务调度者(Scheduler)、任务实例(JobDetail)、任务执行器(Job)之间传递任务相关的业务数据。
  • 我们在ScheduleUtils中存入
    // 放入参数,运行时的方法可以获取
    jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
  • 现在在execute中取出
BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES));

4.2 JobDataMap与threadlocal

注意 JobDataMap并不是threadlocal

对比维度 ThreadLocal(Java 原生) JobDataMap(Quartz 框架)
所属生态 Java SE/EE 核心 API,无第三方依赖 Quartz 定时任务框架专属,依赖 Quartz 包
核心用途 为单个线程维护专属变量副本,实现线程内数据共享、线程间数据隔离 存储与 Quartz 任务相关的配置 / 业务数据,实现任务上下游(调度者→任务)数据传递
数据作用域 绑定单个线程,线程生命周期内有效(线程销毁则数据回收) 绑定单个 Quartz 任务(JobDetail/Trigger),任务生命周期内有效(任务未被移除则数据可复用)
线程安全特性 本身是线程安全的(仅提供数据隔离能力),但存储的对象本身若为可变对象,仍可能存在线程安全问题(如共享对象引用) 本身是线程安全的(内部基于 HashMap 封装,加了同步锁),支持任务执行时的数据安全存取
数据存储形式 键为 ThreadLocal 实例本身,值为任意 Object 类型(Java 8+ 支持泛型约束) 键为 String 类型,值为可序列化的 Object 类型(推荐实现 Serializable,支持任务持久化)
生命周期管理 手动管理为主:需调用 remove() 清理数据,否则线程池复用线程时可能导致内存泄漏;线程销毁时,对应的 ThreadLocalMap 会被回收 自动管理为主:跟随 JobDetail/Trigger 生命周期,Quartz 框架负责创建、存储和回收;也可手动调用 clear()/remove(String key) 清理
数据传递能力 仅支持同一线程内的方法间传递数据(无法跨线程、跨任务) 支持 Quartz 框架内任务相关组件间传递(Scheduler→JobDetail→Job→Trigger),可跨线程(任务被不同线程执行时仍能获取数据)
序列化支持 不支持序列化(仅存在于当前 JVM 内存的线程中),无法用于跨进程、持久化场景 支持序列化(内部封装了序列化逻辑),可配合 Quartz 的任务持久化(如存储到数据库),实现任务重启后数据不丢失
典型使用场景 1. 存储线程专属的用户会话(如 Web 中的用户登录信息)2. 避免多线程共享 SimpleDateFormat 等非线程安全对象3. 跨方法传递上下文数据(无需方法参数透传) 1. 向 Quartz 任务传递执行参数(如任务执行的目标 ID、配置参数)2. 存储任务执行过程中的临时结果(供后续任务调度使用)3. 任务持久化时,存储需要持久化的任务配置信息
核心优缺点 优点:轻量、无依赖、线程内数据传递高效缺点:易引发内存泄漏(忘记 remove)、无法跨线程、不支持序列化 优点:线程安全、支持序列化 / 持久化、适配 Quartz 任务生态、数据传递场景明确缺点:依赖 Quartz 框架、仅适用于任务调度场景、相对重量级

4.3 任务执行前后

任务前

    /**
     * 执行前
     *
     * @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);
    }
  • 从 threadLocal 中取出任务的开始时间,然后立刻清理掉这个 ThreadLocal 中的数据,避免内存泄漏。

  • 构建任务日志对象,计算任务的运行耗时,写入日志的消息字段。

  • 根据执行结果设置日志状态

  • 最后通过 SpringUtils 获取日志服务的实例,把这条日志记录写入数据库。

4.4 任务的执行

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

此方法是抽象方法,由两个子类实现

    @Override
    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception
    {
        JobInvokeUtil.invokeMethod(sysJob);
    }

两个类重载的方法体都一样,只不过QuartzDisallowConcurrentExecution类上加了@DisallowConcurrentExecution

打开JobInvokeUtil

    /**
     * 执行方法
     *
     * @param 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);
        }
    }

    /**
     * 调用任务方法
     *
     * @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);
        }
    }

这里主要是通过反射来调用对应方法。

五、图解

六、后续相关内容

定时任务接口分析

若依框架 Quartz :定时任务接口源码全拆解-CSDN博客

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐