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:
如果您覺得我的文章對您有幫助,請關注我的微信公眾號,謝謝!
程序員打怪之路


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM