Quartz

Quartz is (according to their website) a richly featured, open source job scheduling library that can be integrated within virtually any Java application.

We can configure Quartz to run Jobs using headless access, for numerous use cases, most commonly involving the polling of work to be performed in the background. For example:

  • period archiving of data to another system, eg blob images to S3

  • aggregating data

  • proactively monitoring health/status

In simple use cases, Spring Boot’s integration with Quartz can be used without bringing in a dependency on this extension.

This extension supports the use case where there is a requirement to pass state from one invocation of a Job to the next.

Simple use case

The simple use case (that doesn’t require a dependeny on this extension) is demonstrated in the SimpleApp starter app.

  • add the dependency (eg to the webapp module):

    pom.xml
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <exclusion>                                         (1)
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    1 to avoid Slf4j ←→ log4j2 cyclic dependency
  • implement quartz’s Job interface:

    SampleJob.java
    @Component
    @RequiredArgsConstructor(onConstructor_ = {@Inject})
    @Slf4j
    public class SampleJob implements Job {
    
        private final InteractionService interactionService;         (1)
        private final TransactionalProcessor transactionalProcessor; (1)
    
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            // ...
        }
    }
    1 for headless access to the domain object model.
  • Set up beans to act as the trigger factory and a job factory:

    QuartzModule.java
    @Configuration
    @ComponentScan
    public class QuartzModule {
    
        private static final int REPEAT_INTERVAL_SECS = 60;
        private static final int START_DELAY_SECS = 20;
        private static final int MILLIS_PER_SEC = 1000;
    
        @Bean
        public JobDetailFactoryBean jobDetail() {
            val jobDetailFactory = new JobDetailFactoryBean();
            jobDetailFactory.setJobClass(SampleJob.class);
            jobDetailFactory.setDescription("Invoke Sample Job service...");
            jobDetailFactory.setDurability(true);
            return jobDetailFactory;
        }
    
        @Bean
        public SimpleTriggerFactoryBean trigger(JobDetail job) {
            val trigger = new SimpleTriggerFactoryBean();
            trigger.setJobDetail(job);
            trigger.setStartDelay(START_DELAY_SECS * MILLIS_PER_SEC);
            trigger.setRepeatInterval(REPEAT_INTERVAL_SECS * MILLIS_PER_SEC);
            trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
            return trigger;
        }
    }
  • include the QuartzModule in the application’s top-level AppManifest.

More complex use cases

This extension supports a couple of slightly more advanced use cases.

If either are used:

  • update the pom.xml dependencies:

    <dependency>
        <groupId>org.apache.isis.extensions</groupId>
        <artifactId>isis-extensions-quartz-impl</artifactId>
    </dependency>
  • import the extension’s module in your application’s top-level AppManifest:

    @Configuration
    @Import({
            // ...
            IsisModuleExtQuartzImpl.class,
    })
    // ...
    public class AppManifest {
        // ...
    }

Preserving job state

Sometimes there is a requirement to pass state from one invocation of a job to another. For example, if some external service is unavailable, then we wouldn’t necessarily want to a periodic job to keep trying to connect, creating noise in the logs.

To support this use case, this extension provides the JobExecutionData class, which simplifies the API of Quartz’s job data map.

Injecting domain services into jobs

TODO - it’s possible this boilerplate may be unnecessary? SimpleApp's job seems to be injected into without this extra rigamorole.

If we want to inject domain services into the Quartz Job, then we should define a number of additional beans. These instantiate AutowiringSpringBeanJobFactory as the job factory:

QuartzModule.java
import org.apache.isis.extensions.quartz.spring.AutowiringSpringBeanJobFactory;

@Configuration
@ComponentScan
public class QuartzModule {

    // ...

    @Bean
    public SpringBeanJobFactory springBeanJobFactory() {
        val jobFactory = new AutowiringSpringBeanJobFactory();  (1)
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    @Bean
    public SchedulerFactoryBean scheduler(
            final Trigger trigger,
            final JobDetail jobDetail,
            final SpringBeanJobFactory sbjf) {
        val schedulerFactory = new SchedulerFactoryBean();

        schedulerFactory.setJobFactory(sbjf);
        schedulerFactory.setJobDetails(jobDetail);
        schedulerFactory.setTriggers(trigger);

        return schedulerFactory;
    }

    @Bean
    public Scheduler scheduler(
            final Trigger trigger,
            final JobDetail job,
            final SchedulerFactoryBean factory)
            throws SchedulerException {
        val scheduler = factory.getScheduler();
        scheduler.start();
        return scheduler;
    }
1 as provided by this extension