若依框架中定时任务Quartz工作流程

官方文档:https://www.quartz-scheduler.org/documentation/

一、Quartz 简介

        Quartz 是一个功能强大且灵活的任务调度框架,由 Java 编写,用于在应用程序中调度任务(如定时任务)。它支持简单和复杂的调度需求,广泛应用于各种 Java 项目中,包括 Web 应用和企业应用。


二、Quartz 的核心概念

  1. Scheduler(调度器)

    • Quartz 的核心组件,用于管理和执行任务。它控制任务的添加、启动、暂停和删除。
    • 关键方法
      • start():启动调度器。
      • shutdown():关闭调度器。
      • scheduleJob():调度一个任务。
      • pauseJob() / resumeJob():暂停或恢复任务。
  2. Job(任务)

    • 一个实现了 Job 接口的类,封装了任务的具体逻辑。
    • Job接口
      public interface Job {
          void execute(JobExecutionContext context) throws JobExecutionException;
      }
      
    • 关键点
      • 每个任务的执行逻辑写在 execute() 方法中。
      • 任务执行时,Quartz 会创建一个新的 Job 实例。
  3. JobDetail(任务详情)

    • 描述一个任务的具体信息,包括任务的名称、分组、执行类等。
    • 创建方式
      JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                                      .withIdentity("myJob", "group1")
                                      .build();
      
  4. Trigger(触发器)

    • 定义任务的调度规则(如执行时间、频率等)。
    • 两种主要触发器
      • SimpleTrigger:简单触发器,基于固定时间间隔。
      • CronTrigger:复杂触发器,使用 Cron 表达式定义调度规则。
    • 创建方式
      Trigger trigger = TriggerBuilder.newTrigger()
                                      .withIdentity("trigger1", "group1")
                                      .startNow()
                                      .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))
                                      .build();
      
  5. JobStore(任务存储)

    • Quartz 用于存储任务和调度信息的机制。支持两种存储方式:
      • RAMJobStore:将任务存储在内存中(默认)。
      • JDBCJobStore:将任务存储在数据库中,适合需要持久化任务的场景。

三、Quartz在若依框架中的工作流程

1. 管理台添加自定义定时任务

        在定时任务部分新增两个自定义方法,一个是无参的,一个是有参的。

2. 添加对应Task类        

        管理平台创建的任务是需要和后端的task类对应的,所以需要添加ZyTask任务类,在具体的任务类中,可以写定时任务具体的内容。代码如下:

package com.ruoyi.quartz.task;
import org.springframework.stereotype.Component;

@Component("zyTask")
public class ZyTask {
    public void noParamTest() {
        System.out.println("执行无参test方法");
    }

    public void paramTest(String param) {
        System.out.println("执行有参test方法,参数为:"+ param);
    }
}
3. 任务持久化到数据库

        在管理台创建任务后,任务会被保存在sys_job表中。

invoke_target:任务调用的目标(Spring Bean 名称和方法)。

misfire_policy:错过执行时的处理策略。

concurrent:是否允许并发,1不允许,0允许。

4. 加载数据库任务到Quartz调度器

        加载sys_job中的任务到Quartz调度器中,具体代码在SysJobServiceImpl中的 init() 方法中。 

    @PostConstruct
    public void init() throws SchedulerException, TaskException
    {
        //清除调度器中的所有任务,确保不会出现重复任务。
        scheduler.clear();
        List<SysJob> jobList = jobMapper.selectJobAll();
        //将每个任务添加到调度器
        for (SysJob job : jobList)
        {
            ScheduleUtils.createScheduleJob(scheduler, job);
        }
    }

 @PostConstruct 是 Java 中的一种注解,用于标注在一个方法上,表示在 依赖注入完成后(即对象创建并完成依赖注入后)自动调用该方法进行初始化操作。

        具体的创建逻辑在ScheduleUtils中的 createScheduleJob(Scheduler scheduler, SysJob job) 方法中。代码如下:

    /**
     * 创建定时任务
     */
    public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException
    {
        //1.确定任务类型,根据job对象动态获取对应的Job类 根据concurrent属性获取
        // 若依中按照是否允许并发定义了两类Job,分别是 QuartzJobExecution 和 QuartzDisallowConcurrentExecution
        Class<? extends Job> jobClass = getQuartzJobClass(job);

        //2. 构建job信息 获取jobId 和 group
        Long jobId = job.getJobId();
        String jobGroup = job.getJobGroup();
        
        //3. 创建JobDetail对象
        //.newJob(jobClass) 是 JobBuilder 的一个静态方法,用来指定该 JobDetail 对应的具体 Job 类。
        //.withIdentity() 方法用于为任务设置一个唯一标识符。任务的身份通常由 jobId 和 jobGroup 来唯一确定。
        //.build() 方法是 JobBuilder 的最后一步,它构建并返回一个 JobDetail 对象。
        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
        
        
        //4. 表达式调度构建器
        //cronSchedule(String cronExpression) 是一个静态方法,它接受一个 Cron 表达式(字符串)并返回一个 CronScheduleBuilder 对象。
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
        
        //5. 处理错过触发策略
        //Quartz 的错过触发(Misfire)指的是当一个任务错过了预定的触发时间(可能因为系统停机、任务阻塞等原因),Quartz 需要决定如何处理这种情况。
        //handleCronScheduleMisfirePolicy方法根据创建任务时的错过触发策略来执行
        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);

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

        //7. 添加参数,运行时的方法可以获取
        //将任务对象 job 放入 Quartz 的 JobDataMap 中,为任务执行时提供参数支持。
        jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);

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

        //9. 判断任务是否过期
        if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression())))
        {
            //10. 执行调度任务 将一个任务(jobDetail)和触发器(trigger)绑定到 Quartz 的调度器(scheduler)中,并启动任务调度。
            scheduler.scheduleJob(jobDetail, trigger);
        }

        // 暂停任务
        if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue()))
        {
            scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
        }
    }

        JobDetail 是 Quartz 中表示一个具体任务(Job)的详细信息的对象,包含任务的类、任务的标识等。

        通过上面代码,最终将jobDetail和trigger绑定到了调度器中。

5. 按定时规则启动任务
5.1 Quartz 调度器触发任务
  • Quartz 调度器在运行时,根据任务注册时定义的触发器(CronTrigger)监控当前时间。
  • 当触发器条件与当前时间匹配时,Quartz 调用任务的执行逻辑。也就是说,会执行Job对应任务类的execute(JobExecutionContext context)方法

传入 JobExecutionContext 对象,该对象包含任务运行时的上下文信息,包括:

  • 任务的元信息(如任务名称、分组等)。
  • 数据参数(通过 JobDataMap 传递)。
  • 当前触发器的状态。
5.2 动态调用任务逻辑
  • Quartz 根据Job对应的任务类QuartzJobExecution或者QuartzDisallowConcurrentExecution。去动态调用的任务执行类。我们创建的任务是允许并发执行的,因此调用的任务类是QuartzJobExecution,然后执行该类中的execute()方法。
  • QuartzJobExecutionQuartzDisallowConcurrentExecution这两个类是若依中定义的,他们继承自AbstractQuartzJob抽象类,AbstractQuartzJob类实现了Job接口。

在它们的父类AbstractQuartzJob类中重写了execute()方法,定义任务执行的通用逻辑。

    //定义任务执行的通用逻辑  Quartz 定时任务的入口方法,负责执行任务。
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException
    {
        SysJob sysJob = new SysJob();
        //从 JobExecutionContext 的 MergedJobDataMap 中提取任务参数,并将其转换为 SysJob 对象。
        //从 Quartz 提供的 JobExecutionContext 中获取任务执行时所携带的参数数据
        Object o = context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES);
        //Bean属性复制工具方法。
        BeanUtils.copyBeanProp(sysJob, o);
        try
        {
            //执行前:调用 before 方法,记录任务开始时间。
            before(context, sysJob);
            if (sysJob != null)
            {
                //实际执行:调用抽象方法 doExecute,由子类实现具体的任务逻辑。
                doExecute(context, sysJob);
            }
            //执行后:调用 after 方法,记录任务执行的状态、耗时、并保存日志。
            after(context, sysJob, null);
        }
        catch (Exception e)
        {
            log.error("任务执行异常  - :", e);
            after(context, sysJob, e);
        }
    }
  • QuartzJobExecution 在执行时,会先执行execute()方法,在execute()方法中,又会去执行doexecute()方法。doexecute()中会通过 JobInvokeUtil 动态调用用户配置的目标方法。
//该类没有 @DisallowConcurrentExecution 注解,表示默认允许并发执行。
//如果任务被频繁触发,Quartz 会同时运行多个任务实例,彼此之间独立运行。
public class QuartzJobExecution extends AbstractQuartzJob
{
    @Override
    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception
    {
        //即通过 JobInvokeUtil.invokeMethod(sysJob) 调用任务的具体方法。
        JobInvokeUtil.invokeMethod(sysJob);
    }
}
5.3 JobInvokeUtil 动态调用机制

  JobInvokeUtil 是任务动态调用的核心,它通过反射机制解析用户配置的 invokeTarget 并执行目标方法。invokeMethod()方法代码如下:

//动态调用任务目标方法,根据任务的 invokeTarget 执行相应逻辑。
    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);
        //判断是bean还是普通类
        //如果 beanName 不是一个合法的类名,认为它是 Spring 容器中的 Bean。
        if (!isValidClassName(beanName))
        {
            //调用 SpringUtils.getBean(beanName) 从 Spring 容器中动态获取 Bean。
            Object bean = SpringUtils.getBean(beanName);
            //动态调用目标方法,核心逻辑在 invokeMethod(重载方法)中完成。
            invokeMethod(bean, methodName, methodParams);
        }
        else
        // 如果是合法的类名,则通过反射直接加载。
        {
            //通过反射机制加载目标类,并实例化对象。
            //这里传入的 beanName 是一个完整的类名
            Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
            //动态调用目标方法,核心逻辑在 invokeMethod(重载方法)中完成。
            invokeMethod(bean, methodName, methodParams);
        }
    }

        通过 invokeMethod(Object bean, String methodName, List<Object[]> methodParams)动态调用目标方法,这个方法是 Java 反射机制 的一个典型使用,它动态调用指定对象上的方法,并可以支持传递参数。

参数

  1. Object bean:

    • 被调用方法所属的对象实例。
    • 如果调用静态方法,这个对象可以是 null
  2. String methodName:

    • 需要调用的方法的名字。
  3. List<Object[]> methodParams:

    • 方法的参数列表。
    • 每个元素是一个数组,数组的第一个值是参数类型,第二个值是参数值。

方法内容如下:

    //执行目标对象 bean 上的方法。
    private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
            InvocationTargetException

    {
        //判断是否有参数
        if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0)
        {
            //通过 getMethod 动态获取方法对象。参数类型:通过 getMethodParamsType(methodParams) 动态生成。
            Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams));
            //调用 invoke 执行该方法:
            //参数值:通过 getMethodParamsValue(methodParams) 动态生成。
            method.invoke(bean, getMethodParamsValue(methodParams));
        }
        else
        {
            Method method = bean.getClass().getMethod(methodName);
            method.invoke(bean);
        }
    }

   invoke 方法是 Java 反射机制 提供的功能,用于在运行时调用指定对象的某个方法。这样最终就执行了ZyTask类中的有参和无参的test方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小印z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值