Run a Spring Batch Job With Quartz
Hey, folks. In this tutorial, we will see how a Spring Batch job runs using a Quartz scheduler. If you are not sure about the basics of Spring Batch, you can visit my tutorial here.
Now, as we know, Spring Batch jobs are used whenever we want to run any business-specific code or run/generate any reports at any particular time/day. There are two ways to implement jobs: tasklet
and chunks
. In this tutorial, I will create a simple job using a tasklet
, which will print a logger
. The basic idea here is what all configurations are required to make this job run. We will use Spring Boot to bootstrap our application.
We require below two dependencies in pom.xml for having Spring Batch and Quartz in our application.
<!-- https://mvnrepository.com/artifact/org.springframework.batch/spring-batch-core -->
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-core</artifactId>
<version>4.0.1.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
Now, let's see what all configurations we require in our code to run the job.
1. BatchConfiguration.java:
package com.category.batch.configurations;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.support.ApplicationContextFactory;
import org.springframework.batch.core.configuration.support.AutomaticJobRegistrar;
import org.springframework.batch.core.configuration.support.DefaultJobLoader;
import org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor;
import org.springframework.batch.core.configuration.support.MapJobRegistry;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean;
import org.springframework.batch.support.transaction.ResourcelessTransactionManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@EnableBatchProcessing
@Import({
BatchJobsDetailedConfiguration.class
})
public class BatchConfiguration {
@Bean
public JobRegistry jobRegistry() {
return new MapJobRegistry();
}
@Bean
public ResourcelessTransactionManager transactionManager() {
return new ResourcelessTransactionManager();
}
@Bean
public JobRepository jobRepository(ResourcelessTransactionManager transactionManager) throws Exception {
MapJobRepositoryFactoryBean mapJobRepositoryFactoryBean = new MapJobRepositoryFactoryBean(transactionManager);
mapJobRepositoryFactoryBean.setTransactionManager(transactionManager);
return mapJobRepositoryFactoryBean.getObject();
}
@Bean
public JobLauncher jobLauncher(JobRepository jobRepository) throws Exception {
SimpleJobLauncher simpleJobLauncher = new SimpleJobLauncher();
simpleJobLauncher.setJobRepository(jobRepository);
simpleJobLauncher.afterPropertiesSet();
return simpleJobLauncher;
}
@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor(JobRegistry jobRegistry) {
JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor = new JobRegistryBeanPostProcessor();
jobRegistryBeanPostProcessor.setJobRegistry(jobRegistry());
return jobRegistryBeanPostProcessor;
}
}
Let's go one-by-one:
-
@Configuration
: This specifies that this class will contain beans and will be instantiated at load time. -
@EnableBatchProcessing
: This enables the Spring Batch features and provides a base configuration for setting up batch jobs. -
@Import({BatchJobsDetailedConfiguration.class})
: This will import some other configurations required, which we will see later. -
JobRegistry
: This interface is used to register the jobs. -
ResourcelessTransactionManager
: This class is used when you want to run the job using any database persistence. -
JobRepository
: This contains all the metadata of the job, which returns aMapJobRepositoryFactoryBean
used for non-persistent DAO implementations. -
JobLauncher
: This is used to launch a job, requires jobRepository as a dependency. -
JobRegistryBeanPostProcessor
: This is used to register a job in thejobRegistry
, which returns thejobRegistry
.
Let's go to the imported class now.
2. BatchJobsDetailedConfiguration.java:
package com.category.batch.configurations;
import java.util.HashMap;
import java.util.Map;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.configuration.support.ApplicationContextFactory;
import org.springframework.batch.core.configuration.support.GenericApplicationContextFactory;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import com.category.batch.job.JobLauncherDetails;
import com.category.batch.reports.config.ReportsConfig;
@Configuration
public class BatchJobsDetailedConfiguration {
@Autowired
private JobLauncher jobLauncher;
@Bean(name = "reportsDetailContext")
public ApplicationContextFactory getApplicationContext() {
return new GenericApplicationContextFactory(ReportsConfig.class);
}
@Bean(name = "reportsDetailJob")
public JobDetailFactoryBean jobDetailFactoryBean() {
JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean();
jobDetailFactoryBean.setJobClass(JobLauncherDetails.class);
jobDetailFactoryBean.setDurability(true);
Map < String, Object > map = new HashMap < > ();
map.put("jobLauncher", jobLauncher);
map.put("jobName", ReportsConfig.jobName);
jobDetailFactoryBean.setJobDataAsMap(map);
return jobDetailFactoryBean;
}
@Bean(name = "reportsCronJob")
public CronTriggerFactoryBean cronTriggerFactoryBean() {
CronTriggerFactoryBean cronTriggerFactoryBean = new CronTriggerFactoryBean();
cronTriggerFactoryBean.setJobDetail(jobDetailFactoryBean().getObject());
cronTriggerFactoryBean.setCronExpression("0 0/1 * 1/1 * ? *");
return cronTriggerFactoryBean;
}
@Bean
public SchedulerFactoryBean schedulerFactoryBean(JobRegistry jobRegistry) throws NoSuchJobException {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setTriggers(cronTriggerFactoryBean().getObject());
schedulerFactoryBean.setAutoStartup(true);
Map < String, Object > map = new HashMap < > ();
map.put("jobLauncher", jobLauncher);
map.put("jobLocator", jobRegistry);
schedulerFactoryBean.setSchedulerContextAsMap(map);
return schedulerFactoryBean;
}
}
Let's dig into this:
ApplicationContextFactory
: This interface is primarily useful when creating a newApplicationContext
per execution of a job. It's better to create a sepearteapplicationContext
for each job.JobDetailFactoryBean
: This is used to create a Quartz job detail instance. This class will set a job class, which we will see later. It creates a map that will define and set the job name using a class andjoblauncher
.CronTriggerFactoryBean
: This is used to create a Quartzcron
trigger instance. This will set thejobDetail
created earlier and then thecron
expression when this job will run. You can set thecron
expressions as per your need. Cron expressions can be calculated from http://cronmaker.com.SchedulerFactoryBean
: This is used to create a Quartz scheduler instance and allows for the registration ofJobDetails
,Calendars
, andTriggers
, automatically starting the scheduler on initialization and shutting it down on destruction.
Let's check out the JobLauncherDetails
class:
3. JobLauncherDetails.java:
package com.category.batch.job;
import java.util.Map;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.JobParametersInvalidException;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.batch.core.launch.NoSuchJobException;
import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException;
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRestartException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.QuartzJobBean;
public class JobLauncherDetails extends QuartzJobBean {
static final String JOB_NAME = "jobName";
public void setJobLocator(JobLocator jobLocator) {
this.jobLocator = jobLocator;
}
public void setJobLauncher(JobLauncher jobLauncher) {
this.jobLauncher = jobLauncher;
}
private JobLocator jobLocator;
private JobLauncher jobLauncher;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis()).toJobParameters();
try {
Map < String, Object > jobDataMap = jobExecutionContext.getMergedJobDataMap();
String jobName = (String) jobDataMap.get(JOB_NAME);
jobLauncher.run(jobLocator.getJob(jobName), jobParameters);
} catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException
|
JobParametersInvalidException e) {
e.printStackTrace();
} catch (NoSuchJobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
This class has an overridden executeInternal
method of the QuartzJobBean
class, which takes the jobdetails
from the map, which were already set before some of the jobParameters
, and then executes the jobLauncher.run()
to run the job as seen in the code.
Lets visit the ReportsConfig
class.
4.ReportsConfig.java:
package com.category.batch.reports.config;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.DuplicateJobException;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.support.ReferenceJobFactory;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.category.batch.reports.tasklet.ReportTasklet;
@Configuration
public class ReportsConfig {
@Autowired
private JobRegistry jobRegistry;
public final static String jobName = "ReportsJob1";
public JobBuilderFactory getJobBuilderFactory() {
return jobBuilderFactory;
}
public void setJobBuilderFactory(JobBuilderFactory jobBuilderFactory) {
this.jobBuilderFactory = jobBuilderFactory;
}
@Autowired
private Tasklet taskletstep;
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public ReportTasklet reportTasklet() {
return new ReportTasklet();
}
@Bean
public Job job() throws DuplicateJobException {
Job job = getJobBuilderFactory().get(jobName).start(getStep()).build();
return job;
}
@Bean
public Step getStep() {
return stepBuilderFactory.get("step").tasklet(reportTasklet()).build();
}
}
The main purpose of the class is having configurations related to each job. You will have a separate config for each job as this. As you can see, we create the tasklet
here, which we will see later. Also, we define and return the Job
, step using a JobBuilderFactory
, and StepBuilderFactory
. These factories will automatically set the JobRepository
for you.
Let's go to the ReportTasklet
, which is our job to be run.
5. ReportTasklet.java:
package com.category.batch.reports.tasklet;
import java.util.logging.Logger;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ReportTasklet implements Tasklet {
private static final Logger logger = Logger.getLogger(ReportTasklet.class.getName());
@Override
public RepeatStatus execute(StepContribution arg0, ChunkContext arg1) {
try {
logger.info("Report's Job is running. Add your business logic here.........");
} catch (Exception e) {
e.printStackTrace();
}
return RepeatStatus.FINISHED;
}
}
This class has a execute method that will be ran when the job is ran through the jobLauncher.run()
from the JobLauncherDetails
class. You can define your business logic that needs to be executed here.
We will need some configuration in application.properties as below:
6. application.properties
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
spring.batch.job.enabled=false
The first property is required to disable the datasource— only for testing purposes and is not required in production.
The second property is when before the server starts, the job is run. To avoaid this, we require this property.
Now, finally, let's go to the application class. This should be self-explanatory.
7. BatchApplication.java:
package com.category.batch;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
public class BatchApplication {
public static void main(String[] args) {
SpringApplication.run(BatchApplication.class, args);
}
}
Enough Configurations! Let's run this application and see the output. We have set the cron
to 1 minute. After 1 minute, the job will be run.
2018-09-14 11:04:18.648 INFO 7008 --- [ost-startStop-1] org.quartz.impl.StdSchedulerFactory : Quartz scheduler 'schedulerFactoryBean' initialized from an externally provided properties instance.
2018-09-14 11:04:18.648 INFO 7008 --- [ost-startStop-1] org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.0
2018-09-14 11:04:18.653 INFO 7008 --- [ost-startStop-1] org.quartz.core.QuartzScheduler : JobFactory set to: org.springframework.scheduling.quartz.AdaptableJobFactory@b29e7d6
2018-09-14 11:04:19.578 INFO 7008 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-09-14 11:04:20.986 INFO 7008 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@74a6e16d: startup date [Fri Sep 14 11:04:12 IST 2018]; root of context hierarchy
2018-09-14 11:04:21.264 INFO 7008 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-09-14 11:04:21.268 INFO 7008 --- [ost-startStop-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-09-14 11:04:21.356 INFO 7008 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-09-14 11:04:21.356 INFO 7008 --- [ost-startStop-1] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-09-14 11:04:22.526 INFO 7008 --- [ost-startStop-1] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-09-14 11:04:22.555 INFO 7008 --- [ost-startStop-1] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 2147483647
2018-09-14 11:04:22.556 INFO 7008 --- [ost-startStop-1] o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now
2018-09-14 11:04:22.556 INFO 7008 --- [ost-startStop-1] org.quartz.core.QuartzScheduler : Scheduler schedulerFactoryBean_$_NON_CLUSTERED started.
2018-09-14 11:04:22.578 INFO 7008 --- [ost-startStop-1] com.category.batch.ServletInitializer : Started ServletInitializer in 17.386 seconds (JVM running for 26.206)
2018-09-14 11:04:23.395 INFO 7008 --- [ main] org.apache.coyote.ajp.AjpNioProtocol : Starting ProtocolHandler ["ajp-nio-8009"]
2018-09-14 11:04:23.399 INFO 7008 --- [ main] org.apache.catalina.startup.Catalina : Server startup in 24866 ms
2018-09-14 11:05:02.889 INFO 7008 --- [ryBean_Worker-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=ReportsJob1]] launched with the following parameters: [{time=1536903301155}]
2018-09-14 11:05:03.262 INFO 7008 --- [ryBean_Worker-1] o.s.batch.core.job.SimpleStepHandler : Executing step: [step]
2018-09-14 11:05:03.503 INFO 7008 --- [ryBean_Worker-1] c.c.batch.reports.tasklet.ReportTasklet : Report's Job is running. Add your business logic here.........
2018-09-14 11:05:03.524 INFO 7008 --- [ryBean_Worker-1] o.s.b.c.l.support.SimpleJobLauncher : Job: [SimpleJob: [name=ReportsJob1]] completed with the following parameters: [{time=1536903301155}] and the following status: [COMPLETED]
The bold lines indicate your job ran and completed successfully. That's all for this tutorial. Please comment if you would like to add anything. Happy learning!