diff --git a/spring-quartz/pom.xml b/spring-quartz/pom.xml index 171f7ffd1064..fefbb2643ad9 100644 --- a/spring-quartz/pom.xml +++ b/spring-quartz/pom.xml @@ -34,6 +34,11 @@ org.springframework.boot spring-boot-starter-jdbc + + org.springframework.boot + spring-boot-starter-data-jpa + + org.springframework @@ -64,6 +69,15 @@ true + + + org.springframework.boot + spring-boot-maven-plugin + + org.baeldung.springquartz.SpringQuartzApp + + + diff --git a/spring-quartz/src/main/java/org/baeldung/recovery/SpringQuartzRecoveryApp.java b/spring-quartz/src/main/java/org/baeldung/recovery/SpringQuartzRecoveryApp.java new file mode 100644 index 000000000000..a7f734b74763 --- /dev/null +++ b/spring-quartz/src/main/java/org/baeldung/recovery/SpringQuartzRecoveryApp.java @@ -0,0 +1,18 @@ +package org.baeldung.recovery; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableScheduling; + +@ComponentScan +@EnableScheduling +@SpringBootApplication +public class SpringQuartzRecoveryApp { + + public static void main(String[] args) { + SpringApplication app = new SpringApplication(SpringQuartzRecoveryApp.class); + app.setAdditionalProfiles("recovery"); + app.run(args); + } +} diff --git a/spring-quartz/src/main/java/org/baeldung/recovery/config/QuartzConfig.java b/spring-quartz/src/main/java/org/baeldung/recovery/config/QuartzConfig.java new file mode 100644 index 000000000000..0bdc0d0e6b26 --- /dev/null +++ b/spring-quartz/src/main/java/org/baeldung/recovery/config/QuartzConfig.java @@ -0,0 +1,31 @@ +package org.baeldung.recovery.config; + +import org.quartz.CronScheduleBuilder; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuartzConfig { + + @Bean + public JobDetail sampleJobDetail() { + return JobBuilder.newJob(SampleJob.class) + .withIdentity("sampleJob", "group1") + .storeDurably() + .requestRecovery(true) + .build(); + } + + @Bean + public Trigger sampleTrigger(JobDetail sampleJobDetail) { + return TriggerBuilder.newTrigger() + .forJob(sampleJobDetail) + .withIdentity("sampleTrigger", "group1") + .withSchedule(CronScheduleBuilder.cronSchedule("0/30 * * * * ?")) // every 30s + .build(); + } +} diff --git a/spring-quartz/src/main/java/org/baeldung/recovery/config/SampleJob.java b/spring-quartz/src/main/java/org/baeldung/recovery/config/SampleJob.java new file mode 100644 index 000000000000..0c24b1a3edeb --- /dev/null +++ b/spring-quartz/src/main/java/org/baeldung/recovery/config/SampleJob.java @@ -0,0 +1,11 @@ +package org.baeldung.recovery.config; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; + +public class SampleJob implements Job { + @Override + public void execute(JobExecutionContext context) { + System.out.println("Executing SampleJob at " + System.currentTimeMillis()); + } +} diff --git a/spring-quartz/src/main/java/org/baeldung/recovery/custom/ApplicationJob.java b/spring-quartz/src/main/java/org/baeldung/recovery/custom/ApplicationJob.java new file mode 100644 index 000000000000..71c1b8ca6c40 --- /dev/null +++ b/spring-quartz/src/main/java/org/baeldung/recovery/custom/ApplicationJob.java @@ -0,0 +1,49 @@ +package org.baeldung.recovery.custom; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class ApplicationJob { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + private boolean enabled; + private Boolean completed; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Boolean getCompleted() { + return completed; + } + + public void setCompleted(Boolean completed) { + this.completed = completed; + } +} diff --git a/spring-quartz/src/main/java/org/baeldung/recovery/custom/ApplicationJobRepository.java b/spring-quartz/src/main/java/org/baeldung/recovery/custom/ApplicationJobRepository.java new file mode 100644 index 000000000000..4f717152cadb --- /dev/null +++ b/spring-quartz/src/main/java/org/baeldung/recovery/custom/ApplicationJobRepository.java @@ -0,0 +1,9 @@ +package org.baeldung.recovery.custom; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ApplicationJobRepository extends JpaRepository { + +} diff --git a/spring-quartz/src/main/java/org/baeldung/recovery/custom/DataSeeder.java b/spring-quartz/src/main/java/org/baeldung/recovery/custom/DataSeeder.java new file mode 100644 index 000000000000..d3d137d5a43d --- /dev/null +++ b/spring-quartz/src/main/java/org/baeldung/recovery/custom/DataSeeder.java @@ -0,0 +1,26 @@ +package org.baeldung.recovery.custom; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +@Component +public class DataSeeder implements CommandLineRunner { + + private final ApplicationJobRepository repository; + + public DataSeeder(ApplicationJobRepository repository) { + this.repository = repository; + } + + @Override + public void run(String... args) { + if (repository.count() == 0) { + ApplicationJob job = new ApplicationJob(); + job.setName("simpleJob"); + job.setEnabled(true); + job.setCompleted(false); + + repository.save(job); + } + } +} diff --git a/spring-quartz/src/main/java/org/baeldung/recovery/custom/JobInitializer.java b/spring-quartz/src/main/java/org/baeldung/recovery/custom/JobInitializer.java new file mode 100644 index 000000000000..6948cd70e321 --- /dev/null +++ b/spring-quartz/src/main/java/org/baeldung/recovery/custom/JobInitializer.java @@ -0,0 +1,49 @@ +package org.baeldung.recovery.custom; + +import org.baeldung.recovery.config.SampleJob; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +@Component +public class JobInitializer implements ApplicationListener { + + @Autowired + private ApplicationJobRepository jobRepository; + + @Autowired + private Scheduler scheduler; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + for (ApplicationJob job : jobRepository.findAll()) { + if (job.isEnabled() && (job.getCompleted() == null || !job.getCompleted())) { + JobDetail detail = JobBuilder.newJob(SampleJob.class) + .withIdentity(job.getName(), "appJobs") + .storeDurably() + .build(); + + Trigger trigger = TriggerBuilder.newTrigger() + .forJob(detail) + .withSchedule(SimpleScheduleBuilder.simpleSchedule() + .withIntervalInSeconds(30) + .repeatForever()) + .build(); + + try { + scheduler.scheduleJob(detail, trigger); + } catch (SchedulerException e) { + throw new RuntimeException(e); + } + } + } + } +} diff --git a/spring-quartz/src/main/resources/application-recovery.properties b/spring-quartz/src/main/resources/application-recovery.properties new file mode 100644 index 000000000000..22a5de2d94f6 --- /dev/null +++ b/spring-quartz/src/main/resources/application-recovery.properties @@ -0,0 +1,12 @@ +spring.quartz.job-store-type=jdbc +# Always create the Quartz database on startup +spring.quartz.jdbc.initialize-schema=always + +spring.datasource.jdbc-url=jdbc:h2:~/quartz-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.hibernate.ddl-auto=create + +spring.h2.console.enabled=true \ No newline at end of file diff --git a/spring-quartz/src/test/java/org/baeldung/recovery/SpringQuartzRecoveryAppUnitTest.java b/spring-quartz/src/test/java/org/baeldung/recovery/SpringQuartzRecoveryAppUnitTest.java new file mode 100644 index 000000000000..1dc4d8ada74c --- /dev/null +++ b/spring-quartz/src/test/java/org/baeldung/recovery/SpringQuartzRecoveryAppUnitTest.java @@ -0,0 +1,54 @@ +package org.baeldung.recovery; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.quartz.JobDetail; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.Trigger; +import org.quartz.TriggerKey; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("recovery") +@SpringBootTest(classes = SpringQuartzRecoveryApp.class) +class SpringQuartzRecoveryAppUnitTest { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private Scheduler scheduler; + + @Test + void givenSampleJob_whenSchedulerRestart_thenSampleJobIsReloaded() throws Exception { + // Given + JobKey jobKey = new JobKey("sampleJob", "group1"); + TriggerKey triggerKey = new TriggerKey("sampleTrigger", "group1"); + + JobDetail jobDetail = scheduler.getJobDetail(jobKey); + assertNotNull(jobDetail, "SampleJob exists in running scheduler"); + + Trigger trigger = scheduler.getTrigger(triggerKey); + assertNotNull(trigger, "SampleTrigger exists in running scheduler"); + + // When + scheduler.standby(); + Scheduler restartedScheduler = applicationContext.getBean(Scheduler.class); + restartedScheduler.start(); + + // Then + assertTrue(restartedScheduler.isStarted(), "Scheduler should be running after restart"); + + JobDetail reloadedJob = restartedScheduler.getJobDetail(jobKey); + assertNotNull(reloadedJob, "SampleJob should be reloaded from DB after restart"); + + Trigger reloadedTrigger = restartedScheduler.getTrigger(triggerKey); + assertNotNull(reloadedTrigger, "SampleTrigger should be reloaded from DB after restart"); + } + +}