I have a code like this:
#Component
public class SampleJob implements Job {
#Autowired
private SampleTask sampleTask;
#Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
sampleTask.imprimir();
}
#Bean
public JobDetail jobDetail(){
return JobBuilder.newJob().ofType(SampleJob.class)
.storeDurably()
.withIdentity("Quartz_Job_Detail")
.withDescription("Estoy invocando el job de ejempo")
.build();
}
#Bean
public Trigger trigger(JobDetail jobDetail){
return TriggerBuilder.newTrigger().forJob(jobDetail)
.withIdentity("Quartz_Trigger")
.withDescription("Trigger de ejemplo")
.withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatForever().withIntervalInMilliseconds(5000))
.build();
}
}
In this case imprimir() is a function that only prints a text to know that quartz is working. The above code creates a trigger that run every 5 seconds. Now, what I want to do is to create multiple triggers based in the elements of a list taken from a database. Each Triggers has to have different Scheduled time. How can I create multiple triggers with different trigger times using a loop or something?
You just need to create a durable job once and then create all triggers for that job.
#Service
class QuartzService {
#Autowired
private SchedulerFactoryBean schedulerFactoryBean;
void init(List<MyTrigger> triggers) {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
scheduler.addJob(job(), true);
for (MyTrigger myTrigger: triggers) {
scheduler.scheduleJob(trigger(myTrigger));
}
}
private JobDetail job() {
return JobBuilder.newJob(SampleJob.class)
.withIdentity("QuartzJob", "QuartzJob")
.storeDurably()
.build();
}
private Trigger trigger(MyTrigger trigger) {
return TriggerBuilder.newTrigger()
.withIdentity(trigger.name)
.withSchedule(
CronScheduleBuilder.cronSchedule(trigger.schedule))
.forJob("QuartzJob", "QuartzJob")
.build();
}
}
class MyTrigger {
String name;
String schedule;
}
So, here you only need to pass a List of MyTrigger to QuartsService.init() method and it will create dynamically the triggers with its own schedule.
Related
I am new to spring batch, and I'm encountering an issue when using multiple data source in my batch.
Let me explain.
I am using 2 databases in my server with Spring Boot.
So far everything worked fine with my implementation of RoutingDataSource.
#Component("dataSource")
public class RoutingDataSource extends AbstractRoutingDataSource {
#Autowired
#Qualifier("datasourceA")
DataSource datasourceA;
#Autowired
#Qualifier("datasourceB")
DataSource datasourceB;
#PostConstruct
public void init() {
setDefaultTargetDataSource(datasourceA);
final Map<Object, Object> map = new HashMap<>();
map.put(Database.A, datasourceA);
map.put(Database.B, datasourceB);
setTargetDataSources(map);
}
#Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getDatabase();
}
}
The implementation require a DatabaseContextHolder, here it is :
public class DatabaseContextHolder {
private static final ThreadLocal<Database> contextHolder = new ThreadLocal<>();
public static void setDatabase(final Database dbConnection) {
contextHolder.set(dbConnection);
}
public static Database getDatabase() {
return contextHolder.get();
}
}
When I received a request on my server, I have a basic interceptor that sets the current database based on some input I have in the request. with the method DatabaseContextHolder.setDatabase(db); Everything works fine with my actual controllers.
It gets more complicated when I try to run a job with one tasklet.
One of my controller start an async task like this.
#GetMapping("/batch")
public void startBatch() {
return jobLauncher.run("myJob", new JobParameters());
}
#EnableBatchProcessing
#Configuration
public class MyBatch extends DefaultBatchConfigurer {
#Autowired private JobBuilderFactory jobs;
#Autowired private StepBuilderFactory steps;
#Autowired private MyTasklet tasklet;
#Bean
public Job job(Step step) {
return jobs.get("myJob").start(step).build();
}
#Bean
protected Step registeredDeliveryTask() {
return steps.get("myTask").tasklet(tasklet).build();
}
/** Overring the joblauncher get method to make it asynchornous */
#Override
public JobLauncher getJobLauncher() {
try {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(super.getJobRepository());
jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());
jobLauncher.afterPropertiesSet();
return jobLauncher;
} catch (Exception e) {
throw new BatchConfigurationException(e);
}
}
}
And my Tasklet :
#Component
public class MyTasklet implements Tasklet {
#Autowired
private UserRepository repository;
#Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)throws Exception {
//Do stuff with the repository.
}
But the RoutingDataSource doesn't work, even if I set my Context before starting the job. For example if I set my database to B, the repo will work on database A.
It is always the default datasource that is selected. (because of this line
setDefaultTargetDataSource(datasourceA); )
I tried to set the database, by passing the value in the parameters, inside the tasklet, but still got the same issue.
#GetMapping("/batch")
public void startBatch() {
Map<String, JobParameter> parameters = new HashMap<>();
parameters.put("database", new JobParameter(DatabaseContextHolder.getCircaDatabase().toString()));
return jobLauncher.run("myJob", new JobParameters(parameters));
}
#Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext)throws Exception {
String database =
chunkContext.getStepContext().getStepExecution().getJobParameters().getString("database");
DatabaseContextHolder.setDatabase(Database.valueOf(database));
//Do stuff with the repository.
}
I feel like the problem is because the database was set in a different thread, because my job is asynchronous. So it cannot fetch the database set before launching the job. But I couldn't find any solution so far.
Regards
Your routing datasource is being used for Spring Batch's meta-data, which means the job repository will interact with a different database depending on the thread processing the request. This is not needed for batch jobs. You need to configure Spring Batch to work with a fixed data source.
I made my job class to consume data from DB. In addition to this I made the schedule class with interval 1 minutes for test. However the job is not triggering
#Component
#RequiredArgsConstructor
public class OfJob implements Job {
private final OfRepository ofRepository;
private final OfService ofService;
#Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
List<OfDomain> ofacList = ofRepository.findByPending(true);
ofList.forEach(ofDomain -> {
ofService.evaluate(ofDomain);
});
}
}
Config:
#Component
public class QuartsScheduleConfiguration {
#Bean
public Scheduler scheduler() throws SchedulerException {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail jobDetail = JobBuilder.newJob(OfJob.class).build();
SimpleTrigger simpleTrigger = newTrigger()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatForever().withIntervalInMinutes(1))
.build();
scheduler.scheduleJob(jobDetail, simpleTrigger);
scheduler.start();
return scheduler;
}
}
I have not error on my logs
Help me please
I have used scheduled annotation to schedule the task reading cronexpression from db. But when we change cron expression using UI , it needs to restart the app. How to inject TaskScheduler and ScheduledFuture or any other approach to reschedule without restarting when user update from UI which will call this method updateTestJobSchedule . Any example as per below code will be much helpful.
Currently i have : -
#Configuration
public class BasicConfig {
#Autowired
private TestJobSchedulerRepository testJobSchedulerRepository;
#Bean
public String getCronExpressionFromDb(){
return testJobSchedulerRepository.findByIsActive(1).getCronExpression();
}
}
#RestController
#EnableScheduling
#RequestMapping("/api")
public class TestJobController {
#Scheduled(cron="#{getCronExpressionFromDb}")
public void doTestJob(){
// does the job
}
// This update cron expression when request comes from UI
#RequestMapping(value = "/update-testjob-schedule_application",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> updateTestJobSchedule(#RequestBody
TestJobScheduler testJobScheduler) throws URISyntaxException {
if(testJobScheduler.getIsActive() == 0){
testJobScheduler.setIsActive(1);
} else{
testJobScheduler.setIsActive(0);
}
testJobSchedulerRepository.save(testJobScheduler);
return new ResponseEntity<>("{\"status\":\"success\"}", HttpStatus.OK);
}
}
Your best bet is to use the TaskScheduler object. The taskScheduler bean is automatically created when you have #EnableScheduling on a class annotated with #Configuration. This object can be used to schedule job on the fly. Here a quick exemple of what you can do with it :
#Configuration
#EnableScheduling
public class CronJobConfiguration implements CommandLineRunner {
#Autowired
private TestJobSchedulerRepository testJobSchedulerRepository;
#Autowired
private TaskScheduler taskScheduler;
#Override
public void run(String... strings) {
String cronExpression = testJobSchedulerRepository.findByIsActive(1).getCronExpression();
Trigger trigger = new CronTrigger(cronExpression);
ScheduledFuture scheduledFuture = taskScheduler
.schedule(() -> {/* the job you want to run*/}, trigger);
/* Keep the scheduledFuture to be able to cancel the job later Ex : */
scheduledFuture.cancel(true);
}
}
You'll probably need to keep the ScheduledFuture object returned by the schedule method to be able to cancel the job at some point. Also you'll need to have a service that use the TaskScheduler so that you can schedule job when the updateTestJobSchedule is called.
Ive being from sometime trying to setup a little program that uses Spring and Quartz together to schedule a task. I followed some other similar answers with no luck.
At the moment I think I have all configured correctly, I see no more exceptions but my job looks like its not kicking off.
In the log.out that Spring generates, I see the following messages at the end:
2015-06-04T15:46:57.928 DEBUG
[org.springframework.core.env.PropertySourcesPropertyResolver]
Searching for key 'spring.liveBeansView.mbeanDomain' in
[systemProperties] 2015-06-04T15:46:57.929 DEBUG
[org.springframework.core.env.PropertySourcesPropertyResolver]
Searching for key 'spring.liveBeansView.mbeanDomain' in
[systemEnvironment] 2015-06-04T15:46:57.929 DEBUG
[org.springframework.core.env.PropertySourcesPropertyResolver] Could
not find key 'spring.liveBeansView.mbeanDomain' in any property
source. Returning [null]
I will show you my codes...
This is the class from which I start the scheduler:
public class JobRunner {
public static void main(String[] args) throws SchedulerException {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(WhatsTheTimeConfiguration.class);
AutowiringSpringBeanJobFactory autowiringSpringBeanJobFactory = new AutowiringSpringBeanJobFactory();
autowiringSpringBeanJobFactory.setApplicationContext(applicationContext);
SpringBeanJobFactory springBeanJobFactory = new SpringBeanJobFactory();
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setTriggers(trigger());
schedulerFactoryBean.setJobFactory(springBeanJobFactory);
schedulerFactoryBean.start();
}
private static SimpleTrigger trigger() {
return newTrigger()
.withIdentity("whatsTheTimeJobTrigger", "jobsGroup1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever())
.build();
}
}
I want to mention that If I use the method schedulerFactoryBean.getScheduler().start(), it throws me a null pointer exception on the scheduler, so thats why im calling start() on the factory.
The class AutowiringSpringBeanJobFactory was copy pasted from another answer here in stackoverflow. I decided to do that since all other answers where I found something was only configuration done via xml and I don't want to use xml.
public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
ApplicationContextAware {
private transient AutowireCapableBeanFactory beanFactory;
#Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
#Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}
This is the class that represents the Job that I want to trigger:
#Component
public class WhatsTheTimeManager extends QuartzJobBean {
#Autowired
private WhatsTheTime usecase;
#Autowired
private LocationRetriever locationDataProvider;
public WhatsTheTimeManager() {
}
#Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
usecase.tellMeWhatsTheTimeIn(locationDataProvider.allLocations());
}
public void setUsecase(WhatsTheTime usecase) {
this.usecase = usecase;
}
public void setLocationDataProvider(LocationRetriever locationDataProvider) {
this.locationDataProvider = locationDataProvider;
}
}
My Spring configuration is doing component scanning, its very simple:
#Configuration
#ComponentScan(basePackages = "com.springpractice")
public class WhatsTheTimeConfiguration {
}
From this point everything I have are just some interfaces, components and a domain object, but I will paste them also, just in case I forgot something:
public interface LocationRetriever {
List<String> allLocations();
}
public interface TimeOutputRenderer {
TimeReport renderReport(String timeInLocation, String location);
}
public interface TimeRetriever {
String timeFor(String location);
}
#Component
public class LocationRetrieverDataProvider implements LocationRetriever{
public LocationRetrieverDataProvider() {
}
#Override
public List<String> allLocations() {
return asList("Europe/London", "Europe/Madrid", "Europe/Moscow", "Asia/Tokyo", "Australia/Melbourne", "America/New_York");
}
}
#Component
public class TimeOutputRendererDataProvider implements TimeOutputRenderer {
public TimeOutputRendererDataProvider() {
}
#Override
public TimeReport renderReport(String location, String time) {
System.out.println(location + " time is " + time);
return new TimeReport(location, time);
}
}
#Component
public class TimeRetrieverDataProvider implements TimeRetriever {
public TimeRetrieverDataProvider() {
}
#Override
public String timeFor(String location) {
SimpleDateFormat timeInLocation = new SimpleDateFormat("dd-M-yyyy hh:mm:ss a");
timeInLocation.setTimeZone(TimeZone.getTimeZone(location));
return timeInLocation.format(new Date());
}
}
Just one last detail, that maybe is of interest.
The versions I am using in my libraries are the following:
quartz 2.2.1
spring 4.1.6.RELEASE
When I run the appliaction, I expect the times of those countries to be printed every second, but it doesn't happen.
If you want to clone the code and try for yourself and see, you can find it at this git repo(Feel free to fork if you want): https://github.com/SFRJ/cleanarchitecture
The main error in your code is that you're not letting Spring handle the scheduling for you.
While you can use Quartz in code as any other code, the idea of the integration with Spring is to tell Spring about the work you want to be done and let Spring do the hard work for you.
In order to allow Spring to run the Quartz scheduling, you need to declare the Job, the JobDetail and the Trigger as Beans.
Spring only handles Beans if they are created through the Spring life-cycle (i.e. using annotations or XML) but not if the objects are created in code with a new statement.
The following code needs to be removed from JobRunner.java:
SpringBeanJobFactory springBeanJobFactory = new SpringBeanJobFactory();
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setTriggers(trigger());
schedulerFactoryBean.setJobFactory(springBeanJobFactory);
schedulerFactoryBean.start();
...
private static SimpleTrigger trigger() {
return newTrigger()
.withIdentity("whatsTheTimeJobTrigger", "jobsGroup1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever())
.build();
}
That code will have to be re-written into WhatsTheTimeConfiguration.java, and here's how it looks now:
#Configuration
#ComponentScan(basePackages = "com.djordje.cleanarchitecture")
public class WhatsTheTimeConfiguration {
#Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setTriggers(trigger());
schedulerFactoryBean.setJobDetails(jobDetail());
schedulerFactoryBean.setJobFactory(springBeanJobFactory());
return schedulerFactoryBean;
}
#Bean
public SpringBeanJobFactory springBeanJobFactory() {
return new AutowiringSpringBeanJobFactory();
}
#Bean
public JobDetail jobDetail() {
JobDetailImpl jobDetail = new JobDetailImpl();
jobDetail.setKey(new JobKey("WhatsTheTime"));
jobDetail.setJobClass(WhatsTheTimeManager.class);
jobDetail.setDurability(true);
return jobDetail;
}
#Bean
public SimpleTrigger trigger() {
return newTrigger()
.forJob(jobDetail())
.withIdentity("whatsTheTimeJobTrigger", "jobsGroup1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(1)
.repeatForever())
.build();
}
}
SchedulerFactoryBean is now a Bean and will be handled and initialized by Spring, and so are SimpleTrigger and AutowiringSpringBeanJobFactory.
I added the missing JobDetail class which was missing and added the necessary wiring to SimpleTrigger and SchedulerFactoryBean. They both need to know about JobDetail which is the only place that knows which class is the job class that needs to be triggered.
In an application, since I converted it from a classical Spring webapp (deployed in a system Tomcat) to a Spring Boot (V1.2.1) application I face the problem that the Quartz-based scheduled jobs are not working anymore.
I schedule these Quartz jobs like this:
// My own Schedule object which holds data about what to schedule when
Schedule schedule = scheduleService.get(id of the schedule);
String scheduleId = schedule.getId();
JobKey jobKey = new JobKey(scheduleId);
TriggerKey triggerKey = new TriggerKey(scheduleId);
JobDataMap jobData = new JobDataMap();
jobData.put("scheduleId", scheduleId);
JobBuilder jobBuilder = JobBuilder.newJob(ScheduledActionRunner.class)
.withIdentity(jobKey)
.withDescription(schedule.getName())
.usingJobData(jobData);
JobDetail job = jobBuilder.build();
TriggerBuilder triggerBuilder = TriggerBuilder.newTrigger()
.forJob(jobKey)
.withIdentity(triggerKey)
.withDescription(schedule.getName());
triggerBuilder = triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(schedule.toCronExpression()));
Trigger trigger = triggerBuilder.build();
org.quartz.Scheduler scheduler = schedulerFactoryBean.getScheduler();
scheduler.scheduleJob(job, trigger);
ScheduledActionRunner:
#Component
public class ScheduledActionRunner extends QuartzJobBean {
#Autowired
private ScheduleService scheduleService;
public ScheduledActionRunner() {
}
#Override
public void executeInternal(final JobExecutionContext context) throws JobExecutionException {
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
final JobDataMap jobDataMap = context.getMergedJobDataMap();
final String scheduleId = jobDataMap.getString("scheduleId");
final Schedule schedule = scheduleService.get(scheduleId);
// here it goes BANG since scheduleService is null
}
}
ScheduleService is a classical Spring service which fetches data from Hibernate.
As I said above, this worked fine until I moved to Spring Boot.
When I implemented this code with the classical Spring application, SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this); did the trick to take care of autowiring the service.
What is needed to make this work again in the Spring Boot environment ?
Edit:
At the end I chose to move away from using Quartz in favour of Spring's ThreadPoolTaskScheduler.The code was much simplified and it works as expected.
The SpringBeanAutowiringSupport uses the web application context, which is not available in your case. If you need a spring managed beans in the quartz you should use the quartz support provided by spring. This will give you full access to all the managed beans.
For more info see the quartz section at spring docs at http://docs.spring.io/spring/docs/current/spring-framework-reference/html/scheduling.html. Also see following example of usage quartz with spring managed beans. Example is based on your code.
So you can change the first code snippet (where the quartz initialization is done) with follwoing spring alternatives.
Create job detail factory
#Component
public class ScheduledActionRunnerJobDetailFactory extends JobDetailFactoryBean {
#Autowired
private ScheduleService scheduleService;
#Override
public void afterPropertiesSet() {
setJobClass(ScheduledActionRunner.class);
Map<String, Object> data = new HashMap<String, Object>();
data.put("scheduleService", scheduleService);
setJobDataAsMap(data);
super.afterPropertiesSet();
}
}
Create the trigger factory
#Component
public class ActionCronTriggerFactoryBean extends CronTriggerFactoryBean {
#Autowired
private ScheduledActionRunnerJobDetailFactory jobDetailFactory;
#Value("${cron.pattern}")
private String pattern;
#Override
public void afterPropertiesSet() throws ParseException {
setCronExpression(pattern);
setJobDetail(jobDetailFactory.getObject());
super.afterPropertiesSet();
}
}
And finally create the SchedulerFactory
#Component
public class ActionSchedulerFactoryBean extends SchedulerFactoryBean {
#Autowired
private ScheduledActionRunnerJobDetailFactory jobDetailFactory;
#Autowired
private ActionCronTriggerFactoryBean triggerFactory;
#Override
public void afterPropertiesSet() throws Exception {
setJobDetails(jobDetailFactory.getObject());
setTriggers(triggerFactory.getObject());
super.afterPropertiesSet();
}
}
My answer not fully matches to you question, but Spring expose you another ability - to start cron-expression based scheduler on any service.
Using Spring.Boot you can configure your application to use scheduler by simple placing
#EnableScheduling
public class Application{
....
After that just place following annotation on public(!) method of #Service
#Service
public class MyService{
...
#Scheduled(cron = "0 * * * * MON-FRI")
public void myScheduledMethod(){
....
}