SpringBoot integrates Quartz for timed tasks

Keywords: Java Spring SpringBoot Database SQL

1 Requirements

One of the functions in my front-end and back-end separated lab management project is student status statistics.My design is to calculate the proportion of each state by day.For ease of calculation, at 0 o'clock a day, the system needs to reset the student's status and insert a piece of data as the start of the day.In addition, considering the students'demand for leave, the application for leave is usually done well in advance. When the system time reaches the actual time for leave, the system will modify the status of students to take leave.

Obviously, both of these sub-requirements can be achieved through timed tasks.After a little search on the web, I chose Quartz, the more popular timed task framework.

2 Quartz

Quartz is a framework for timed tasks and other descriptions are available online.Here's a look at some of the very core interfaces in Quartz.

2.1 Scheduler interface

Scheduler is translated into a dispatcher through which Quartz registers, pauses, and deletes Trigger s and JobDetail s.Scheduler also has a SchedulerContext, which, as its name implies, is a context through which we can get some information about triggers and tasks.

2.2 Trigger interface

Triggers can be translated into triggers that specify the cycle of task execution through cron expressions or classes such as SimpleScheduleBuilder.When the system time reaches the time specified by the Trigger, the Trigger triggers the execution of the task.

2.3 JobDetail interface

Job interfaces are tasks that really need to be performed.The JobDetail interface is equivalent to wrapping the Job interface, which Trigger and Scheduler actually use.

3 Interpretation of SpringBoot Official Documents

SpringBoot officially wrote spring-boot-starter-quartz.Students who have used SpringBoot know this is an officially provided starter, with which integration can be greatly simplified.

Now let's take a look at the official SpingBoot 2.2.6 document, where Quartz Scheduler talks about Quartz in subsection 4.20, but unfortunately there are only two pages to go. Let's first look at what you can learn from such a great document.

Spring Boot offers several conveniences for working with the Quartz scheduler, including the
spring-boot-starter-quartz "Starter". If Quartz is available, a Scheduler is auto-configured (through the SchedulerFactoryBean abstraction).
Beans of the following types are automatically picked up and associated with the Scheduler:
• JobDetail: defines a particular Job. JobDetail instances can be built with the JobBuilder API.
• Calendar.
• Trigger: defines when a particular job is triggered.

Translate:

SpringBoot offers some convenient ways to work with Quartz, including the `spring-boot-starter-quartz'starter.If Quartz is available, Scheduler automatically configures it to SpringBoot through the Scheduler FactoryBean factory bean.
JobDetail, Calendar, Trigger bean s are automatically collected and associated with Scheduler.
Jobs can define setters to inject data map properties. Regular beans can also be injected in a similar manner.

Translate:

Job can define a setter, or set method, to inject configuration information.Ordinary bean s can also be injected in the same way.

Here is the sample code given in the document. I just wrote it exactly, but I got null.I don't know if I'm using it in the wrong way.When you think about it later, the document should mean that after you create a Job object, you invoke the set method to inject the dependency into it.Later, however, we generated Job objects through frame reflection, which is more complicated.Finally, you decide to annotate the Job class with @Component.

Other sections of the document cover some configurations, but they are not comprehensive enough to help.Detailed configuration can refer to w3school's Quartz Configuration.

4 SpringBoot Integrated Quartz

4.1 Table building

I chose to keep the information of the timer task in the database. The advantage is obvious. The timer task will not be lost because of the system crash.

The sql statements for building tables can be found in Quartz's github, which contains sql statements for each common database at: Quartz database table sql.

After creating the table, you can see that there are 11 more tables in the database.We don't need to worry about the specific role of each table at all. The Quartz framework operates on these tables when adding delete tasks, triggers, and so on.

4.2 Introducing Dependency

Add dependencies in pom.xml.

<!-- quartz Timed Tasks -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>2.2.6.RELEASE</version>
</dependency>

4.3 Configure quartz

Configure quartz in application.yml.The role of the associated configuration is already noted in the note.

# Configurations such as spring's datasource are not posted
spring:
  quartz:
      # Save tasks, etc. to the database
      job-store-type: jdbc
      # Wait for quartz-related content to end at the end of the program
      wait-for-jobs-to-complete-on-shutdown: true
      # Update existing Jobs at QuartzScheduler startup so that you do not delete the corresponding records of the qrtz_job_details table each time you modify the targetObject
      overwrite-existing-jobs: true
      # It's a map here, without any smart tips, Buddha
      properties:
        org:
          quartz:
          	# scheduler correlation
            scheduler:
              # Instance name of scheduler
              instanceName: scheduler
              instanceId: AUTO
            # Persistent correlation
            jobStore:
              class: org.quartz.impl.jdbcjobstore.JobStoreTX
              driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
              # Indicates that the related tables in the database begin with QRTZ_
              tablePrefix: QRTZ_
              useProperties: false
            # Thread pool correlation
            threadPool:
              class: org.quartz.simpl.SimpleThreadPool
              # Number of threads
              threadCount: 10
              # thread priority
              threadPriority: 5
              threadsInheritContextClassLoaderOfInitializingThread: true

4.4 Register periodic timed tasks

The first sub-requirement mentioned in Section 1 is a periodic task that is executed at 0 o'clock per day, and the content of the task is determined, so it's OK to register the bean s of JobDetail and Trigger directly in the code.Of course, these JobDetails and Triggers will also be persisted to the database.

/**
 * Quartz Related Configurations, Register JobDetail and Trigger
 * Note that JobDetail and Trigger are under the org.quartz package, not the spring package. Do not import errors
 */
@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail jobDetail() {
        JobDetail jobDetail = JobBuilder.newJob(StartOfDayJob.class)
                .withIdentity("start_of_day", "start_of_day")
                .storeDurably()
                .build();
        return jobDetail;
    }

    @Bean
    public Trigger trigger() {
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail())
                .withIdentity("start_of_day", "start_of_day")
                .startNow()
                // Execute at 0 o'clock per day
                .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?"))
                .build();
        return trigger;
    }
}

The builder class creates a JobDetail and a Trigger and registers them as Spring bean s.From the official documents excerpted from section 3, we already know that these beans are automatically associated with the scheduler.It is important to note that JobDetail and Trigger need to set up group names and their own names to be used as unique identifiers.Of course, the unique identities of JobDetail and Trigger can be the same because they are different classes.

Trigger specifies the cycle of task execution through a Cron expression.Students who are not familiar with cron expression can learn it in Baidu.

There is a StartOfDayJob class in JobDetail, which is an implementation class of Job interface. It defines the specific content of the task. Take a look at the code:

@Component
public class StartOfDayJob extends QuartzJobBean {
    private StudentService studentService;

    @Autowired
    public StartOfDayJob(StudentService studentService) {
        this.studentService = studentService;
    }

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext)
            throws JobExecutionException {
        // The specific logic of the task
    }
}

There is a minor problem here. When the builder created the JobDetail above, the StartOfDayJob.class was passed in. It is reasonable to assume that the Quartz framework created the StartOfDayJob object through reflection and then called executeInternal() to perform the task.With this dependency, the Job is created by reflection by Quartz, and even with the comment @Component, the StartOfDayJob object will not be registered in the ioc container, making it impossible to automate the assembly of the dependency.

Many blogs on the Internet are also described in this way.However, based on my actual tests, this allows Dependent Injection to be written, but I still don't know how it works.

4.5 Register non-periodic timed tasks

The second sub-requirement mentioned in Section 1 is that students take time off, which is obviously irregular, one-off and not cyclical.

Section 4.5 is roughly the same as Section 4.4, but there are two differences:

  • Job class needs to get some data for task execution;
  • Delete Job and Trigger after task execution is complete.

Business logic is to add Trigger and JobDetail to the scheduler when a teacher approves a student's leave request.

Entity class:

public class LeaveApplication {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private Long proposerUsername;
    @JsonFormat( pattern = "yyyy-MM-dd HH:mm",timezone="GMT+8")
    private LocalDateTime startTime;
    @JsonFormat( pattern = "yyyy-MM-dd HH:mm",timezone="GMT+8")
    private LocalDateTime endTime;
    private String reason;
    private String state;
    private String disapprovedReason;
    private Long checkerUsername;
    private LocalDateTime checkTime;

    // Omit getter, setter
}

The Service-tier logic, which is important, is described in the notes.

@Service
public class LeaveApplicationServiceImpl implements LeaveApplicationService {
    @Autowired
    private Scheduler scheduler;
    
    // Omit other methods and other dependencies

    /**
     * Add job and trigger to scheduler
     */
    private void addJobAndTrigger(LeaveApplication leaveApplication) {
        Long proposerUsername = leaveApplication.getProposerUsername();
        // Create leave start Job
        LocalDateTime startTime = leaveApplication.getStartTime();
        JobDetail startJobDetail = JobBuilder.newJob(LeaveStartJob.class)
            	// Specify task group name and task name
                .withIdentity(leaveApplication.getStartTime().toString(),
                        proposerUsername + "_start")
                // Add some parameters and execute with
                .usingJobData("username", proposerUsername)
                .usingJobData("time", startTime.toString())
                .build();
        // Create a trigger for taking time off to start a task
        // Create a cron expression to specify when the task will execute. Since the time off is determined, the time, time, and second of the year, month, day are determined, which also meets the requirement that the task be executed only once.
        String startCron = String.format("%d %d %d %d %d ? %d",
                startTime.getSecond(),
                startTime.getMinute(),
                startTime.getHour(),
                startTime.getDayOfMonth(),
                startTime.getMonth().getValue(),
                startTime.getYear());
        CronTrigger startCronTrigger = TriggerBuilder.newTrigger()
	            // Specify trigger group name and trigger name
                .withIdentity(leaveApplication.getStartTime().toString(),
                        proposerUsername + "_start")
                .withSchedule(CronScheduleBuilder.cronSchedule(startCron))
                .build();

        // Add job and trigger to scheduler
        try {
            scheduler.scheduleJob(startJobDetail, startCronTrigger);
        } catch (SchedulerException e) {
            e.printStackTrace();
            throw new CustomizedException("Failed to add leave task");
        }
    }
}

Job class logic, important points are explained in the notes.

@Component
public class LeaveStartJob extends QuartzJobBean {
    private Scheduler scheduler;
    private SystemUserMapperPlus systemUserMapperPlus;

    @Autowired
    public LeaveStartJob(Scheduler scheduler,
                         SystemUserMapperPlus systemUserMapperPlus) {
        this.scheduler = scheduler;
        this.systemUserMapperPlus = systemUserMapperPlus;
    }

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext)
            throws JobExecutionException {
        Trigger trigger = jobExecutionContext.getTrigger();
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        // Take out the data you saved when you added the task
        long username = jobDataMap.getLongValue("username");
        LocalDateTime time = LocalDateTime.parse(jobDataMap.getString("time"));

        // Write the logic of the task

        // Delete tasks after execution
        try {
            // Pause trigger timing
            scheduler.pauseTrigger(trigger.getKey());
            // Remove tasks from triggers
            scheduler.unscheduleJob(trigger.getKey());
            // Delete Task
            scheduler.deleteJob(jobDetail.getKey());
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

5 Summary

The above should meet the needs of most timed tasks.After checking the blogs on the Internet, I found that most of the blogs described in Quartz use or stay in Spring phase, and the configuration is also through xml, so after I implemented the function, I summarized the whole process, leaving it for people who need it and for their own reference in the future.

Overall, Quartz is still very convenient to implement timed tasks, and after integration with SpringBoot, configuration is very simple, which is a good choice for achieving timed tasks.

5.2 Pit 1

When using SpringBoot and Quartz in IDEA 2020.1, no org.quartz package can be found for error, but depending on the org.quartz package, import in the class has no error, and you can also jump directly to the corresponding class by Ctrl+left mouse button.I used IDEA 2019.3.4 later and no longer had this error.That's the new version of IDEA's BUG.

This article is distributed by blogs and other operating tool platforms OpenWrite Release

Posted by e-novative on Tue, 05 May 2020 18:44:49 -0700