SpringBoot (2) Integrated Quartz

Quartz is a widely used open source task scheduling framework for executing scheduled and periodic tasks in Java applications. It provides powerful scheduling features that allow you to plan, manage and execute a variety of tasks, from simple to complex tasks.

Here are some of Quartz’s key features and capabilities:

  • Flexible scheduler: Quartz provides a highly configurable scheduler that allows you to execute tasks according to different schedules, including fixed time, daily, weekly, monthly, every second, etc. You can set the time and frequency of task execution.
  • Multi-tasking support: Quartz supports managing and executing multiple tasks simultaneously. You can define multiple jobs and triggers and add them to the scheduler.
  • Cluster and distributed scheduling: Quartz supports cluster mode, which can coordinate the execution of tasks on multiple machines. This makes Quartz ideal for large-scale and distributed applications to ensure high availability and load balancing of tasks.
  • Persistence: Quartz can persist task and scheduling information to the database so that task information is not lost when the application is restarted. This is very important for reliability and data retention.
  • Missed task handling: Quartz can configure how to handle when a task misses execution, for example, whether to execute it immediately, delay execution, or discard the task.
  • Listeners: Quartz provides various listeners that can be used to monitor the execution of tasks and perform custom operations before and after task execution.
  • Multiple job types: Quartz supports different types of jobs, including stateless jobs and stateful jobs.
    Job). This allows you to choose the job type that best suits your needs.
  • Plug-in mechanism: Quartz has a flexible plug-in mechanism that can extend its functionality. You can create custom plugins to meet specific needs.
  • Rich API: Quartz provides a rich Java API, making the configuration and management of task scheduling very convenient.

Dependencies

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
            <version>2.5.4</version>
        </dependency>

Configuration

spring.quartz.job-store-type=jdbc
# The first boot uses ALWAYS
spring.quartz.jdbc.initialize-schema=never
spring.quartz.auto-startup=true
spring.quartz.startup-delay=5s
spring.quartz.overwrite-existing-jobs=true
spring.quartz.properties.org.quartz.scheduler.instanceName=ClusterQuartz
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_
spring.quartz.properties.org.quartz.jobStore.isClustered=true
spring.quartz.properties.org.quartz.jobStore.acquireTriggersWithinLock=true
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=12000
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=5000
spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=1
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true


Configuration explanation

spring.quartz.job-store-type=jdbc: Specifies to use the database as Quartz’s job storage.

spring.quartz.jdbc.initialize-schema=never: This property is set to "never", which means that the Quartz database schema will not be automatically initialized. This means you need to manually create the Quartz database tables for Quartz to function properly. You can use the SQL scripts provided by Quartz to create these tables.

spring.quartz.auto-startup=true: Set to true to indicate that Quartz automatically starts when the Spring Boot application starts.

spring.quartz.startup-delay=5s: Quartz startup delay time, set to 5 seconds.

spring.quartz.overwrite-existing-jobs=true: Set to true, which means that if the task already exists, the existing task will be overwritten.

spring.quartz.properties.org.quartz.scheduler.instanceName=ClusterQuartz: Set the instance name for the Quartz scheduler.

spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO: The Quartz scheduler instance ID is set to "AUTO", which means it is automatically assigned a unique instance ID.

spring.quartz.properties.org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore: Specifies to use org.springframework.scheduling.quartz.LocalDataSourceJobStore as the job store.

spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate: Specifies the delegate class using the PostgreSQL database.

spring.quartz.properties.org.quartz.jobStore.tablePrefix=QRTZ_: Add a prefix to Quartz database tables to avoid conflicts with other tables.

spring.quartz.properties.org.quartz.jobStore.isClustered=true: Enable Quartz cluster mode.

spring.quartz.properties.org.quartz.jobStore.acquireTriggersWithinLock=true: Set to true to ensure that locks are used to handle concurrency when acquiring triggers.

spring.quartz.properties.org.quartz.jobStore.misfireThreshold=12000: Set the timeout period of the Quartz task to determine whether the task misses execution.

spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=5000: Set the time interval between nodes to check their status.

spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool: Defines the type of Quartz thread pool.

spring.quartz.properties.org.quartz.threadPool.threadCount=1: Set the number of threads in the thread pool.

spring.quartz.properties.org.quartz.threadPool.threadPriority=5: Set the priority of the thread.

spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true: Allows threads to inherit the class loader of the initializing thread.

job

import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.PersistJobDataAfterExecution;
import org.springframework.stereotype.Component;

/**
 * @author Wang
 */
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
@Slf4j
@Component
public class DealerJob implements Job {<!-- -->

    @Override
    public void execute(JobExecutionContext context) {<!-- -->
        log.info("start Quartz job name: {}", context.getJobDetail().getKey().getName());
        DealerImportFacade dealerImportFacade = SpringUtil.getBean(DealerImportFacade.class);

        log.info(" start import US dealer data ");
        RequestContext.current().set(RequestContextCons.REGION, DataSourceEnum.US.toString().toLowerCase());
        try {<!-- -->
// dealerImportFacade.importUsDealerData();
            log.info(" end import US dealer data ");
        } catch (Exception e) {<!-- -->
            log.error(e.getMessage(), e);
        }
    }
}

controller

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * @author Wang
 */
@RequiredArgsConstructor
@Slf4j
@RestController
@RequestMapping("/schedule/job")
public class ScheduleJobController {<!-- -->

    final ScheduleJobService scheduleJobService;
    final QuartzHelper quartzHelper;

    @PostMapping
    public AjaxRespData<ScheduleJobVO> addJob(@Valid @RequestBody AddScheduleJobDTO scheduleJobDTO) {<!-- -->
        ScheduleJobEntity scheduleJobEntity = BeanConvertUtils.convert(scheduleJobDTO, ScheduleJobEntity.class);
        scheduleJobEntity.init();
        scheduleJobEntity.setStatus(EnumScheduleJobStatus.RUN);
        ScheduleJobData data = scheduleJobService.save(scheduleJobEntity);
        quartzHelper.scheduleJob(data);
        return AjaxRespData.success(BeanConvertUtils.convert(data, ScheduleJobVO.class));
    }

    @DeleteMapping("/{jobId}")
    public AjaxRespData<Void> removeJob(@PathVariable("jobId") String jobId) {<!-- -->
        ScheduleJobEntity scheduleJobEntity = scheduleJobService.checkExist(jobId, EnumError.E30001);
        scheduleJobService.remove(jobId);
        quartzHelper.remove(scheduleJobEntity);
        return AjaxRespData.success();
    }

    @PutMapping("/{jobId}")
    public AjaxRespData<ScheduleJobVO> updateJob(@PathVariable String jobId, @Valid @RequestBody AddScheduleJobDTO scheduleJobDTO) {<!-- -->
        ScheduleJobEntity scheduleJobEntity = BeanConvertUtils.convert(scheduleJobDTO, ScheduleJobEntity.class);
        scheduleJobEntity.setId(jobId);
        ScheduleJobData data = scheduleJobService.update(scheduleJobEntity);
        quartzHelper.scheduleJob(data);
        return AjaxRespData.success(BeanConvertUtils.convert(data, ScheduleJobVO.class));
    }

    @GetMapping("/{jobId}")
    public AjaxRespData<ScheduleJobVO> getJob(@PathVariable("jobId") String jobId) {<!-- -->
        ScheduleJobEntity scheduleJobEntity = scheduleJobService.checkExist(jobId, EnumError.E30001);
        return AjaxRespData.success(BeanConvertUtils.convert(scheduleJobEntity, ScheduleJobVO.class));
    }


    @PutMapping("/operate")
    public void operateJob(@Valid @RequestBody AddScheduleJobDTO scheduleJobDTO) {<!-- -->
        ScheduleJobEntity scheduleJobEntity = scheduleJobService.checkExist(scheduleJobDTO.getId(), EnumError.E30001);
        scheduleJobEntity.setStatus(scheduleJobDTO.getStatus());
        scheduleJobService.update(scheduleJobEntity);
        quartzHelper.operateJob(scheduleJobDTO.getStatus(), scheduleJobEntity);
    }

}

service

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author Wang
 */
@RequiredArgsConstructor
@Slf4j
@Service
public class ScheduleJobService extends BaseService<ScheduleJobEntity, ScheduleJobData> {<!-- -->

    final ScheduleJobRepository scheduleJobRepository;
    final QuartzHelper quartzHelper;

    @PostConstruct
    public void init(){<!-- -->
        log.info("init schedule job...");
        List<ScheduleJobEntity> jobs = this.getRepository().findAll();
        for (ScheduleJobEntity job : jobs) {<!-- -->
            quartzHelper.scheduleJob(job);
            quartzHelper.operateJob(EnumScheduleJobStatus.PAUSE, job);
            if (job.getStatus().equals(EnumScheduleJobStatus.RUN)) {<!-- -->
                quartzHelper.operateJob(EnumScheduleJobStatus.RUN, job);
            }
        }
        log.info("init schedule job completed...");
    }

    @Override
    public BaseRepository<ScheduleJobEntity> getRepository() {<!-- -->
        return scheduleJobRepository;
    }


}

helper

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.stereotype.Component;

import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;

/**
 * @author Wang
 */
@RequiredArgsConstructor
@Slf4j
@Component
public class QuartzHelper {<!-- -->

    final Scheduler scheduler;

    public void scheduleJob(ScheduleJobEntity jobInfo) {<!-- -->

        JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup());
        try {<!-- -->
            JobDetail jobDetail = scheduler.getJobDetail(jobKey);
            if (Objects.nonNull(jobDetail)){<!-- -->
                scheduler.deleteJob(jobKey);
            }
        } catch (SchedulerException e) {<!-- -->
            e.printStackTrace();
        }

        JobDetail jobDetail = JobBuilder.newJob(getJobClass(jobInfo.getType()))
                .withIdentity(jobKey)
                .build();

        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(jobInfo.getTriggerName(), jobInfo.getTriggerGroup()).startNow()
                .withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getCronExpression()))
                .build();

        try {<!-- -->
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {<!-- -->
            log.error(e.getMessage(), e);
        }
    }

    public void rescheduleJob(ScheduleJobEntity job) {<!-- -->

        TriggerKey triggerKey = new TriggerKey(job.getTriggerName(), job.getTriggerGroup());
        try {<!-- -->
            CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);

            CronTrigger newCronTrigger = cronTrigger.getTriggerBuilder()
                    .withIdentity(triggerKey)
                    .withSchedule(CronScheduleBuilder.cronSchedule(job.getCronExpression()))
                    .build();

            scheduler.rescheduleJob(triggerKey, newCronTrigger);
        } catch (SchedulerException e) {<!-- -->
            throw new RuntimeException(e);
        }
    }

    public void remove(ScheduleJobEntity job) {<!-- -->
        TriggerKey triggerKey = new TriggerKey(job.getTriggerName(), job.getTriggerGroup());
        try {<!-- -->
            scheduler.pauseTrigger(triggerKey);
            scheduler.unscheduleJob(triggerKey);
            scheduler.deleteJob(JobKey.jobKey(job.getTriggerName(), job.getTriggerGroup()));
        } catch (SchedulerException e) {<!-- -->
            throw new RuntimeException(e);
        }
    }

    public void unscheduleJob(ScheduleJobEntity job) {<!-- -->

        TriggerKey triggerKey = new TriggerKey(job.getTriggerName(), job.getTriggerGroup());
        try {<!-- -->
            scheduler.unscheduleJob(triggerKey);
        } catch (SchedulerException e) {<!-- -->
            throw new RuntimeException(e);
        }
    }

    public void operateJob(EnumScheduleJobStatus status, ScheduleJobEntity job) {<!-- -->
        JobKey jobKey = JobKey.jobKey(job.getJobName(), job.getJobGroup());
        try {<!-- -->
            switch (status) {<!-- -->
                case RUN:
                    scheduler.resumeJob(jobKey);
                    break;
                case PAUSE:
                    scheduler.pauseJob(jobKey);
                    break;
                default:
                    throw new IllegalArgumentException();
            }
        } catch (SchedulerException e) {<!-- -->
            throw new RuntimeException(e);
        }
    }

    public String nextTime(ScheduleJobEntity job) {<!-- -->
        TriggerKey triggerKey = new TriggerKey(job.getTriggerName(), job.getTriggerGroup());
        try {<!-- -->
            Trigger trigger = scheduler.getTrigger(triggerKey);
            Date nextFireTime = trigger.getNextFireTime();
            return DateUtil.format(nextFireTime, DateTimeFormatter.ISO_DATE_TIME);
        } catch (SchedulerException e) {<!-- -->
            throw new RuntimeException(e);
        }
    }

    private Class<? extends Job> getJobClass(EnumScheduleJobType type) {<!-- -->
        Class<? extends Job> clazz;
        switch (type) {<!-- -->
            case DEALER_IMPORT:
                clazz = DealerJob.class;
                break;
// case SECONDARY_INVITING_EXPIRE:
// clazz = MockDeviceReportJob.class;
// break;
            default:
                throw new IllegalArgumentException();
        }
        returnclazz;
    }
}

Final effect

Instance 1, 8281

Instance 2, 8282

Step into the trap

Scheduled task execution interval, minimum setting is one minute