I want to be able to start my job with a REST controller, then when the job is started, it should run on a scheduled basis, until i stop it again with REST.
So this is m
In first you are defining the job:
@Bean
@Qualifier("fancyScheduledJob")
public Job job() {
return jobBuilderFactory.get("job")
.incrementer(new RunIdIncrementer())
.flow(step1())
.end()
.build();
}
In second you are initiating the execution of this job:
@Autowired
@Qualifier(value = "fancyScheduledJob")
private Job job;
@Autowired
private JobLauncher jobLauncher;
@Scheduled(cron = "0/5 * * * * ?")
public void launch() throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, JobInstanceAlreadyExistsException, NoSuchJobException {
jobLauncher.run(job, JobParametersBuilder()
.addLong("launchTime", System.currentTimeMillis())
.toJobParameters())
}
Also note that the "launchTime" paramter is introduced: by default spring batch is preventing launching the job with same parameter values.
While your schedule is quite tight - every 5 seconds you should be aware of concurrency. Or if you want to be assured that at each and every moment only 1 instance of the job is executed you can configure custom single threaded job launcher:
@Bean(name = "fancyJobExecutorPool")
public TaskExecutor singleThreadedJobExecutorPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(100500);
executor.setThreadNamePrefix("fancy-job-batch-");
return executor;
}
@Bean(name = "fancyJobLauncher")
public JobLauncher singleThreadedJobLauncher(JobRepository jobRepository)
{
SimpleJobLauncher sjl = new SimpleJobLauncher();
sjl.setJobRepository(jobRepository);
sjl.setTaskExecutor(singleThreadedJobExecutorPool());
return sjl;
}
And use this single threaded job launcher during launch time.
@Autowired
@Qualifier("fancyJobLauncher")
private JobLauncher jobLauncher;
With this your job instances will be executed one by one (but this doesn't limits parallel execution of steps inside of your job).
@Scheduled
is defined on a method and not on a Bean. So create a new Class which will be a Bean
public class BatchConfiguration {
...
@Bean
public Job job() {
return new Job();
}
new Class:
public class Job {
@Scheduled(cron = "0/5 * * * * ?")
public Job job() {
return jobBuilderFactory.get("job")
.incrementer(new RunIdIncrementer())
.flow(step1())
.end()
.build();
}
I would approach it in a way, that scheduled job runs always, but it does something only when the flag is set to true:
@Component
class ScheduledJob {
private final AtomicBoolean enabled = new AtomicBoolean(false);
@Scheduled(fixedRate = 1000)
void execute() {
if (enabled.get()) {
// run spring batch here.
}
}
void toggle() {
enabled.set(!enabled.get());
}
}
and a controller:
@RestController
class HelloController {
private final ScheduledJob scheduledJob;
// constructor
@GetMapping("/launch")
void toggle() {
scheduledJob.toggle();
}
}
In this solution you will be able to schedule and unschedule pre defined jobs using http requests. In this example we will create a daily, weekly and an oneTime Job. The application is using Quartz
.
<!--Quartz Scheduler -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
First we have to create an AutowiringSpringBeanJobFactory
class extends SpringBeanJobFactory
.
- Subclass of {@link AdaptableJobFactory} that also supports Spring-style * dependency injection on bean properties. This is essentially the direct * equivalent of Spring's {@link QuartzJobBean} in the shape of a Quartz * {@link org.quartz.spi.JobFactory}. * *
Applies scheduler context, job data map and trigger data map entries * as bean property values. If no matching bean property is found, the entry * is by default simply ignored. This is analogous to QuartzJobBean's behavior.
public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
beanFactory = applicationContext.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}
The second part is to configure the quartz configuration. In this config we need to create a
SchedulerFactoryBean
where we set global config and the application context,JobDetailFactoryBean
where we set our job, the jobGroup and the class,
CronTriggerFactoryBean
where we set the cron expression.
QuartzConfig.class
@Configuration
public class QuartzConfig {
@Autowired
ApplicationContext context;
@Bean
public SchedulerFactoryBean quartzScheduler(){
SchedulerFactoryBean quartzScheduler = new SchedulerFactoryBean();
quartzScheduler.setOverwriteExistingJobs(true);
quartzScheduler.setSchedulerName("job-scheduler");
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setApplicationContext(context);
quartzScheduler.setJobFactory(jobFactory);
return quartzScheduler;
}
@Bean
@Scope(value = "prototype")
public JobDetailFactoryBean getJobBean(String jobName, String jobGroup, Class<?> clazz){
JobDetailFactoryBean bean = new JobDetailFactoryBean();
bean.setJobClass(clazz);
bean.setGroup(jobGroup);
bean.setName(jobName);
return bean;
}
@Bean
@Scope(value = "prototype")
public CronTriggerFactoryBean getCronTriggerBean(String cronExpression, String triggerGroup){
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
bean.setCronExpression(cronExpression);
bean.setGroup(triggerGroup);
return bean;
}
}
So, after the config is done we are now able to create our jobs where the business logic will be placed. For that we have to create a class which implements Job
.
@Component
public class DailyJob implements Job{
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("Daily Job runs!");
}
}
The DailyJob
class is now ready to get scheduled. We want to schedule this job from outside via a http request. In this example we have a controller where we can send the jobname and the cron expression to schedule the dailyJob
.
@Controller
public class JobController {
@Autowired
private Scheduler scheduler;
@Autowired
private ApplicationContext context;;
@ResponseBody
@RequestMapping(value = "/job/create/daily", method = RequestMethod.POST)
public ResponseEntity<JobModel> dailyJob(@RequestBody JobModel jobModel) throws SchedulerException {
JobDetail jobDetail = context.getBean(
JobDetail.class, jobModel.getName(), "MyDailyJob", DailyJob.class);
Trigger cronTrigger = context.getBean(
Trigger.class, jobModel.getCronExpression(), "MyDailyJob");
scheduler.scheduleJob(jobDetail, cronTrigger);
return new ResponseEntity<JobModel>(jobModel, HttpStatus.CREATED);
}
}
What we see here is that we will send a post request with a JobModel
as @RequestBody
. JobModel
is a simple Pojo with two attributes name
and cronExpression
both Strings.
In this method we have to create the bean instances which we have configured previously in our config class. First create JobDetail
with Quartz JobDetail.class
, the name of your job, the name of the group and the Class which should be scheduled (in this case DailyJob.class
). After that we have to create the Trigger with Quartz Trigger.class
, the cronExpression and the group name.
After both beans are created we need to schedule the job now. So we have autowired Quartz Scheduler
to schedule the job. After that the job is enabled and ready to do its job.
So let's test the stuff. Start the application and send a post request to /job/create/daily
:
{"name":"Job 1", "cronExpression":"0 * * * * ?"}
Here we say that the job should run every minute (just to see that everything works). In your console you should see every minute Daily Job runs!
.
And here are some additional things you can do. For example get a list of the scheduled jobs:
@ResponseBody
@RequestMapping("job/list")
public List<String> jobList() throws SchedulerException {
return scheduler.getJobGroupNames();
}
To delete a job you can create endpoints, too. For example:
@ResponseBody
@RequestMapping(value = "job/delete/daily", method = RequestMethod.POST)
public ResponseEntity<Boolean> deleteJob(@RequestBody JobModel jobModel) throws SchedulerException {
JobKey jobKey = new JobKey(jobModel.getName(), "MyDailyJob");
return new ResponseEntity<Boolean>(scheduler.deleteJob(jobKey), HttpStatus.OK);
}
You are free to create many different endpoints to get informations about currently running jobs, how often jobs were running, reschedule jobs and so on. Important is just, that your jobname and the jobgroup( in our case "MyDailyJob"
) are reusable. Those information are needed to create the jobKey.
P.S.: Just to show the other mappings for the other jobs:
@ResponseBody
@RequestMapping(value = "/job/create/weekly", method = RequestMethod.POST)
public ResponseEntity<JobModel> weeklyJob(@RequestBody JobModel jobModel) throws SchedulerException {
JobDetail jobDetail = context.getBean(JobDetail.class, jobModel.getName(), JobGroup.WEEKLY_GROUP.name(),
WeeklyJob.class);
Trigger cronTrigger = context.getBean(Trigger.class, jobModel.getCronExpression(),
JobGroup.WEEKLY_GROUP.name());
scheduler.scheduleJob(jobDetail, cronTrigger);
return new ResponseEntity<JobModel>(jobModel, HttpStatus.CREATED);
}
@ResponseBody
@RequestMapping(value = "/job/create/oneTime", method = RequestMethod.POST)
public ResponseEntity<JobModel> oneTimeJob(@RequestBody JobModel jobModel) throws SchedulerException {
JobDetail jobDetail = context.getBean(JobDetail.class, jobModel.getName(), JobGroup.ONE_TIME_GROUP.name(),
OneTimeJob.class);
Trigger cronTrigger = context.getBean(Trigger.class, jobModel.getCronExpression(),
JobGroup.ONE_TIME_GROUP.name());
scheduler.scheduleJob(jobDetail, cronTrigger);
return new ResponseEntity<JobModel>(jobModel, HttpStatus.CREATED);
}
The full application is on github