SoFunction
Updated on 2025-03-06

Detailed explanation of Spring integrating Quartz to implement dynamic timing tasks

Recently, the function of timing tasks is needed in the project. Although spring also comes with a lightweight timing task implementation, it feels not flexible enough and the functions are not powerful enough. After consideration, it was decided to integrate the more professional Quartz to implement the timing task function.

Normal timing tasks

First of all, of course, add the dependency jar file. My project is managed by maven, and the following dependencies of my project:

<dependencies>
  <dependency>
    <groupId></groupId>
    <artifactId>spring-core</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>spring-context</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>spring-web</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>spring-tx</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>mybatis</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.7.4</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>ojdbc14</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId></groupId>
    <artifactId>spring-context-support</artifactId>
    <version>${}</version>
  </dependency>
  <dependency>
    <groupId>-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>${}</version>
  </dependency>
</dependencies>

Maybe you should see that my project is spring integrating mybatis. Currently, the latest version of spring has entered the series, but the latest version of mybatis-spring integration plug-in still relies on spring 3.1., so we did not use the latest version of spring but used the recommended 3.1., after all, the latest version of the function is generally not used.

As for quartz, it uses the latest version of 2.2.1

The reason why I specifically explain the version here is because the integration of spring and quartz has requirements for the version.

Versions below spring3.1 must use series, and versions above 3.1 must support quartz, otherwise an error will occur.

As for the reason, it is that spring's support implementation for quartz, inherits it. It is a class in the series, but becomes an interface in the series, which makes it impossible to configure quartz's triggers in spring.

There are two ways to implement Quartz in Spring: the first is to inherit QuartzJobBean from the task class, and the second is to define the task class and the method to be executed in the configuration file. The classes and methods can be ordinary classes. Obviously, the second method is much more flexible than the first method.

The second method is adopted here.

Spring configuration file:

&lt;!-- useMethodInvokingJobDetailFactoryBean,Task class can not be implementedJobinterface,passtargetMethodSpecify the call method--&gt;
&lt;bean  class=""/&gt;
&lt;bean  class=""&gt;
  &lt;property name="group" value="job_work"/&gt;
  &lt;property name="name" value="job_work_name"/&gt;
  &lt;!--falseIt means that the previous task is executed before starting a new task.--&gt;
  &lt;property name="concurrent" value="false"/&gt;
  &lt;property name="targetObject"&gt;
    &lt;ref bean="taskJob"/&gt;
  &lt;/property&gt;
  &lt;property name="targetMethod"&gt;
    &lt;value&gt;run&lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;

&lt;!-- Scheduling triggers --&gt;
&lt;bean 
   class=""&gt;
  &lt;property name="name" value="work_default_name"/&gt;
  &lt;property name="group" value="work_default"/&gt;
  &lt;property name="jobDetail"&gt;
    &lt;ref bean="jobDetail" /&gt;
  &lt;/property&gt;
  &lt;property name="cronExpression"&gt;
    &lt;value&gt;0/5 * * * * ?&lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;

&lt;!-- Scheduling factory --&gt;
&lt;bean  class=""&gt;
  &lt;property name="triggers"&gt;
    &lt;list&gt;
      &lt;ref bean="myTrigger"/&gt;
    &lt;/list&gt;
  &lt;/property&gt;
&lt;/bean&gt;

The Task class is an ordinary Java class, which does not inherit any class and implement any interface (of course, you can declare beans in annotation):

//@Component
public class DataConversionTask{

  /** Log object */
  private static final Logger LOG = ();

  public void run() {

    if (()) {
      ("The data conversion task thread starts execution");
    }
  }
}

At this point, the simple integration is completed. The run method will be executed every 5 seconds. Because concurrent is configured equal to false, if the execution time of the run method exceeds 5 seconds, the next scheduled execution task will not be enabled even if the time has exceeded 5 seconds before execution. If it is true, regardless of whether the execution is completed or not, the time will be enabled.

Next, how to dynamically modify the time of execution and how to stop the task being executed.

By the way, I'll post a cronExpression expression memo:

Field Allowed Values ​​Allowed Special Characters

  1. Seconds 0-59, – * /
  2. Score 0-59, – * /
  3. Hours 0-23, – * /
  4. Date 1-31, – * ? / L W C
  5. Months 1-12 or JAN-DEC, – * /
  6. Week 1-7 or SUN-SAT, – * ? / L C #
  7. Year (optional) Leave blank, 1970-2099, – * /

Expression meaning

"0 0 12 * * ?"       Every day at noon12Point trigger
"0 15 10 ? * *"       Every morning10:15trigger
"0 15 10 * * ?"       Every morning10:15trigger
"0 15 10 * * ? *"      Every morning10:15trigger
"0 15 10 * * ? 2005"    2005年的Every morning10:15trigger
"0 * 14 * * ?"       Every afternoon2Click to afternoon2:59During each period1分钟trigger
"0 0/5 14 * * ?"      Every afternoon2Click to afternoon2:55During each period5分钟trigger
"0 0/5 14,18 * * ?"     Every afternoon2Click to2:55Period and afternoon6Click to6:55During each period5分钟trigger
"0 0-5 14 * * ?"      Every afternoon2Click to afternoon2:05During each period1分钟trigger
"0 10,44 14 ? 3 WED"    Wednesday afternoon every year2:10and2:44trigger
"0 15 10 ? * MON-FRI"    Monday to Friday morning10:15trigger
"0 15 10 15 * ?"      per month15On the morning of the day10:15trigger
"0 15 10 L * ?"       per month最后一日的上午10:15trigger
"0 15 10 ? * 6L"      per month的最后一个星期五上午10:15trigger
"0 15 10 ? * 6L 2002-2005" 2002Year to2005年的per month的最后一个星期五上午10:15trigger
"0 15 10 ? * 6#3" Triggered on the third Friday of each month at 10:15 am0 6 * * *          Every morning6point
0 */2 * * *         Every two hours
0 23-7/2,8 * * *      night11Click to早上8point之间Every two hours,早上八point
0 11 4 * 1-3        Monthly4号and每个礼拜的礼拜一到礼拜三的早上11point
0 4 1 1 *          1moon1Day morning4point

Dynamically add timed tasks

Previously, we have integrated Spring and Quartz in configuration files, and if the requirements are relatively simple, it should be able to meet them. However, many times, we often encounter tasks that need to be added or modified dynamically, while the timing task component provided in spring can only control the time of the timing task and the activation or stop of the task by modifying the trigger configuration in XML. This not only brings us convenience but also loses the flexibility of dynamically configuring tasks. I searched for some online solutions, but none of them solved this problem well, and most of the mentioned solutions remained on the Quartz series version, and the code and API used were no longer applicable to the new versions of Spring and Quartz. I had no choice but to rely on myself. I spent some time studying the relevant code in Spring and Quartz.

First, let’s review the configuration code using quartz in spring:

&lt;!-- useMethodInvokingJobDetailFactoryBean,Task class can not be implementedJobinterface,passtargetMethodSpecify the call method--&gt;
&lt;bean  class=""/&gt;
&lt;bean  class=""&gt;
  &lt;property name="group" value="job_work"/&gt;
  &lt;property name="name" value="job_work_name"/&gt;
  &lt;!--falseIt means that the previous task is executed before starting a new task.--&gt;
  &lt;property name="concurrent" value="false"/&gt;
  &lt;property name="targetObject"&gt;
    &lt;ref bean="taskJob"/&gt;
  &lt;/property&gt;
  &lt;property name="targetMethod"&gt;
    &lt;value&gt;execute&lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;

&lt;!-- Scheduling triggers --&gt;
&lt;bean 
   class=""&gt;
  &lt;property name="name" value="work_default_name"/&gt;
  &lt;property name="group" value="work_default"/&gt;
  &lt;property name="jobDetail"&gt;
    &lt;ref bean="jobDetail" /&gt;
  &lt;/property&gt;
  &lt;property name="cronExpression"&gt;
    &lt;value&gt;0/5 * * * * ?&lt;/value&gt;
  &lt;/property&gt;
&lt;/bean&gt;

&lt;!-- Scheduling factory --&gt;
&lt;bean  class=""&gt;
  &lt;property name="triggers"&gt;
    &lt;list&gt;
      &lt;ref bean="myTrigger"/&gt;
    &lt;/list&gt;
  &lt;/property&gt;
&lt;/bean&gt;

All configurations are completed in xml, including cronExpression expressions, which is very convenient. But if my task information is saved in the database, I want to be dynamically initialized, and there are many tasks, don’t I have to have a lot of XML configurations? Or I want to modify the trigger expression to make the task that was originally run every 5 seconds into 10 seconds. The problem arises. I tried not to pass in cronExpression and other parameters into the configuration file, but it reported an error when starting. Do I modify the xml file every time and restart the application? This is obviously inappropriate. The ideal is to integrate with spring while adding, deleting and modifying dynamic tasks.

Let’s take a look at the way spring implements quartz, first look at the jobDetail defined in the above configuration file. In fact, the jobDetail generated above is not the bean we defined, because in the Quartz version, JobDetail is already an interface (of course, the previous version did not directly generate JobDetail):

public interface JobDetail extends Serializable, Cloneable {…} 

Spring is implemented by converting it to the MethodInvokingJob or StatefulMethodInvokingJob type, both of which are static inner classes. The MethodInvokingJob class inherits from QuartzJobBean, while StatefulMethodInvokingJob is inherited directly from MethodInvokingJob. The difference between the implementation of these two classes is stateful and stateless, which corresponds to quartz's Job and StatefulJob. For details, you can check the quartz document, which will not be described here. Let’s first look at the main code of the QuartzJobBean they implement:

/**
 * This implementation applies the passed-in job data map as bean property
 * values, and delegates to &lt;code&gt;executeInternal&lt;/code&gt; afterwards.
 * @see #executeInternal
 */
public final void execute(JobExecutionContext context) throws JobExecutionException {
  try {
    // Reflectively adapting to differences between Quartz  and Quartz 2.0...
    Scheduler scheduler = (Scheduler) (getSchedulerMethod, context);
    Map mergedJobDataMap = (Map) (getMergedJobDataMapMethod, context);

    BeanWrapper bw = (this);
    MutablePropertyValues pvs = new MutablePropertyValues();
    (());
    (mergedJobDataMap);
    (pvs, true);
  }
  catch (SchedulerException ex) {
    throw new JobExecutionException(ex);
  }
  executeInternal(context);
}

/**
 * Execute the actual job. The job data map will already have been
 * applied as bean property values by execute. The contract is
 * exactly the same as for the standard Quartz execute method.
 * @see #execute
 */
protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
//There are also codes in MethodInvokingJobDetailFactoryBean:
public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException {
  prepare();

  // Use specific name if given, else fall back to bean name.
  String name = ( != null ?  : );

  // Consider the concurrent flag to choose between stateful and stateless job.
  Class jobClass = ( ?  : );

  // Build JobDetail instance.
  if (jobDetailImplClass != null) {
    // Using Quartz 2.0 JobDetailImpl class...
     = (JobDetail) (jobDetailImplClass);
    BeanWrapper bw = ();
    ("name", name);
    ("group", );
    ("jobClass", jobClass);
    ("durability", true);
    ((JobDataMap) ("jobDataMap")).put("methodInvoker", this);
  }
  else {
    // Using Quartz  JobDetail class...
     = new JobDetail(name, , jobClass);
    (true);
    (true);
    ().put("methodInvoker", this);
  }

  // Register job listener names.
  if ( != null) {
    for (String jobListenerName : ) {
      if (jobDetailImplClass != null) {
        throw new IllegalStateException("Non-global JobListeners not supported on Quartz 2 - " +
            "manually register a Matcher against the Quartz ListenerManager instead");
      }
      (jobListenerName);
    }
  }

  postProcessJobDetail();
}

The above mainly looks at the implementation part of the Quartz 2.0 version we are currently using. At this point, you may have already understood the principle of Spring's encapsulation of Quartz. Spring is in this way, when the job is actually executed, the class and method we inject is reverse-called.

Now that we have understood the implementation principles of Spring, we can design our own. When designing, I thought of the following points:

1. Reduce spring configuration files. In order to implement a timed task, there are too many configuration codes for spring.

2. Users can add, enable, and disable a task through pages, etc.

3. The user can modify the runtime expression of a certain task that is already running, CronExpression.

4. To facilitate maintenance and simplify the operation and call processing of tasks, it is best to have only one job implementation class, the job operation entry of the task, which is equivalent to the factory class. When the actual call is called, the relevant information of the task is passed in through parameters, and the factory class will perform the required operations based on the task information.

Let’s develop our ideas below.

1. Spring configuration file

Through research, it was found that to implement our functions, only the following configuration is required:

2. The task operation entrance, that is, the Job implementation class, here I regard it as a factory class:

/**
  * Timed task operation factory class
  *
  * @author tq
  * @date 2016/5/1
  */
public class QuartzJobFactory implements Job {

  @Override
  public void execute(JobExecutionContext context) throws JobExecutionException {
    ("Task runs successfully");
    ScheduleJob scheduleJob = (ScheduleJob)().get("scheduleJob");
    ("Task Name = [" + () + "]");
  }
}

Here we implement a stateless job. If you want to implement a stateful job, you used to implement the StatefulJob interface. In the quartz 2.2.1 I used, the StatefulJob interface is no longer recommended. It is replaced with annotation method. You only need to add annotation @DisallowConcurrentExecution to the Job class you implemented to achieve stateful:

/**
 * Timed task operation factory class
  * @author tq
  * @date 2016/5/1
 */
@DisallowConcurrentExecution
public class QuartzJobFactory implements Job {...}

3. Create a task

Since we want to create tasks dynamically, our task information must of course be saved somewhere. Here we create a new entity class that saves the task information:

/**
  * Schedule task information
  *
  * @author tq
  * @date 2016/5/1
  */
public class ScheduleJob {
  /** Task id */
  private String jobId;
  /** Task Name */
  private String jobName;
  /** Task Grouping */
  private String jobGroup;
  /** Task status 0 Disable 1 Enable 2 Delete*/
  private String jobStatus;
  /** Task run time expression */
  private String cronExpression;
  /** Task Description */
  private String desc;
  getter and setter ....
}

Next, we create test data. In actual application, the data can be saved in a database and other places. We use the task's group name + task name as the only key of the task, which is consistent with the implementation method in quartz:

/** Schedule task map */
private static Map&lt;String, ScheduleJob&gt; jobMap = new HashMap&lt;String, ScheduleJob&gt;();

static {
  for (int i = 0; i &lt; 5; i++) {
    ScheduleJob job = new ScheduleJob();
    ("10001" + i);
    ("data_import" + i);
    ("dataWork");
    ("1");
    ("0/5 * * * * ?");
    ("Data Import Task");
    addJob(job);
  }
}

/**
  * Add a task
  * @param scheduleJob
  */
public static void addJob(ScheduleJob scheduleJob) {
  (() + "_" + (), scheduleJob);
}

With the scheduling factory, the task operation entry implementation class, and the task information, the next step is to create our timed task. Here I designed it as a job corresponding to a trigger. The two groups and names are the same, which are convenient for management and are relatively clear in structure. If a new one does not exist when creating a task, if it already exists, the task will be updated. The main code is as follows:

//schedulerFactoryBean created and injected by springScheduler scheduler = ();

//Get task information data hereList&lt;ScheduleJob&gt; jobList = ();

for (ScheduleJob job : jobList) {

  TriggerKey triggerKey = ((), ());

  //Get trigger, that is, bean defined in spring configuration file  CronTrigger trigger = (CronTrigger) (triggerKey);

  //Don't exist, create a  if (null == trigger) {
    JobDetail jobDetail = ()
      .withIdentity((), ()).build();
    ().put("scheduleJob", job);

    //Expression Scheduler Builder    CronScheduleBuilder scheduleBuilder = (job
      .getCronExpression());

    //Build a new trigger by the new cronExpression expression    trigger = ().withIdentity((), ()).withSchedule(scheduleBuilder).build();

    (jobDetail, trigger);
  } else {
    // Trigger already exists, so update the corresponding timing settings    //Expression Scheduler Builder    CronScheduleBuilder scheduleBuilder = (job
      .getCronExpression());

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

    //Press the new trigger to reset the job execution    (triggerKey, trigger);
  }
}

In this way, it can be said that our dynamic task has been created and the task is done. With the above code, will you also add and modify tasks? Is it solved it by the way?

The five test tasks we created above are executed once every 5 seconds, and all will call the execution method of QuartzJobFactory. However, the incoming task information parameters are different. The following code in the execute method is to obtain specific task information, including task grouping and task name:

Copy the codeThe code is as follows:

ScheduleJob scheduleJob = (ScheduleJob)().get(“scheduleJob”);

With the task grouping and task name, the uniqueness of the task is determined. Is it easy to implement what operations are needed next?

In the future, you need to add a new timing task. You only need to add records to the task information list, and then use the execution method to achieve your specific operations by judging the task grouping and task name.

The above has initially implemented the functions we need, and additions and modifications can be learned from the source code. However, we need to test them during actual development. If a task is run every 1 hour, isn't it very inconvenient to test? Of course you can modify the task's runtime expression, but I believe this is not the best method. Next, we need to trigger the task without making any modifications to the current task information, and the trigger will only run once for testing.

Dynamic Pause Recovery Modify and Delete Tasks

Previously, we have completed the integration of spring 3 and quartz 2 and dynamically adding timing tasks. We will then improve it so that it can support more operations, such as pause, recovery, modification, etc.

Some of the codes have actually been involved in the dynamic addition timing task. Here we will refine the logic. Let’s first look at the renderings of the target we want to achieve. Here we only operate in memory and do not save any information from quartz to the database. That is, we use RAMJobStore. Of course, if you need it, you can implement it as JDBCJobStore, so that the task information will be more comprehensive.

trigger status description:

  1. None: Trigger has been completed and will not be executed, or the trigger cannot be found, or the Trigger has been deleted
  2. NORMAL: Normal state
  3. PAUSED: Pause status
  4. COMPLETE: The trigger is completed, but the task may still be executing
  5. BLOCKED: Thread blocking state
  6. ERROR: An error occurred

Planned tasks

Refers to tasks that have been added to the quartz scheduler. Because quartz does not directly provide such a query interface, we need to combine JobKey and Trigger to implement it. The core code:

Scheduler scheduler = ();
GroupMatcher&lt;JobKey&gt; matcher = ();
Set&lt;JobKey&gt; jobKeys = (matcher);
List&lt;ScheduleJob&gt; jobList = new ArrayList&lt;ScheduleJob&gt;();
for (JobKey jobKey : jobKeys) {
  List&lt;? extends Trigger&gt; triggers = (jobKey);
  for (Trigger trigger : triggers) {
    ScheduleJob job = new ScheduleJob();
    (());
    (());
    ("trigger:" + ());
     triggerState = (());
    (());
    if (trigger instanceof CronTrigger) {
      CronTrigger cronTrigger = (CronTrigger) trigger;
      String cronExpression = ();
      (cronExpression);
    }
    (job);
  }
}

The jobList in the above code is the planned task list we need. It is necessary to note that a job may have multiple triggers. When running a task immediately mentioned below, a temporary trigger will be generated and will appear here. Here we regard the situation where a Job has multiple triggers as multiple tasks. We included the previous ones that we usually use in actual projects, so here we focus on dealing with the CronTrigger situation.

Running tasks

The implementation is similar to the planned tasks, the core code:

Scheduler scheduler = ();
List&lt;JobExecutionContext&gt; executingJobs = ();
List&lt;ScheduleJob&gt; jobList = new ArrayList&lt;ScheduleJob&gt;(());
for (JobExecutionContext executingJob : executingJobs) {
  ScheduleJob job = new ScheduleJob();
  JobDetail jobDetail = ();
  JobKey jobKey = ();
  Trigger trigger = ();
  (());
  (());
  ("trigger:" + ());
   triggerState = (());
  (());
  if (trigger instanceof CronTrigger) {
    CronTrigger cronTrigger = (CronTrigger) trigger;
    String cronExpression = ();
    (cronExpression);
  }
  (job);
}

Pause the task

This is relatively simple, core code:

Scheduler scheduler = ();
JobKey jobKey = ((), ());
(jobKey);

Recovery Tasks

Compared to pause tasks, the core code:

Scheduler scheduler = ();
JobKey jobKey = ((), ());
(jobKey);

Delete a task

After deleting the task, the corresponding trigger will also be deleted.

Scheduler scheduler = ();
JobKey jobKey = ((), ());
(jobKey);

Run the task now

Run it immediately here and will only run once, making it convenient for testing. Quartz is implemented by temporarily generating a trigger, which will be automatically deleted after the task is completed. The trigger's key is generated randomly, for example: DEFAULT.MT_4k9fd10jcn9mg. In my tests, the front is fixed, and the latter part is randomly generated.

Scheduler scheduler = ();
JobKey jobKey = ((), ());
(jobKey);

Update the time expression of the task

After the update, the task will be executed immediately according to the new time expression:

Scheduler scheduler = ();

TriggerKey triggerKey = ((),
  ());

//Get trigger, that is, bean defined in spring configuration fileCronTrigger trigger = (CronTrigger) (triggerKey);

//Expression Scheduler BuilderCronScheduleBuilder scheduleBuilder = (scheduleJob
  .getCronExpression());

//Rebuild trigger by new cronExpression expressiontrigger = ().withIdentity(triggerKey)
  .withSchedule(scheduleBuilder).build();

//Press the new trigger to reset the job execution(triggerKey, trigger);

The above is all the content of this article. I hope it will be helpful to everyone's study and I hope everyone will support me more.