Fixing Spring Boot Quartz Integration: JobFactory and Autowiring Explained
After discovering misconfigurations in a Spring Boot project’s QuartzJobBean implementation, this article examines the root causes, compares legacy and modern Spring solutions—including custom JobFactory, AutowireCapableBeanFactory, and the spring-boot-starter-quartz starter—and provides detailed code examples to correctly integrate Quartz with Spring’s IoC container.
Problem Discovery
While reviewing an older Spring Boot project, the author noticed that the QuartzJobBean implementation ( UnprocessedTaskJob) was annotated with @Component and injected a TaskMapper via @Autowired. This caused a conflict because Quartz creates a new job instance for each trigger, while Spring treats the job class as a singleton bean.
Root Cause
The conflict arises from the fact that Quartz instantiates job classes via reflection, ignoring Spring's IoC container. When the job class is also a Spring singleton, the container tries to inject dependencies that are not available in the Quartz‑created instance, leading to runtime errors.
Legacy Solution (Spring 4.x / Quartz 2.1.4)
To bridge the gap, a custom JobFactory is introduced. The classic approach extends SpringBeanJobFactory and implements ApplicationContextAware so that the created job instance can be autowired.
public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(ApplicationContext context) {
this.beanFactory = context.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}The factory is then registered with the scheduler:
@Bean
public JobFactory jobFactory(ApplicationContext ctx) {
AutowiringSpringBeanJobFactory factory = new AutowiringSpringBeanJobFactory();
factory.setApplicationContext(ctx);
return factory;
}
@Bean
public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource ds) {
SchedulerFactoryBean bean = new SchedulerFactoryBean();
bean.setJobFactory(jobFactory);
bean.setDataSource(ds);
bean.setOverwriteExistingJobs(true);
bean.setAutoStartup(true);
bean.setStartupDelay(2);
return bean;
}New Solution (Spring Boot 2.x+)
Spring Boot provides the spring-boot-starter-quartz starter, which auto‑configures a SchedulerFactoryBean and a default SpringBeanJobFactory. The newer SchedulerFactoryBean already implements ApplicationContextAware, so explicit factory beans are often unnecessary.
implementation "org.springframework.boot:spring-boot-starter-quartz"When the starter is on the classpath, all @JobDetail, @Trigger, and @Bean definitions are automatically registered with the scheduler. Autowiring works out‑of‑the‑box as long as the job class is a Spring component.
SpringBeanJobFactory Deep Dive
The SpringBeanJobFactory extends AdaptableJobFactory and implements both ApplicationContextAware and SchedulerContextAware. Its createJobInstance method first creates the job via reflection, then populates it with properties from the scheduler context, job data map, and trigger data map. If an ApplicationContext is present, it uses the AutowireCapableBeanFactory to perform constructor injection, enabling full Spring dependency injection.
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object job = (applicationContext != null) ?
applicationContext.getAutowireCapableBeanFactory()
.createBean(bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false)
: super.createJobInstance(bundle);
// Populate properties from scheduler context and job data maps
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);
MutablePropertyValues pvs = new MutablePropertyValues();
if (schedulerContext != null) pvs.addPropertyValues(schedulerContext);
pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());
pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());
bw.setPropertyValues(pvs, true);
return job;
}AutowireCapableBeanFactory Role
When a bean is not managed by Spring (e.g., a Quartz‑created job), the AutowireCapableBeanFactory can inject dependencies manually. By calling beanFactory.autowireBean(job), the job instance receives all @Autowired fields, allowing it to interact with other Spring‑managed services such as repositories or mappers.
Conclusion
For legacy Spring versions, a custom JobFactory (often extending SpringBeanJobFactory) is required to bridge Quartz and Spring IoC. In modern Spring Boot applications, the starter handles most configuration automatically, but understanding the underlying JobFactory and AutowireCapableBeanFactory mechanisms is valuable for advanced scenarios or when fine‑grained control is needed.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
