Spring/Spring boot正确集成Quartz及解决@Autowired失效问题

周五检查以前Spring boot集成Quartz项目的时候,发现配置错误,因此通过阅读源码的方式,探索Spring正确集成Quartz的方式.

问题发现

检查去年的项目代码,发现关于QuartzJobBean的实现存在不合理的地方.

(1) 项目依赖:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
        </dependency>
    </dependencies>

(2) 问题代码:

@Component
public class UnprocessedTaskJob extends QuartzJobBean {

    private TaskMapper taskMapper;

    @Autowired
    public UnprocessedTaskJob(TaskMapper taskMapper){
        this.taskMapper = taskMapper;
    }
}

private JobDetail generateUnprocessedJobDetail(Task task) {
    JobDataMap jobDataMap = new JobDataMap();
    jobDataMap.put(UnprocessedTaskJob.TASK_ID, task.getId());
    return JobBuilder.newJob(UnprocessedTaskJob.class)
            .withIdentity(UnprocessedTaskJob.UNPROCESSED_TASK_KEY_PREFIX + task.getId(), UnprocessedTaskJob.UNPROCESSED_TASK_JOB_GROUP)
            .usingJobData(jobDataMap)
            .storeDurably()
            .build();
    }

(3) 提炼问题:

以上代码存在错误的原因是,UnprocessedTaskJob添加@Component注解,表示其是Spring IOC容器中的单例类.
然而Quartz在创建Job是通过相应的Quartz Job Beanclass反射创建相应的Job.也就是说,每次创建新的Job时,都会生成相应的Job实例.从而,这与UnprocessedTaskJob单例相冲突.
查看代码提交记录,原因是当时认为不添加@Component注解,则无法通过@Autowired引入由Spring IOC托管的taskMapper实例,即无法实现依赖注入.

然而令人感到奇怪的是,当我在开发环境去除了UnprocessedTaskJob@Component注解之后,运行程序后发现TaskMapper实例依然可以注入到Job中,程序正常运行...

Spring托管Quartz

代码分析

网上搜索Spring托管Quartz的文章,大多数都是Spring MVC项目,集中于如何解决在Job实现类中通过@Autowired实现Spring依赖注入.
网上大多实现均依赖SpringBeanJobFactory去实现SpringQuartz的集成.

/**
 * Subclass of {@link AdaptableJobFactory} that also supports Spring-style
 * dependency injection on bean properties. This is essentially the direct
 * equivalent of Spring's {@link QuartzJobBean} in the shape of a Quartz
 * {@link org.quartz.spi.JobFactory}.
 *
 * <p>Applies scheduler context, job data map and trigger data map entries
 * as bean property values. If no matching bean property is found, the entry
 * is by default simply ignored. This is analogous to QuartzJobBean's behavior.
 *
 * <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
 *
 * @author Juergen Hoeller
 * @since 2.0
 * @see SchedulerFactoryBean#setJobFactory
 * @see QuartzJobBean
 */
public class SpringBeanJobFactory extends AdaptableJobFactory
        implements ApplicationContextAware, SchedulerContextAware {
}

/**
 * {@link JobFactory} implementation that supports {@link java.lang.Runnable}
 * objects as well as standard Quartz {@link org.quartz.Job} instances.
 *
 * <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
 *
 * @author Juergen Hoeller
 * @since 2.0
 * @see DelegatingJob
 * @see #adaptJob(Object)
 */
public class AdaptableJobFactory implements JobFactory {
}

通过上述代码以及注释可以发现:
(1) AdaptableJobFactory实现了JobFactory接口,可以藉此创建标准的Quartz实例(仅限于Quartz 2.1.4及以上版本);
(2) SpringBeanJobFactory继承于AdaptableJobFactory,从而实现对Quartz封装实例的属性依赖注入.
(3) SpringBeanJobFactory实现了ApplicationContextAware以及SchedulerContextAware接口(Quartz任务调度上下文),因此可以在创建Job Bean的时候注入ApplicationContex以及SchedulerContext.

Tips:
以上代码基于Spring 5.1.8版本.
Spring 4.1.0版本, SpringBeanJobFactory的实现如以下代码所示:

public class SpringBeanJobFactory extends AdaptableJobFactory
    implements SchedulerContextAware{

    // 具体代码省略
}

因此,在早期的Spring项目中,需要封装SpringBeanJobFactory并实现ApplicationContextAware接口(惊不惊喜?).

Spring老版本解决方案

基于老版本Spring给出解决Spring集成Quartz解决方案.
解决方案由第三十九章:基于SpringBoot & Quartz完成定时任务分布式单节点持久化提供(大神的系列文章质量很棒).

@Configuration
public class QuartzConfiguration
{
    /**
     * 继承org.springframework.scheduling.quartz.SpringBeanJobFactory
     * 实现任务实例化方式
     */
    public static class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
            ApplicationContextAware {

        private transient AutowireCapableBeanFactory beanFactory;

        @Override
        public void setApplicationContext(final ApplicationContext context) {
            beanFactory = context.getAutowireCapableBeanFactory();
        }

        /**
         * 将job实例交给spring ioc托管
         * 我们在job实例实现类内可以直接使用spring注入的调用被spring ioc管理的实例
         * @param bundle
         * @return
         * @throws Exception
         */
        @Override
        protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
            final Object job = super.createJobInstance(bundle);
            /**
             * 将job实例交付给spring ioc
             */
            beanFactory.autowireBean(job);
            return job;
        }
    }

    /**
     * 配置任务工厂实例
     * @param applicationContext spring上下文实例
     * @return
     */
    @Bean
    public JobFactory jobFactory(ApplicationContext applicationContext)
    {
        /**
         * 采用自定义任务工厂 整合spring实例来完成构建任务
         * see {@link AutowiringSpringBeanJobFactory}
         */
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    /**
     * 配置任务调度器
     * 使用项目数据源作为quartz数据源
     * @param jobFactory 自定义配置任务工厂(其实就是AutowiringSpringBeanJobFactory)
     * @param dataSource 数据源实例
     * @return
     * @throws Exception
     */
    @Bean(destroyMethod = "destroy",autowire = Autowire.NO)
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, DataSource dataSource) throws Exception
    {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        //将spring管理job自定义工厂交由调度器维护
        schedulerFactoryBean.setJobFactory(jobFactory);
        //设置覆盖已存在的任务
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        //项目启动完成后,等待2秒后开始执行调度器初始化
        schedulerFactoryBean.setStartupDelay(2);
        //设置调度器自动运行
        schedulerFactoryBean.setAutoStartup(true);
        //设置数据源,使用与项目统一数据源
        schedulerFactoryBean.setDataSource(dataSource);
        //设置上下文spring bean name
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");
        //设置配置文件位置
        schedulerFactoryBean.setConfigLocation(new ClassPathResource("/quartz.properties"));
        return schedulerFactoryBean;
    }
}

通过以上代码,就实现了由SpringBeanJobFactorycreateJobInstance创建Job实例,并将生成的Job实例交付由AutowireCapableBeanFactory来托管.
schedulerFactoryBean则设置诸如JobFactory(实际上是AutowiringSpringBeanJobFactory,内部封装了applicationContext)以及DataSource(数据源,如果不设置,则Quartz默认使用RamJobStore).

RamJobStore优点是运行速度快,缺点则是调度任务无法持久化保存.

因此,我们可以在定时任务内部使用Spring IOC@Autowired等注解进行依赖注入.

Spring新版本解决方案

(1) 解释

如果你使用Spring boot,并且版本好大于2.0,则推荐使用spring-boot-starter-quartz.

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>

Auto-configuration support is now include for the Quartz Scheduler. We’ve also added a new spring-boot-starter-quartz starter POM.
You can use in-memory JobStores, or a full JDBC-based store. All JobDetail, Calendar and Trigger beans from your Spring application context will be automatically registered with the Scheduler.
For more details read the new “Quartz Scheduler” section of the reference documentation.

以上是spring-boot-starter-quartz的介绍,基于介绍可知,如果你没有关闭Quartz的自动配置,则SpringBoot会帮助你完成Scheduler的自动化配置,诸如JobDetail/Calendar/TriggerBean会被自动注册至Shceduler中.你可以在QuartzJobBean中自由的使用@Autowired依赖注入注解.

其实,不引入spring-boot-starter-quartz,而仅仅导入org.quartz-scheduler,Quartz的自动化配置依然会起效(这就是第一节问题分析中,去除@Bean注解,程序依然正常运行原因,悲剧中万幸).

(2) 代码分析

/**
 * {@link EnableAutoConfiguration Auto-configuration} for Quartz Scheduler.
 *
 * @author Vedran Pavic
 * @author Stephane Nicoll
 * @since 2.0.0
 */
@Configuration
@ConditionalOnClass({ Scheduler.class, SchedulerFactoryBean.class, PlatformTransactionManager.class })
@EnableConfigurationProperties(QuartzProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class })
public class QuartzAutoConfiguration{

    // 此处省略部分代码

    @Bean
    @ConditionalOnMissingBean
    public SchedulerFactoryBean quartzScheduler() {
        // 因为新版本SchedulerFactoryBean已经实现ApplicationContextAware接口
        // 因此相对于老版本Spring解决方案中的AutowiringSpringBeanJobFactory进行封装
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
        SpringBeanJobFactory jobFactory = new SpringBeanJobFactory();
        // SpringBeanJobFactory中注入applicationContext,为依赖注入创造条件
        jobFactory.setApplicationContext(this.applicationContext);
        // schedulerFactoryBean中注入setJobFactory(注意此处没有配置DataSource,DataSource详见`JdbcStoreTypeConfiguration`)
        // 以上这几个步骤,与老版本的Spring解决方案类似
        schedulerFactoryBean.setJobFactory(jobFactory);

        // 后续都是Quartz的配置属性设置,不再叙述
        if (this.properties.getSchedulerName() != null) {
            schedulerFactoryBean.setSchedulerName(this.properties.getSchedulerName());
        }
        schedulerFactoryBean.setAutoStartup(this.properties.isAutoStartup());schedulerFactoryBean.setStartupDelay((int) this.properties.getStartupDelay().getSeconds());
        schedulerFactoryBean.setWaitForJobsToCompleteOnShutdown(this.properties.isWaitForJobsToCompleteOnShutdown());
        schedulerFactoryBean.setOverwriteExistingJobs(this.properties.isOverwriteExistingJobs());
        if (!this.properties.getProperties().isEmpty()) {
            schedulerFactoryBean.setQuartzProperties(asProperties(this.properties.getProperties()));
        }
        if (this.jobDetails != null && this.jobDetails.length > 0) {
            schedulerFactoryBean.setJobDetails(this.jobDetails);
        }
        if (this.calendars != null && !this.calendars.isEmpty()) {
            schedulerFactoryBean.setCalendars(this.calendars);
        }
        if (this.triggers != null && this.triggers.length > 0) {
            schedulerFactoryBean.setTriggers(this.triggers);
        }
        customize(schedulerFactoryBean);
        return schedulerFactoryBean;
    }

    @Configuration
    @ConditionalOnSingleCandidate(DataSource.class)
    protected static class JdbcStoreTypeConfiguration {

        // 为Quartz的持久化配置DataSource,具体代码可以翻阅Spring源码得到
    }
}

下面对SpringBeanJobFactory进行分析,它是生成Job实例,以及进行依赖注入操作的关键类.

/**
 * Subclass of {@link AdaptableJobFactory} that also supports Spring-style
 * dependency injection on bean properties. This is essentially the direct
 * equivalent of Spring's {@link QuartzJobBean} in the shape of a Quartz
 * {@link org.quartz.spi.JobFactory}.
 *
 * <p>Applies scheduler context, job data map and trigger data map entries
 * as bean property values. If no matching bean property is found, the entry
 * is by default simply ignored. This is analogous to QuartzJobBean's behavior.
 *
 * <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
 *
 * @author Juergen Hoeller
 * @since 2.0
 * @see SchedulerFactoryBean#setJobFactory
 * @see QuartzJobBean
 */
public class SpringBeanJobFactory extends AdaptableJobFactory
        implements ApplicationContextAware, SchedulerContextAware {

    @Nullable
    private String[] ignoredUnknownProperties;

    @Nullable
    private ApplicationContext applicationContext;

    @Nullable
    private SchedulerContext schedulerContext;

    /**
     * Specify the unknown properties (not found in the bean) that should be ignored.
     * <p>Default is {@code null}, indicating that all unknown properties
     * should be ignored. Specify an empty array to throw an exception in case
     * of any unknown properties, or a list of property names that should be
     * ignored if there is no corresponding property found on the particular
     * job class (all other unknown properties will still trigger an exception).
     */
    public void setIgnoredUnknownProperties(String... ignoredUnknownProperties) {
        this.ignoredUnknownProperties = ignoredUnknownProperties;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setSchedulerContext(SchedulerContext schedulerContext) {
        this.schedulerContext = schedulerContext;
    }

    /**
     * Create the job instance, populating it with property values taken
     * from the scheduler context, job data map and trigger data map.
     */
    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        // 创建Job实例
        // (1) 包含applicationContext,则通过AutowireCapableBeanFactory()创建相应Job实例,实现依赖注入
        // (2) 如果applicationContext为空,则使用AdaptableJobFactory创建相应的Bean(无法实现依赖注入)
        Object job = (this.applicationContext != null ?
                        this.applicationContext.getAutowireCapableBeanFactory().createBean(
                            bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) :
                        super.createJobInstance(bundle));

        if (isEligibleForPropertyPopulation(job)) {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);
            MutablePropertyValues pvs = new MutablePropertyValues();
            if (this.schedulerContext != null) {
                pvs.addPropertyValues(this.schedulerContext);
            }
            pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());
            pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());
            if (this.ignoredUnknownProperties != null) {
                for (String propName : this.ignoredUnknownProperties) {
                    if (pvs.contains(propName) && !bw.isWritableProperty(propName)) {
                        pvs.removePropertyValue(propName);
                    }
                }
                bw.setPropertyValues(pvs);
            }
            else {
                bw.setPropertyValues(pvs, true);
            }
        }

        return job;
    }

    // 省略部分代码
}

/**
 * {@link JobFactory} implementation that supports {@link java.lang.Runnable}
 * objects as well as standard Quartz {@link org.quartz.Job} instances.
 *
 * <p>Compatible with Quartz 2.1.4 and higher, as of Spring 4.1.
 *
 * @author Juergen Hoeller
 * @since 2.0
 * @see DelegatingJob
 * @see #adaptJob(Object)
 */
public class AdaptableJobFactory implements JobFactory {
    /**
     * Create an instance of the specified job class.
     * <p>Can be overridden to post-process the job instance.
     * @param bundle the TriggerFiredBundle from which the JobDetail
     * and other info relating to the trigger firing can be obtained
     * @return the job instance
     * @throws Exception if job instantiation failed
     */
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        // 获取`QuartzJobBean`的实现`class`,通过反射工具创建相应的类实例(自然无法注入Spring托管的Bean实例)
        Class<?> jobClass = bundle.getJobDetail().getJobClass();
        return ReflectionUtils.accessibleConstructor(jobClass).newInstance();
    }
}

此处需要解释下AutowireCapableBeanFactory的作用.
项目中,有部分实现并未与Spring深度集成,因此其实例并未被Spring容器管理.
然而,出于需要,这些并未被Spring管理的Bean需要引入Spring容器中的Bean.
此时,就需要通过实现AutowireCapableBeanFactory,从而让Spring实现依赖注入等功能.

希望能够通过上述解释以及代码分析,让你知晓如何在老版本以及新版本Spring中正确集成Quartz.
此外,Spring boot的自动化配置能够解决绝大多数配置问题,但是在时间充裕的情况下,建议通过阅读源码等方式了解配置细节,从而做到更加的胸有成竹.

PS:
如果您觉得我的文章对您有帮助,请关注我的微信公众号,谢谢!
程序员打怪之路

posted @ 2019-06-30 16:09  从此寂静无声  阅读(10855)  评论(2编辑  收藏  举报