# 解决 Quartz Job 中无法注入 Spring Bean

# 问题

在 Spring 集成 Quartz 的时候有没有遇到过这样一个问题,就是在 Quartz 的 Job 中依赖注入的时候报空指针异常。如果在 Spring 中无法使用 @Autowired 进行注入一个 Bean 的话,这无疑是一个噩耗,那么遇到这个问题,该如何解决呢?

# 原因

出现这个问题是因为定时任务的 Job 对象实例化的过程是通过 Quartz 内部自己完成的,但是我们通过 Spring 进行注入的 Bean 却是由 Spring 容器管理的,Quartz 内部无法感知到 Spring 容器管理的 Bean,所以没有办法在创建 Job 的时候就给装配进去。

# 源码分析

传统的 Spring 项目,我们可以看到 Schedule 的创建是通过 SchedulerFactoryBean 进行创建的,我们看一下 SchedulerFactoryBean 源码,该类实现了 InitializingBean 接口,会在 Bean 属性初始化之后调用 afterPropertiesSet() 方法

afterPropertiesSet()方法
@Override
public void afterPropertiesSet() throws Exception {
   // 省略部分代码...
   // Initialize the Scheduler instance...
   this.scheduler = prepareScheduler(prepareSchedulerFactory());
   // 省略部分代码...
}

接着查看 prepareScheduler() 方法,可以发现如果 jobFactory 不存在的话,默认会使用 AdaptableJobFactory 实现对 Job 对象的创建。

prepareSchedule()
private Scheduler prepareScheduler(SchedulerFactory schedulerFactory) throws SchedulerException {
   // 省略部分代码...
   // Get Scheduler instance from SchedulerFactory.
   try {
      Scheduler scheduler = createScheduler(schedulerFactory, this.schedulerName);
      populateSchedulerContext(scheduler);
      if (!this.jobFactorySet && !(scheduler instanceof RemoteScheduler)) {
         // Use AdaptableJobFactory as default for a local Scheduler, unless when
         // explicitly given a null value through the "jobFactory" bean property.
         this.jobFactory = new AdaptableJobFactory();
      }
      if (this.jobFactory != null) {
         if (this.applicationContext != null && this.jobFactory instanceof ApplicationContextAware) {
            ((ApplicationContextAware) this.jobFactory).setApplicationContext(this.applicationContext);
         }
         if (this.jobFactory instanceof SchedulerContextAware) {
            ((SchedulerContextAware) this.jobFactory).setSchedulerContext(scheduler.getContext());
         }
         scheduler.setJobFactory(this.jobFactory);
      }
      return scheduler;
   }
   // 省略部分代码...
}

既然找到了源码,那么处理起来就方便了,我们如果可以自定义 JobFactory 的话,在创建完 Job 实例之后,再将 Job 注入到 Spring 容器中即可解决该问题。

# 解决

# 自定义 JobFactory

首先自定义一个 JobFactory,通过 AutowireCapableBeanFactory 将创建好的 Job 对象交给 Spring 管理

自定义JobFactory
@Configuration
public class CustomJobFactory extends AdaptableJobFactory {
    @Autowired
    private AutowireCapableBeanFactory autowireCapableBeanFactory;
    /**
     * Create the job instance, populating it with property values taken
     * from the scheduler context, job data map and trigger data map.
     *
     * @param bundle
     */
    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        Object jobInstance = super.createJobInstance(bundle);
        autowireCapableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

再创建一个配置类,将自定义的 JobFactory 设置到 Schedule

config
@Configuration
public class QuartzConfig {
    @Autowired
    private CustomJobFactory customJobFactory;
    @SneakyThrows
    @Bean
    public Scheduler scheduler(){
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 自定义 JobFactory 使得在 Quartz Job 中可以使用 @Autowired
        scheduler.setJobFactory(customJobFactory);
        scheduler.start();
        return scheduler;
    }
    
}

这样你就可以愉快的在定时任务中使用 @Autowired 注入 Spring 管理的 Bean 了。

@Slf4j
public class JobDemo2 implements Job {
    @Autowired
    private DemoService demoService;
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        demoService.echo("JobDemo2");
    }
}

注:此方法原理上可行,但是我在实现上依赖还是注入不了

# 通过 SpringUtil 实现

创建一个 Spring 工具类

Spring 工具类
/**
 * <p>
 * Spring 工具类
 * </p>
 *
 * @author yangkai.shen
 * @date Created in 2020-06-05 11:13
 */
@Slf4j
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        SpringUtil.context = context;
    }
    /**
     * 获取 Spring Bean
     *
     * @param clazz 类
     * @param <T>   泛型
     * @return 对象
     */
    public static <T> T getBean(Class<T> clazz) {
        if (clazz == null) {
            return null;
        }
        return context.getBean(clazz);
    }
    /**
     * 获取 Spring Bean
     *
     * @param bean 名称
     * @param <T>  泛型
     * @return 对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String bean) {
        if (bean == null) {
            return null;
        }
        return (T) context.getBean(bean);
    }
    /**
     * 获取 Spring Bean
     *
     * @param beanName 名称
     * @param clazz    类
     * @param <T>      泛型
     * @return 对象
     */
    public static <T> T getBean(String beanName, Class<T> clazz) {
        if (null == beanName || "".equals(beanName.trim())) {
            return null;
        }
        if (clazz == null) {
            return null;
        }
        return (T) context.getBean(beanName, clazz);
    }
    /**
     * 获取上下文
     *
     * @return 上下文
     */
    public static ApplicationContext getContext() {
        if (context == null) {
            throw new RuntimeException("There has no Spring ApplicationContext!");
        }
        return context;
    }
    /**
     * 发布事件
     *
     * @param event 事件
     */
    public static void publishEvent(ApplicationEvent event) {
        if (context == null) {
            return;
        }
        try {
            context.publishEvent(event);
        } catch (Exception ex) {
            log.error(ex.getMessage());
        }
    }
}

然后在定时任务中通过该工具类获取 Spring Bean,也可以实现同样的效果。

springUtils
@Slf4j
public class JobDemo1 implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        DemoService demoService = SpringUtil.getBean(DemoService.class);
        demoService.echo("JobDemo1");
    }
}

# Spring Boot 2.x 中的实现

了解 Spring Boot 自动装配机制的小伙伴应该都知道,当我们引入了 spring-boot-starter-quartz 这样一个 starter 依赖之后,最终其实是通过 Spring Boot 的 SPI 机制,自动加载了一个 QuartzAutoConfiguration 配置类,该配置类是对 Quartz 中的一些对象及配置进行一系列的初始化操作。

在这个配置类中,定义了一个 SchedulerFactoryBean ,这个类主要是实现在 Spring 中进行对任务的调度。

需要注意的是,在 Spring Boot 中,该类默认是通过 SpringBeanJobFactory 实现对 Job 对象的创建。源码如下:

@Bean
@ConditionalOnMissingBean
public SchedulerFactoryBean quartzScheduler(QuartzProperties properties,
      ObjectProvider<SchedulerFactoryBeanCustomizer> customizers, ObjectProvider<JobDetail> jobDetails,
      Map<String, Calendar> calendars, ObjectProvider<Trigger> triggers, ApplicationContext applicationContext) {
   SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
   SpringBeanJobFactory jobFactory = new SpringBeanJobFactory();
   jobFactory.setApplicationContext(applicationContext);
   schedulerFactoryBean.setJobFactory(jobFactory);
   // 省略其余代码....
   return schedulerFactoryBean;
}

那么这个 SpringBeanJobFactory 有什么特殊的呢?该类继承了 AdaptableJobFactory 同时也实现了 ApplicationContextAware 接口

@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
   // 关键代码
   Object job = (this.applicationContext != null ?
         this.applicationContext.getAutowireCapableBeanFactory().createBean(
               bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) :
         super.createJobInstance(bundle));
   // 省略其余代码....
   return job;
}

我们可以看到该类在重写 createJobInstance() 方法中,创建 Job 对象的时候,会先判断是否在 Spring 上下文中,如果是在 Spring 环境中,也是通过 AutowireCapableBeanFactory 将 Job 对象放在 Spring 容器中,如果没有,则会调用父类 AdaptableJobFactory 进行反射创建 Job 对象。

从这儿可以看出,Spring Boot 2.x 版本通过 spring-boot-starter-quartz 集成之后,默认就是可以在 Job 对象中使用 @Autowired 的,并且实现的思路和我们是一致的,通过 AutowireCapableBeanFactory 将 Job 对象放入 Spring 容器中才是正确做法。

注:其实我用的就是 2.x 版本,在一开始什么别的操作都没有的时候,也会出现注入不成功的问题,只能说很奇怪。