Injecting CDI managed beans into Quarz jobs
Quarz Job Scheduler
If you have worked with enterprise applications in the past you have most likely at some point in time needed to schedule some jobs to run at either some interval or possibly at some specific point in time. One of the go-to solutions to do this has for a long time been using the Quarz Job Scheduler.
Here is an example of a typical Quarz job you might create:
class MyJob implements Job {
@Override
void execute(JobExecutionContext context){
// Execute some code
}
}
and then in your application you would schedule it pretty much like this
class MyApp {
void startMyApp() {
// Get scheduler and start it
def scheduler = StdSchedulerFactory.defaultScheduler
scheduler.start()
// Create a QuarzJob to run
def job = JobBuilder.newJob(MyJob).build()
// Create a Trigger to trigger the job every five minutes
def runEveryFiveMinutes = CronScheduleBuilder.cronSchedule('0 0/5 * 1/1 * ? *')
def trigger = TriggerBuilder.newTrigger()
.withSchedule(runEveryFiveMinutes)
.forJob(job)
.build()
// Register Job and Trigger with the scheduler
scheduler.scheduleJob(job, trigger)
}
}
CDI
Another technology you might have come across with is CDI, or JSR 299 as it also is called. What it does is basically allow you to inject, say a service, directly into a bean, without needing to manage its lifecycle (meaning you do not need to construct the service object, nor clean it up yourself, it is managed by CDI semi-automatically). This is usually done to decouple the actual implementation from the interface and allows the clients to only care about the interfaces.
A typical example of using this is when implementing a service like this:
interface MyService {
List<Person> getPersons()
}
@ApplicationScoped
class MyServiceImpl implements MyService {
@PostConstruct
void init() {
// Setup database connection
}
@PreDestroy
void tearDown() {
// Close database connection
}
@Override
List<Person> getPersons() {
// Retrieve persons from database
}
}
And the way you usually get the service in your app is
@ApplicationScoped
class MyApp {
@Inject
MyService service
@PostConstruct
void startMyApp() {
// Print all names of the registered persons
service.persons.each { println it.name }
}
The problem
Now that we know the technologies, say we want to use MyService inside our Quarz MyJob.
Following the above example, we would write the following job and schedule it the way we did above:
class MyJob implements Job {
@Inject
MyService service
@Override
void execute(JobExecutionContext context){
// Print all names of the registered persons every 5 minutes
service.persons.each { println it.name }
}
}
But, when the scheduler would run the job it would not print each persons name, but instead throw a NullPointerException stating that service is null!
Why is that?
For CDI to work it needs to manage the whole bean creation chain so it can inject the correct instances into the beans. But when we create a Quarz job using JobBuilder.newJob(MyJob).build()
the JobBuilder is creating job instances that CDI does not know anything about and so never has a chance to inject the service into the job.
The solution
So how do we tell the Quarz scheduler that it should not create the bean instance itself, but, instead delegate the creation of the bean instance to CDI?
To do that we need to create our own factory for creating jobs with CDI.
@ApplicationScoped
class CDIJobFactory implements JobFactory {
@Inject
BeanManager beanManager
@Override
Job newJob(TriggerFiredBundle bundle, Scheduler scheduler) {
def jobClazz = bundle.jobDetail.jobClass
def bean = beanManager.getBeans(jobClazz).first()
def ctx = beanManager.createCreationalContext(bean)
beanManager.getReference(bean, jobClazz, ctx)
}
}
What this factory will do is use the CDI BeanManager to create the job instances with. This way, the instances will be properly managed by CDI and we can use CDI injection inside the jobs.
So how do we use this factory to create the jobs with, lets combine both MyApp implementations from the Quarz example and the CDI example into one application and use our job factory to create the project. It would look like this:
@ApplicationScoped
class MyApp {
@Inject
CDIJobFactory jobFactory
void startMyApp() {
// Get scheduler and start it
def scheduler = StdSchedulerFactory.defaultScheduler
// Use the CDI managed job factory
scheduler.jobFactory = jobFactory
// Start scheduler
scheduler.start()
// Create a QuarzJob to run
def job = JobBuilder.newJob(MyJob).build()
// Create a Trigger to trigger the job every five minutes
def runEveryFiveMinutes = CronScheduleBuilder.cronSchedule('0 0/5 * 1/1 * ? *')
def trigger = TriggerBuilder.newTrigger()
.withSchedule(runEveryFiveMinutes)
.forJob(job)
.build()
// Register Job and Trigger with the scheduler
scheduler.scheduleJob(job, trigger)
}
}
Now, the JobBuilder will use our CDIJobFactory to create the MyJob and the service will be injected properly into the instance for the execute method to use.
A word about scopes
In this example I have used the @ApplicationScope extensively to denote that the beans we create should always exist as long as the application is running. However, if you are building a web application you might have beans that have @SessionScope and will only live for the duration of the HTTP session. These beans won't work with Quarz jobs as nothing guarantees that there exists a HTTP Session when the Quarz trigger runs.