Quartz explains and uses CommandLineRunner to initialize scheduled tasks at project startup

Posted by ammupon on Thu, 16 Dec 2021 20:19:21 +0100

A previous article record related to scheduled tasks:
springboot uses @ Scheduled as a Scheduled task. Detailed usage

Introduction to Quartz

Quartz is a full-featured open source job scheduling library written in Java, which can be integrated into almost any Java application, ranging from independent applications to large-scale e-commerce systems. Quartz can be used to create simple or complex plans to execute dozens, hundreds or even tens of thousands of jobs; The task of a job is defined as a standard Java component that can perform almost any task you can program. Moreover, Quartz Scheduler contains many enterprise level functions, such as supporting JTA transactions and clustering.

Learn about several class concepts involved in Quartz:

  • SchedulerFactory: scheduler factory. This is an interface for scheduler creation and management. The default implementation in Quartz is used in the example.

  • Scheduler: task scheduler. It represents a Quartz independent running container, in which multiple triggers and jobdetails are registered. They use their respective groups and names as the unique basis for locating an object in the container, so the groups and names must be unique (the groups and names of triggers and task instances can be the same because the object types are different).

  • Job: it is an interface with only one method void execute (JobExecutionContext). Developers implement this interface to define running tasks. JobExecutionContext class provides various information of scheduling context. The job runtime information is saved in the JobDataMap instance.

  • JobDetail: Job instance. Quartz recreates a Job instance every time it executes a Job, so it does not directly accept an instance of a Job. Instead, it receives a Job implementation class so that the runtime can instantiate a Job through the reflection mechanism of newInstance(). Therefore, it is necessary to use a class to describe the Job implementation class and other related static information, such as Job name, description, associated listener and so on. JobDetail assumes this role.

  • Trigger

    : trigger, which describes the time trigger rules that trigger Job execution.

    • SimpleTrigger: SimpleTrigger is the most suitable choice when it needs to be triggered only once or executed periodically at fixed time intervals.
    • CronTrigger: define scheduling schemes of various complex time rules through Cron expression: such as 9:00 every morning, 5:00 on Monday, Wednesday and Friday.
      The whole Quartz operation scheduling process is as follows:
  1. Create a Scheduler through the trigger factory (the implementation class of SchedulerFactory);
  2. Create a task instance (JobDetail), specify the implementation class that implements the Job interface (HelloWord.class in the example), and specify the unique Identity group (group1 in the example) and name (myJob in the example);
  3. Create a Trigger, specify the time Trigger rule (the SimpleTrigger generated by simpleSchedule() in the example), and specify the unique Identity group (group1 in the example) and name (myTrigger in the example)
  4. Finally, the task instance (JobDetail) and Trigger (Trigger) are bound together through the Scheduler, and the task scheduling is started through the start() method.

Custom CommandLineRunner class:

About CommandLineRunner: in normal development, it may be necessary to implement the functions executed after the project is started. A simple implementation scheme provided by SpringBoot is to add a model and implement the CommandLineRunner interface. The code to realize the functions is placed in the implemented run method
The run method String... args of this class are parameters that can be passed in when the application is started. There are two ways to pass parameters

One is to pass parameters through the command line, so this interface is called CommandLineRunner
Another way is to configure parameters through IntelliJ IDEA
Command line parameters
First, type the application into a jar package, and then run the following command line, where three parameters are passed in

java -jar MyProject.jar my name is
Or configure in idea:

code:

import com.itheima.pinda.entity.ScheduleJobEntity;
import com.itheima.pinda.mapper.ScheduleJobMapper;
import com.itheima.pinda.utils.ScheduleUtils;
import lombok.extern.slf4j.Slf4j;
import org.quartz.CronTrigger;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * Initialize scheduled tasks at project startup
 */
@Component
@Slf4j
public class DispatchCommandLineRunner implements CommandLineRunner {
    @Autowired
    private Scheduler scheduler;
    @Autowired
    private ScheduleJobMapper scheduleJobMapper;

    @Override
    public void run(String... args) throws Exception {
        log.info("Start scheduled task initialization...");
        //Query scheduled task table schedule_ All data in the job
        List<ScheduleJobEntity> list = scheduleJobMapper.selectList(null);//query data base
        for (ScheduleJobEntity scheduleJobEntity : list) {
            //Get the trigger object and call the method in ScheduleUtils
            CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJobEntity.getId());
            if(cronTrigger == null){
                //The trigger object is empty, indicating that the corresponding scheduled task has not been created
                ScheduleUtils.createScheduleJob(scheduler,scheduleJobEntity);
            }else{
                //The trigger object is not empty, indicating that the corresponding scheduled task already exists. At this time, it only needs to be updated
                ScheduleUtils.updateScheduleJob(scheduler,scheduleJobEntity);
            }
        }
    }
}

Create and update scheduled tasks

The above custom CommandLineRunner class calls the tool class method to really run the task. It needs to write a timed task utility class, which is easy to get trigger key and jobkey through jobid, and get expression trigger.
Here, you can create and update scheduled tasks and execute a specific task immediately

/**
 * Scheduled task tool class
 *
 * @author
 */
public class ScheduleUtils {
    private final static String JOB_NAME = "TASK_";
    /**
     * Task scheduling parameter key
     */
    public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY";

    /**
     * Get trigger key
     */
    public static TriggerKey getTriggerKey(String jobId) {
        return TriggerKey.triggerKey(JOB_NAME + jobId);
    }

    /**
     * Get jobKey
     */
    public static JobKey getJobKey(String jobId) {
        return JobKey.jobKey(JOB_NAME + jobId);
    }

    /**
     * Get expression trigger
     */
    public static CronTrigger getCronTrigger(Scheduler scheduler, String jobId) {
        try {
            return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
        } catch (SchedulerException e) {
            throw new PdException("getCronTrigger ERROR", e);
        }
    }

    /**
     * Create scheduled task
     */
    public static void createScheduleJob(Scheduler scheduler, ScheduleJobEntity scheduleJob) {
        try {
            //Build job information
            JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getId())).build();

            //Expression scheduling builder
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
                    .withMisfireHandlingInstructionDoNothing();

            //Build a new trigger according to the new cronExpression expression
            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(scheduleJob.getId())).withSchedule(scheduleBuilder).build();

            //Put in parameters, and the runtime method can get
            jobDetail.getJobDataMap().put(JOB_PARAM_KEY, scheduleJob);

            scheduler.scheduleJob(jobDetail, trigger);

            //Suspend task
            if (scheduleJob.getStatus() == ScheduleStatus.PAUSE.getValue()) {
                pauseJob(scheduler, scheduleJob.getId());
            }
        } catch (SchedulerException e) {
            throw new PdException("CREATE ERROR", e);
        }
    }

    /**
     * Update scheduled tasks
     */
    public static void updateScheduleJob(Scheduler scheduler, ScheduleJobEntity scheduleJob) {
        try {
            TriggerKey triggerKey = getTriggerKey(scheduleJob.getId());

            //Expression scheduling builder
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
                    .withMisfireHandlingInstructionDoNothing();

            CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getId());

            //Rebuild the trigger with the new cronExpression expression
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();

            //parameter
            trigger.getJobDataMap().put(JOB_PARAM_KEY, scheduleJob);

            scheduler.rescheduleJob(triggerKey, trigger);

            //Suspend task
            if (scheduleJob.getStatus() == ScheduleStatus.PAUSE.getValue()) {
                pauseJob(scheduler, scheduleJob.getId());
            }

        } catch (SchedulerException e) {
            throw new PdException("UPDATE ERROR", e);
        }
    }

    /**
     * Execute the task now
     */
    public static void run(Scheduler scheduler, ScheduleJobEntity scheduleJob) {
        try {
            //parameter
            JobDataMap dataMap = new JobDataMap();
            dataMap.put(JOB_PARAM_KEY, scheduleJob);

            scheduler.triggerJob(getJobKey(scheduleJob.getId()), dataMap);
        } catch (SchedulerException e) {
            throw new PdException("RUN ERROR", e);
        }
    }

    /**
     * Suspend task
     */
    public static void pauseJob(Scheduler scheduler, String jobId) {
        try {
            scheduler.pauseJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            throw new PdException("PAUSE ERROR", e);
        }
    }

    /**
     * Recovery task
     */
    public static void resumeJob(Scheduler scheduler, String jobId) {
        try {
            scheduler.resumeJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            throw new PdException("RESUME ERROR", e);
        }
    }

    /**
     * Delete scheduled task
     */
    public static void deleteScheduleJob(Scheduler scheduler, String jobId) {
        try {
            scheduler.deleteJob(getJobKey(jobId));
        } catch (SchedulerException e) {
            throw new PdException("DELETE ERROR", e);
        }
    }
}

service layer

@Service
public class ScheduleJobServiceImpl extends ServiceImpl<ScheduleJobMapper, ScheduleJobEntity> implements IScheduleJobService {
    @Autowired
    private Scheduler scheduler;
   // Calling ScheduleUtils. in the service layer Run method to execute the task immediately   
@Override
    @Transactional(rollbackFor = Exception.class)
    public void run(String[] ids) {
        for (String id : ids) {
            ScheduleUtils.run(scheduler, baseMapper.selectById(id));
        }
    }
    }

Method of calling service in controller

  @PutMapping("/run/{id}")
    @ApiOperation("Execute now")
    public Result run(@PathVariable String id) {
        scheduleJobService.run(new String[]{id});

        return Result.ok();
    }

Customize QuartzJobBean

[QuartzJobBean]:
A simple implementation of the Quartz Job interface, using the passed in JobDataMap and SchedulerContext as bean property values. This is appropriate because each execution creates a new Job instance. The JobDataMap entry will overwrite the SchedulerContext entry with the same key.
For example, suppose that the JobDataMap contains a key "myParam" with a value of "5": then, the Job implementation can expose a bean attribute "myParam" of type int to receive such a value, that is, the method "setMyParam(int)" This also applies to complex types, such as business objects.
Use jobbuilder in ScheduleUtils above Newjob (schedulejob. Class) constructs job information. It is necessary to expand QuartzJobBean and customize a scheduled task class
ScheduleJob inherits from QuartzJobBean, and the program will enter executeInternal to execute scheduled tasks

/**
 * Timed task class for intelligent scheduling
 */
public class ScheduleJob extends QuartzJobBean {
    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        ScheduleJobEntity scheduleJob =
                (ScheduleJobEntity) context.getMergedJobDataMap().
                        get(ScheduleUtils.JOB_PARAM_KEY);
        System.out.println(new Date() + "Scheduled task start execution...,Timed task id: " + scheduleJob.getId());

        //Record log information related to scheduled tasks
        //Encapsulating log objects
        ScheduleJobLogEntity logEntity = new ScheduleJobLogEntity();
        logEntity.setId(IdUtils.get());
        logEntity.setJobId(scheduleJob.getId());
        logEntity.setBeanName(scheduleJob.getBeanName());
        logEntity.setParams(scheduleJob.getParams());
        logEntity.setCreateDate(new Date());

        long startTime = System.currentTimeMillis();

        try{
            //The target object is called through reflection, and the core logic of intelligent scheduling is encapsulated in the target object
            logger.info("Scheduled task ready for execution id Is:{}",scheduleJob.getId());

            //Get target object
            Object target = SpringContextUtils.getBean(scheduleJob.getBeanName());
            //Get target method object
            Method method = target.getClass().getDeclaredMethod("run", String.class, String.class, String.class, String.class);

            //Call the method of the target object through reflection
            method.invoke(target,scheduleJob.getBusinessId(),scheduleJob.getParams(),scheduleJob.getId(),logEntity.getId());

            logEntity.setStatus(1);//success
        }catch (Exception ex){
            logEntity.setStatus(0);//fail
            logEntity.setError(ExceptionUtils.getErrorStackTrace(ex));
            logger.error("Timed task execution failed, task id Is:{}",scheduleJob.getId());
        }finally {
            int times = (int) (System.currentTimeMillis() - startTime);
            logEntity.setTimes(times);//time consuming

            IScheduleJobLogService scheduleJobLogService = SpringContextUtils.getBean(IScheduleJobLogService.class);
            scheduleJobLogService.save(logEntity);
        }
    }
}

Scheduled task entity class:

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("schedule_job")
public class ScheduleJobEntity implements Serializable {

    /**
     * id
     */
    @TableId(value = "id", type = IdType.INPUT)
    private String id;
    /**
     * creator
     */
    @TableField(fill = FieldFill.INSERT)
    private Long creator;
    /**
     * Creation time
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createDate;
    /**
     * spring bean name
     */
    private String beanName;
    /**
     * parameter
     */
    private String params;
    /**
     * cron expression
     */
    private String cronExpression;
    /**
     * Task status 0: paused 1: normal
     */
    private Integer status;
    /**
     * remarks
     */
    private String remark;
    /**
     * Business id organization id
     */
    private String businessId;
    /**
     * Updater
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updater;
    /**
     * Update time
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateDate;
}

mapper interface:

/**
 * Timed task Mapper interface
 */
@Component
@Mapper
public interface ScheduleJobMapper extends BaseMapper<ScheduleJobEntity> {

}

When the scheduled task is started after successful configuration, it will query the database and output the corresponding contents on the console:

Some writing methods of time trigger

The specific time setting can be referred to
“0/10 * * * * ?” Triggered every 10 seconds
“0 0 12 * * ?” Triggered at 12 noon every day
"0 15 10? * *" is triggered at 10:15 a.m. every day
“0 15 10 * * ?” Triggered every morning at 10:15
"0 15 10 * * ? " Triggered every morning at 10:15
"0 15 10 * *? 2005" triggered at 10:15 am every day in 2005
“0 * 14 * * ?” Triggered every 1 minute between 2 p.m. and 2:59 p.m. every day
“0 0/5 14 * * ?” Triggered every 5 minutes between 2 p.m. and 2:55 p.m. every day
“0 0/5 14,18 * * ?” Triggered every 5 minutes between 2 p.m. and 2:55 p.m. and between 6 p.m. and 6:55 p.m
“0 0-5 14 * * ?” Triggered every 1 minute between 2 p.m. and 2:05 p.m. every day
"0 10,44 14? 3 wed" is triggered at 2:10 PM and 2:44 pm on Wednesday in March every year
"0 15 10? * Mon-Fri" is triggered at 10:15 am from Monday to Friday
“0 15 10 15 * ?” Triggered at 10:15 am on the 15th of each month
“0 15 10 L * ?” Triggered at 10:15 a.m. on the last day of each month
"0 15 10? * 6L" is triggered at 10:15 a.m. on the last Friday of each month
"0 15 10? * 6L 2002-2005" triggered at 10:15 a.m. on the last Friday of each month from 2002 to 2005
"0 15 10? * 6#3" is triggered at 10:15 am on the third Friday of each month
Execute every 5 seconds: / 5 * * * *?
Every 1 minute: 0 * / 1 * *?
Once every day at 23:00: 0 0 23 * *?
Execute once every day at 1 a.m.: 0 0 1 * *?
At 1:00 a.m. on the 1st of each month: 0 0 1 *?
Once at 23:00 on the last day of each month: 0 0 23 L *?
Once every Sunday at 1 a.m.: 0 0 1* L
Once at 26, 29 and 33 points: 0 26,29,33 * *?
At 0:00, 13:00, 18:00 and 21:00 every day: 0,13,18,21 * *?

Topics: Java Spring Spring Boot