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 themain
class. - Called the static
SpringApplicatio.run
function from the main method. - Added
webFlux
dependency inpom.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<Books>
, 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<Books> books = new ArrayList<>(); public List<Books> 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 Books.
@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 addedwebFlux
dependency if I’m returning just aList
not aFlux
. The reason is, halfway through the blog I realized, thatFlux
opens anIterator
on the list. So I can not modify that listExplanation
For our example, I used the List-as-a- database in the Repository class. I can’t open aFlux
on thisList
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 aConcurrentModificationException
That's why for now, let’s settle on returning a
List
from theRestController
, 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 creationSimpleTriggerFactoryBean
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 SchedulerFactoryBean
in 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.
Comments