最近在实现任务调度服务,使用 quartz调度框架,由于使用的spring  boot版本为1.5.10,该版本并没有提供quartz的starter,在集成的时候还是遇到了很多问题。本文并不会详细介绍quartz框架,请自行查阅相关资料。如果对Spring boot不太熟悉,可以看前边几篇关于spring boot的文章。

1. quartz简介

1.1. 核心概念

  • scheduler:任务调度器,相当于指挥中心,负责管理和调度job,由SchedulerFactory创建和关闭,创建后需要通过starter()方法来启动调度。

  • trigger:触发器,定义job的触发时机,通过TriggerBuilder来创建,有SimpleTriggerCronTrigger,前者可以定义在某一个时刻触发或者周期性触发,后者使用cron表达式来定义触发时间。

  • Job:具体的调度业务逻辑实现,也就是任务,具体任务需要实现org.quartz.Job接口。

  • JobDetail:org.quartz.JobDetail用来描述Job实例的属性,并且可以通过JobDataMap传递给Job实例数据,通过JobBuilder来创建。

1.2. quartz配置

quartz框架默认会通过quartz.properties文件来加载配置信息,有几个重要的配置项:

  • org.quartz.scheduler.*:调度器相关配置

  • org.quartz.threadPool.*:线程池相关配置

  • org.quartz.jobStore.*:quartz数据存储相关配置,在quartz中,有三种存储类型:

    • RAMJobStore:基于内存存储job

    • JobStoreCMT:基于数据库的数据存储,并且受运行的java容器的事务控制

    • JobStoreTX:基于数据库的数据存储,不受事务控制

  • org.quartz.dataSource.*:配置数据库存储quartz数据时的数据源信息

  • org.quartz.plugin.*:quartz插件相关配置

quartz简单介绍这么多,更多信息请看 这里

2. Spring boot与quartz集成

前边说过,我使用的spring boot版本为1.5.10,没有quartz的starter,所以需要自己编码实现。同时,我还需要对quartz做持久化处理,需要在mysql库中导入quartz的建表脚本。集成步骤如下:

1、引入依赖

引入quartz的依赖包,quartz-jobs根据实际需要决定是否引入。

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.3</version>
</dependency>

2、创建表

在下载的quartz包中有建表脚本,我的为quartz-2.2.3,建表脚本在/docs/dbTables下边,导入tables_mysql_innodb.sql即可。

3、配置文件

在resources目下新建一个quartz.properties配置文件,我的配置如下:

#============================================================================
# Configure Main Scheduler Properties
#============================================================================
org.quartz.scheduler.instanceName:event-scheduler
org.quartz.scheduler.instanceId:AUTO
org.quartz.scheduler.skipUpdateCheck:true
#============================================================================
# Configure ThreadPool
#============================================================================
org.quartz.threadPool.class:org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount:5
org.quartz.threadPool.threadPriority:5
#============================================================================
# Configure JobStore
#============================================================================
org.quartz.jobStore.misfireThreshold:60000
org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass:org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties:false
org.quartz.jobStore.tablePrefix:QRTZ_
org.quartz.jobStore.isClustered:false
#============================================================================
# Configure Datasources
#============================================================================
org.quartz.dataSource.ds.maxConnections:7
#============================================================================
# Configure Plugins
#============================================================================
org.quartz.plugin.triggHistory.class: org.quartz.plugins.history.LoggingTriggerHistoryPlugin

这里并没有数据源ds相关的信息,仅配置了最大连接,稍后会解释为什么。注意jobStore配置,这里我配置为JobStoreTX,表示使用数据库持久化。

4、创建quartz配置类

新建一个QuartzAutoConfiguration类,用来配置quartz,代码如下:

@Configuration
@ConditionalOnClass({Scheduler.class, SchedulerFactoryBean.class,
        PlatformTransactionManager.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class,
        HibernateJpaAutoConfiguration.class})
public class QuartzAutoConfiguration {
    private static final String DATASOURCE_NAME = "ds";

    @Value("${spring.datasource.driver-class-name}")
    private String datasourceDriverClass;
    @Value("${spring.datasource.url}")
    private String datasourceUrl;
    @Value("${spring.datasource.username}")
    private String datasourceUsername;
    @Value("${spring.datasource.password}")
    private String datasourcePassword;

    /**
     * 声明自定义的{@link JobFactory}。
     *
     * @param applicationContext spring上下文
     * @return jobFactory实例
     */
    @Bean
    public JobFactory jobFactory(ApplicationContext applicationContext) {
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
    }

    /**
     * 声明{@link SchedulerFactoryBean},使用自定义{@link AutowiringSpringBeanJobFactory}。
     *
     * @param jobFactory jobFactory
     * @return SchedulerFactoryBean实例
     * @throws Exception exception
     */
    @Bean
    public SchedulerFactoryBean quartzScheduler(JobFactory jobFactory) throws Exception {
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();

        Properties properties = new Properties();
        InputStream is = QuartzAutoConfiguration.class.getResourceAsStream("/quartz.properties");
        properties.load(is);

        // 替换数据源配置,使用自带的数据源

        properties.setProperty("org.quartz.jobStore.dataSource", DATASOURCE_NAME);
        properties.setProperty("org.quartz.dataSource." + DATASOURCE_NAME + ".driver", datasourceDriverClass);
        properties.setProperty("org.quartz.dataSource." + DATASOURCE_NAME + ".URL", datasourceUrl);
        properties.setProperty("org.quartz.dataSource." + DATASOURCE_NAME + ".user", datasourceUsername);
        properties.setProperty("org.quartz.dataSource." + DATASOURCE_NAME + ".password", datasourcePassword);

        schedulerFactoryBean.setJobFactory(jobFactory);
        schedulerFactoryBean.setQuartzProperties(properties);
        schedulerFactoryBean.afterPropertiesSet();
        return schedulerFactoryBean;
    }
」

这里用到了AutowiringSpringBeanJobFactory自定义类,稍后再Job自动注入Bean章节会讲到,简单而言这个就是自定义的SpringBeanJobFactory,能够使Job实例自动注入其他Bean。

这里需要注意,前边说了quartz配置文件并没有配置数据源信息,其实是在这里编码配置的。在配置SchedulerFactoryBean时,会首先加载前边创建quartz.properties的配置信息,并且将quartz的数据源信息配置为本工程的Spring管理的默认数据源,使其使用同一个数据源。

SchedulerFactoryBean就是Spring管理quartz的核心,通过它来创建和配置Scheduler,管理器生命周期和依赖。

5、编写调度Service

这里编写一个SchedulerServiceImpl,来实现调度、取消调度等方法:

@Service
@EnableScheduling
public class SchedulerServiceImpl implements SchedulerService {
    private static Logger log = LoggerFactory.getLogger(SchedulerServiceImpl.class);

    @Autowired
    private Scheduler scheduler;

    @Override
    public void schedule(JobDetail jobDetail, Trigger trigger) {
        try {
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            log.error(String.format("Can not schedule %s on %s", jobDetail, trigger), e);
        }
    }

    @Override
    public void reschedule(TriggerKey triggerKey, Trigger newTrigger) {
        try {
            scheduler.rescheduleJob(triggerKey, newTrigger);
        } catch (SchedulerException e) {
            log.error(String.format("Can not reschedule %s with new trigger %s", triggerKey, newTrigger), e);
        }
    }

    @Override
    public void unscheduleJob(TriggerKey triggerKey) {
        try {
            scheduler.unscheduleJob(triggerKey);
        } catch (SchedulerException e) {
            log.error(String.format("Can not destroy schedule on %s", triggerKey), e);
        }
    }

    @Override
    public JobDetail getJobDetail(JobKey jobKey) {
        try {
            return scheduler.getJobDetail(jobKey);
        } catch (SchedulerException e) {
            log.error(String.format("Can not find job detail of %s", jobKey), e);
            return null;
        }
    }
}

注意这里使用了@EnableScheduling注解来启用quartz调度。

5、编写一个测试Service:

我们编写一个QuartzService,来测试调度情况:

@Service
public class QuartzService {
    private static Logger log = LoggerFactory.getLogger(QuartzService.class);

    @Autowired
    private SchedulerService schedulerService;

    @Scheduled(cron = "0/20 * * * * ?")
    public void cron() {
        log.info("cron scheduling ...");
    }

    @Scheduled(fixedDelay = 1000 * 10)
    public void fixedDelay() {
        log.info("fixedDelay scheduling ...");
    }

    @PostConstruct
    public void demo() {
        String group = "test-group";
        JobDetail jobDetail = JobBuilder.newJob(TestJob.class)
                .withIdentity("test-job", group)
                .build();
        Trigger trigger = newTrigger()
                .withIdentity("test-trigger", group)
                .startAt(new Date(System.currentTimeMillis() + 10000))
                .usingJobData("businessId", "test-id")
                .build();
        schedulerService.schedule(jobDetail, trigger);
    }

    public static class TestJob implements Job {
        private String businessId;

        @Autowired
        private DemoService demoService;

        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            System.out.println("businessId : " + businessId);
            System.out.println("job : " + context.getJobDetail());
            System.out.println("trigger : " + context.getTrigger());
            String s = demoService.say("test service invoke");
            System.out.println(s);
        }

        public void setBusinessId(String businessId) {
            this.businessId = businessId;
        }
    }
}

依赖的DemoService用来测试Job实例的依赖注入,这里的Job实例使用内部静态来来实现,businessId属性是通过Trigger上的usingJobData()方法来传递的,其底层其实就是JobDataMap,需要提供setter。

6、测试

启动应用和查看数据库,可以看到调度任务成功持久化和执行。

3. Job自动注入Bean

3.1. 如何创建Job实例

前边提到了AutowiringSpringBeanJobFactory自定义类,为什么需要这个类?有何作用?这跟Quartz的job实例创建有关,Job实例都是 通过反射来创建的,因为我们只给JobDetai提供了Job实例的class。

Quartz默认使用的SimpleJobFactory来创建Job实例:

public Job newJob(TriggerFiredBundle bundle, Scheduler Scheduler) throws SchedulerException
    JobDetail jobDetail = bundle.getJobDetail();
    Class<? extends Job> jobClass = jobDetail.getJobClass();
    try {
        ……
        return jobClass.newInstance();
    } catch (Exception e) {
        ……
    }
}

Spring是通过SpringBeanFactorycreateJobInstance()方法来创建Job实例,同样也是通过反射创建的:

protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
    return bundle.getJobDetail().getJobClass().newInstance();
}

可以看到,通过JobDetail获取到JobClass,然后通过反射创建了Job实例。

因此,该实例并没有受Spring管理,所以在Job实例中不能使用依赖注入,我们需要使创建的Job实例被Spring管理。

3.2. 改造SpringBeanFactory

要让Job实例受Spring控制,我们需要对SpringBeanFactory进行扩展,同时需要用到ApplicationContextAware接口,以获取ApplicationContext类,并从它身上获取能够让Bean能够自动注入的AutowireCapableBeanFactory类。

编写AutowiringSpringBeanJobFactory

public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
        ApplicationContextAware {
    private AutowireCapableBeanFactory autowireCapablebeanFactory;

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

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        Object job = super.createJobInstance(bundle);
        // 让job实例可以自动注入
        autowireCapablebeanFactory.autowireBean(job);
        return job;
    }
}

然后,在Quartz配置类中,使用该自定义的JobFactory即可,前边的创建Quartz配置类章节已经提到,不在赘述。

4. Spring boot 2.0的quartz集成

其实,在Spring boot2.0已经提供了spring-boot-starter-quartz,集成就更加方便了,也不用担心依赖注入的问题。

1、引入依赖:

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

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>

2、配置

不需要quartz.properties了,只需要在application.properties加入如下配置:

spring.application.name=quartz
# 基于数据库持久化数据
spring.quartz.job-store-type=jdbc
# 应用初始化后自动启动调度器
spring.quartz.auto-startup=true
# 是否每次都删除并重新创建表
spring.quartz.jdbc.initialize-schema=never
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/spring-boot-quartz?useUnicode=true&characterEncoding=utf-8&rewriteBatchedStatements=TRUE
spring.datasource.username=root
spring.datasource.password=123
server.port=8080

spring.quartz.job-store-type有两种类型:jdbc和memory,分别是基于数据库和内存存储数据。

3、编写SchedulerServiceImpl

代码同前。

4、编写测试QuartzService

代码同前。

5、测试

启动应用和查看数据库,可以看到调度任务成功持久化和执行。

5. 总结

在低版本的spring boot中,集成quartz还是比较麻烦,除了要手动配置quartz外,还需要处理依赖注入的问题,而2.0已经提供了相应的启动器,有条件还是使用高版本吧!


相关阅读