Quartz Scheduler with SpringBoot (Phần 1)
In the previous post, Quartz Scheduler Introduction we learned the basics of the Quartz subsystem with plain java. In this post, We will use spring boot magic to create an application with Quartz.
This application will have the following.
- An endpoint, to show current items in the system.
- A quartz job, to keep adding a new item at a regular interval.
Before we start with quartz, let's do some basic SpringBoot setup
1. Maven Project:
Create a maven project the way you like, Either by using your favorite IDE or by command line or by spring-starter. just keep the name of your project as QuartzSpringApplication If you do not want to modify any code provided in this article After that add the following dependencies in your pom.xml. Lombok is not needed, but I like to use it everywhere.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Note: If you are using spring-starter then you can add these dependencies at these at the beginning.
2. Main class:
Create a main class containing the main method and SpringBoot annotation. Copy the code mentioned below
@SpringBootApplication
public class QuartzSpringApplication {
public static void main(String[] args {
SpringApplication.run(QuartzSpringApplication.class, args);
}
}
By performing the below 3 steps, we made sure that SpringBoot will spin up a Netty server upon application startup. And it will also autoconfigure a few opinionated beans that we are going to create in just a moment
- Added @SpringBootApplication annotation on the main class.
- Called the static SpringApplicatio.run function from the main method.
- Added webFlux dependency in pom.xml in the previous step,
3. Model class:
This will be used to store/transfer data.
@Getter
@Setter
@AllArgsConstructorpublic
class Book {
private UUID id;
private String name;
private String description;
private String authorName;
private BigDecimal cost;
}
Note about the Model:
This is NOT how a model class should look like. In a real-world application, you should always use Objects, not scaler values to represent data.
For example, Cost and Author should be custom objects, not String and BigDecimal, but for the sake of this blog, I will go simple.
4. Repository class:
To keep things focused only on Quartz, I’m not using any database. Our Repository class will contain.
- A property List<Book>, that will be used as a Database.
- A method getAllBooks, will be used by the Controller to read data
- A method addBook, will be used by Quartz job to save a new book at regular intervals.
@Component
public class BookRepository { private final List<Book> books = new ArrayList<>(); public List<Book> getAllBooks(){
return books;
}
public void addBook(Book book){
books.add(book);
}
public int getBooksCount(){
return books.size();
}
}
5. Controller class:
This will be used to verify Quartz's work.
Let’s create a RestController that has an endpoint to return the List of Book.
@RestController
@RequestMapping("/books")
public class BooksController { private final BookRepository bookRepository; public BooksController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
} @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE)
List<Book> getBooks() {
return bookRepository.getAllBooks();
}
}
NOTE:
Unused webFlux dependency
You might be wondering, why I added webFlux dependency if I’m returning just a List not a Flux. The reason is, halfway through the blog I realized, that Flux opens an Iterator on the list. So I can not modify that list
Explanation
For our example, I used the List-as-a- database in the Repository class. I can’t open a Flux on this List because it is simultaneously being modified in another thread spawned by Quartz (Quartz code will be in step 6). Iterators are fail-fast so that code will throw a ConcurrentModificationException
That's why for now, let’s settle on returning a List from the RestController, I will cover Flux in the next blog with some reactive DB.
Note: Until now, We have not done anything related to quartz, except adding a dependency in pom.xml, from here after we will code only Quartz components.
In the previous blog, we had to explicitly instantiate various Quartz-related objects. but with Spring magic, We need to create only Factory instances, Spring will manage bean creations.
However, we still need to create a Job class, similar to the previous post.
6. Job class:
For our example, this job class will use BookRepository that we created above. Every time it runs it will add one Book to the database.
@Slf4j
@Component
public class LoadBookJob implements Job { final BookRepository bookRepository; public LoadBookJob(BookRepository bookRepository) {
this.bookRepository = bookRepository;
} @Override
public void execute(JobExecutionContext context)
throws JobExecutionException
{ int booksCounter = bookRepository.getBooksCount() + 1; log.info("Adding book number {} ", booksCounter); bookRepository
.addBook(new Book(UUID.randomUUID(),
"name" + booksCounter,
"description" + booksCounter,
"author" + booksCounter,
BigDecimal.valueOf(booksCounter + 100)));
}
}
7. QuartzConfiguration class:
The last class will be a Configuration class. As mentioned earlier, with Spring, we only need to provide FactoryBeans for all types.
So let's create a QuartzConfig class.
For now, we will add only 2 @Bean methods, for the following Beans
- JobDetailFactoryBean
At startup, spring will use this to create JobDetail beans, to pass to trigger the creation - SimpleTriggerFactoryBean At startup spring will use this to create Triggers to pass to SchedulerFactoryBean creation
@Slf4j
@Configuration
public class QuartzConfig { /**
----------------------------
To complete this config class
we will add some more code at this location.
First look at the below lines and understand
----------------------------
**/
@Bean
public SimpleTriggerFactoryBean
createSimpleTriggerFactoryBean(JobDetail jobDetail)
{
SimpleTriggerFactoryBean simpleTriggerFactory
= new SimpleTriggerFactoryBean();
simpleTriggerFactory.setJobDetail(jobDetail);
simpleTriggerFactory.setStartDelay(0);
simpleTriggerFactory.setRepeatInterval(5000);
simpleTriggerFactory.setRepeatCount(10);
return simpleTriggerFactory;
} @Bean
public JobDetailFactoryBean createJobDetailFactoryBean(){
JobDetailFactoryBean jobDetailFactory
= new JobDetailFactoryBean();
jobDetailFactory.setJobClass(LoadBookJob.class);
return jobDetailFactory;
}
}
Now, all we are missing in the spring context is an implementation of Quartz SchedulerFactory. For our example, I will use spring’s implementation SchedulerFactoryBea
Add the below code in the above class QuartzConfig. The Below code deserves some more explanation so I kept it separate at the end, look into it only if you completely understand all the above explanations.
final ApplicationContext applicationContext;
public QuartzConfig(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Bean
SpringBeanJobFactory createSpringBeanJobFactory (){
return new SpringBeanJobFactory() {
@Override
protected Object createJobInstance
(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
applicationContext
.getAutowireCapableBeanFactory()
.autowireBean(job);
return job;
}
};
}@Bean
public SchedulerFactoryBean createSchedulerFactory
(SpringBeanJobFactory springBeanJobFactory,Trigger trigger) {
SchedulerFactoryBean schedulerFactory
= new SchedulerFactoryBean(); schedulerFactory.setAutoStartup(true);
schedulerFactory.setWaitForJobsToCompleteOnShutdown(true);
schedulerFactory.setTriggers(trigger);
springBeanJobFactory.setApplicationContext(applicationContext);
schedulerFactory.setJobFactory(springBeanJobFactory);
return schedulerFactory;
}
Explanation of the above code snippet:
Every time a trigger is fired, Scheduler creates a new instance of the Job bean.
In our Job class, I Autowired BookRepository. So, our Scheduler needs to have the capability to weave the beans with spring context. Some special handling is required with JobFactory for this.
You do not need this if your Job class is not Autowiring any bean. But If you are using spring most probably you will have Beans wired into Job class.
When we use quartz with spring, we do not explicitly create any Scheduler instance, we create only SchedulerFactoryBean and spring implicitly uses it to create Scheduler.
So, I created a method createSpringBeanJobFactory which creates a SpringBeanJobFactory bean. Thereafter I add this JobFactory bean to our SchedulerFactoryBeanin the createSchedulerFactory method.
When Spring will use SchedulerFactoryBean to create Scheduler, SchedulerFactoryBean will set this SpringBeanJobFactory in the Scheduler.
Then upon each trigger fire, createJobInstance of this SpringBeanJobFactory will be called and you can see in the code, I’m explicitly weaving the beans from applicationContext.
Moment of Truth:
Finally, we are done with coding.
Just start the application, a Netty server will be started and our Quartz job will keep adding a new Book every 2 seconds. You can open the URL in the browser and keep refreshing to check the progress.
Please provide your feedback by commenting/applauding on this post. In the next blog, we’ll enhance the same application to add a reactive Database driver and Flux response.
No comments:
Post a Comment