springboot整合quartz实战

1、Quartz的实操

  • Quartz API的关键接口是

  1. Scheduler - 与调度程序进行交互的主要API
  2. Job - 由您希望调度程序执行的组件所实现的接口
  3. JobDetail - 用于定义Jobs实例
  4. Trigger - 定义时间表的组件,在该时间表上将执行给定的作业
  5. JobBuilder - 用于定义/构建JobDetail实例,该实例定义Jobs实例
  6. TriggerBuilder - 用于定义/构建触发器实例
  • 创建SchedulerFactory实例,使用默认的quartz.properties

springboot整合quartz实战_第1张图片

# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#

org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

org.quartz.jobStore.misfireThreshold: 60000

org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
  • 当使用JdbcJobStore时,需要指定数据源并加入c3p0的连接池

springboot整合quartz实战_第2张图片

org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate

org.quartz.jobStore.dataSource: myDS
org.quartz.dataSource.myDS.driver: com.mysql.cj.jdbc.Driver
org.quartz.dataSource.myDS.URL: jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
org.quartz.dataSource.myDS.user: myUser
org.quartz.dataSource.myDS.password: myPassword
org.quartz.dataSource.myDS.maxConnections: 30
org.quartz.dataSource.myDS.connectionProvider.class: org.quartz.utils.C3p0PoolingConnectionProvider
  • 通过实现Job接口定义作业程序,构造JobDetail和Trigger并加入到调度器中

  package org.quartz;

  public interface Job {
    public void execute(JobExecutionContext context)
      throws JobExecutionException;
  }

当Job的触发器触发时,调度程序的工作线程之一将调用execute方法。传递给此方法的JobExecutionContext对象为作业实例提供有关其“运行时”环境的信息-执行作业的Scheduler的句柄,触发执行的Trigger的句柄,作业的JobDetail对象,以及其他一些项目。

public class DumbJob implements Job {

    public DumbJob() {
    }

    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobKey key = context.getJobDetail().getKey();

        JobDataMap dataMap = context.getJobDetail().getJobDataMap();

        String jobSays = dataMap.getString("jobSays");
        float myFloatValue = dataMap.getFloat("myFloatValue");

        System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
    }
}
public class QuartzDemo {
    public static void main(String[] args) throws SchedulerException {

        // 根据默认的quartz.properties构造调度器工厂
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        // 获得默认名称为QuartzScheduler的调度器
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 开启调度器线程,将会触发所有的触发器
        scheduler.start();

        // 构造排除日期并放入内部的TreeSet中
        HolidayCalendar calendar = new HolidayCalendar();
        calendar.addExcludedDate(DateBuilder.dateOf(12,0,0,11,11));
        calendar.addExcludedDate(DateBuilder.dateOf(12,0,0,12,12));
        scheduler.addCalendar("excludeHolidays", calendar, false, false);

        // 通过JobBuilder构造JobDetail实例
        JobDetail jobDetail = JobBuilder.newJob(DumbJob.class)
                // 构成唯一的JobKey(group.name)
                .withIdentity("myJob", "group1")
                // 设置的键值对会传递给DumbJob.class
                .usingJobData("jobSays", "Hello World!")
                .usingJobData("myFloatValue", 3.141f)
                .build();

        // 通过TriggerBuilder构造Trigger实例
        Trigger trigger = TriggerBuilder.newTrigger()
                // 构成唯一的TriggerKey(group.name)
                .withIdentity("myTrigger", "group1")
                .startNow()
                // 通过DateBuilder可以快速构造一个日期对象
                //.startAt(DateBuilder.futureDate(10, DateBuilder.IntervalUnit.MINUTE))
                // SimpleScheduleBuilder和CronScheduleBuilder最常用
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)
                        .withRepeatCount(60)
                        // 当持久的触发器由于其它原因错过触发后实行的策略
                        .withMisfireHandlingInstructionNextWithExistingCount())
                // 当同一时刻多个触发器同时触发,根据优先级大的触发
                .withPriority(5)
                // 排除哪些非工作日不进行触发操作
                .modifiedByCalendar("excludeHolidays")
                // 指定触发器不再进行触发响应的最终时间
                //.endAt(DateBuilder.futureDate(20, DateBuilder.IntervalUnit.MINUTE))
                .build();

        // 添加作业和触发器到调度器中
        scheduler.scheduleJob(jobDetail, trigger);

    }
}
  • 通过继承xxxSupport来实现JobListener、TriggerListener和SchedulerListener的事件监听

  1. JobListener:当JobDetail被执行时,实现该接口的类会被通知
  2. TriggerListener:当Trigger被执行时,实现该接口的类会被通知
  3. SchedulerListener:当Scheduler事件发生时,实现该接口的类会被通知

springboot整合quartz实战_第3张图片

// 添加作业事件的监听器到调度器中
scheduler.getListenerManager().addJobListener(new JobListenerSupport() {
    @Override
    public String getName() {
        return "MyJobListener";
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println(context.getJobDetail().getKey() + " ===> jobWasExecuted");
    }
}, KeyMatcher.keyEquals(JobKey.jobKey("myJob", "group1")));
  • quartz的企业级相关特性和Cron-Expressions(* 1-3 / ? L W #)

springboot整合quartz实战_第4张图片

群集当前仅适用于JDBC-Jobstore(JobStoreTX或JobStoreCMT),并且实质上是通过使群集的每个节点共享同一数据库来工作的。

负载平衡是自动发生的,群集中的每个节点都会尽快启动作业。当触发器的触发时间发生时,首先触发该节点(通过在其上放置锁)的节点将触发它。

每次触发时,只有一个节点将触发该作业。我的意思是,如果作业具有重复的触发器,告诉它每10秒触发一次,则在12:00:00恰好一个节点将运行该作业,而在12:00:10恰好一个节点将运行作业等等。不一定每次都在同一个节点上-哪个节点运行它或多或少是随机的。对于繁忙的调度程序(大量触发器),负载平衡机制几乎是随机的,但对于不繁忙(例如,少量触发器)的调度程序,则倾向于使用同一节点。

当节点之一在执行一个或多个作业的过程中发生故障时,就会发生故障转移。当一个节点发生故障时,其他节点将检测到该情况,并识别故障节点内正在进行的数据库作业。

标记为恢复的所有作业(在JobDetail中具有“requests recovery”属性)将由其余节点重新执行。未标记为要恢复的作业将在下一次相关触发器触发时被释放以执行。

通过将“ org.quartz.jobStore.isClustered”属性设置为“ true”来启用集群。集群中的每个实例都应使用quartz.properties文件的相同副本。例外情况是使用相同的属性文件,但允许以下例外:不同的线程池大小和“ org.quartz.scheduler.instanceId”属性的不同值。集群中的每个节点必须具有唯一的instanceId,可以通过将“ AUTO”放置为该属性的值来轻松完成(无需其他属性文件)。有关更多信息,请参见有关JDBC-JobStore的配置属性的信息。

#============================================================================
# Configure Main Scheduler Properties  
#============================================================================

org.quartz.scheduler.instanceName = MyClusteredScheduler
org.quartz.scheduler.instanceId = AUTO

#============================================================================
# Configure ThreadPool  
#============================================================================

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 25
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.oracle.OracleDelegate
org.quartz.jobStore.useProperties = false
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_

org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000

#============================================================================
# Configure Datasources  
#============================================================================

org.quartz.dataSource.myDS.driver = oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@polarbear:1521:dev
org.quartz.dataSource.myDS.user = quartz
org.quartz.dataSource.myDS.password = quartz
org.quartz.dataSource.myDS.maxConnections = 5
org.quartz.dataSource.myDS.validationQuery=select 0 from dual

JTA Transactions:JobStoreCMT允许在较大的JTA事务中执行Quartz调度操作。通过将“ org.quartz.scheduler.wrapJobExecutionInUserTransaction”属性设置为“ true”,作业还可以在JTA事务(UserTransaction)中执行。设置了此选项后,JTA事务将在Job的execute方法被调用之前开始begin(),而在execute调用终止之后进行commit()。这适用于所有作业。如果要为每个作业指示JTA事务是否应该包装其执行,则应在作业类上使用@ExecuteInJTATransaction批注。除了Quartz在JTA事务中自动包装Job执行之外,在使用JobStoreCMT时,您在Scheduler界面上进行的调用也会参与事务。只需确保已开始事务,然后再调用调度程序上的方法即可。您可以通过使用UserTransaction来直接执行此操作,也可以将使用调度程序的代码放在使用容器管理的事务的SessionBean中。

Cron-Expressions: 如果您需要一个基于类似于日历的概念而不是根据SimpleTrigger的确切间隔重复发生的工作解雇时间表,则CronTrigger通常比SimpleTrigger有用。使用CronTrigger,您可以指定触发计划,例如“每个星期五中午”,“每个工作日至9:30”,甚至“每个星期一,星期三的上午9:00至10:00之间的每5分钟”和一月的星期五”。即便如此,与SimpleTrigger一样,CronTrigger具有一个startTime,用于指定计划何时生效;以及一个(可选)endTime,用于指定计划何时终止。Cron-Expressions用于配置CronTrigger的实例。 Cron-Expression是实际上由七个子表达式组成的字符串,它们描述了日程表的各个细节。这些子表达式用空格分隔,代表:

1. Seconds
2. Minutes
3. Hours
4. Day-of-Month
5. Month
6. Day-of-Week
7. Year (optional field)

完整的cron表达式的示例是字符串“ 0 0 12 ? * WED”-表示“每个星期三的12:00:00 pm”。

各个子表达式可以包含范围和/或列表。例如,上一个(读为“ WED”)示例中的“星期几”字段可以替换为“ MON-FRI”,“ MON,WED,FRI”,甚至“ MON-WED,SAT”。

通配符(“*”字符)可用于表示此字段的“所有”可能值。因此,上一个示例的“月”字段中的“*”字符仅表示“每个月”。因此,“周几”字段中的“*”显然表示“一周中的每一天”。

所有字段都有一组可以指定的有效值。这些值应该非常明显-例如秒和分钟的数字0到59,小时的数字0到23。 Day-of-month可以是任何值1-31,但是您需要注意给定月份中有多少天!可以将月份指定为0到11之间的值,也可以使用字符串JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV和DEC来指定。每周的天数可以指定为1到7之间的值(1 =星期日),也可以使用字符串SUN,MON,TUE,WED,THU,FRI和SAT来指定。

“ /”字符可用于指定值的增量。例如,如果您在“分钟”字段中输入“ 0/15”,则表示“每小时的第15分钟,从零分钟开始”。如果您在“分钟”字段中使用“ 3/20”,则表示“每小时的第20分钟,从第3分钟开始”;换句话说,它与在“分钟”中指定“ 3,23,43”相同领域。请注意,“ /35”并不表示“每35分钟”,而是“每小时的每35分钟,从零分钟开始”,也就是指定为“ 0,35”。

day-of-month和day-of-week字段中允许使用“?”字符。用于指定“无特定值”。当您需要在两个字段之一中指定某项而不是另一个字段时,这很有用。有关说明,请参见下面的示例(和CronTrigger JavaDoc)。

day-of-month和day-of-week字段允许使用“ L”字符。此字符是“last”的简写,但在两个字段中都有不同的含义。例如,“月”字段中的值“ L”表示“月的最后一天”,即1月31日。如果单独用于星期几字段,则仅表示“ 7”或“ SAT”。但是,如果在星期几字段中使用另一个值,则表示“该月的最后一个xxx天”-例如“ 6L”或“ FRIL”均表示“该月的最后一个星期五”。您还可以指定与该月最后一天的偏移量,例如“ L-3”,这表示日历月的倒数第三天。使用“ L”选项时,不要指定列表或值的范围,这一点很重要,因为这样会导致混淆/意外的结果。

“ W”用于指定最接近给定日期的工作日(星期一至星期五)。例如,如果您将“15W”指定为“day-of-week”字段的值,则含义是:“离每月15日最近的工作日”。

“#”用于指定每月的第“n”个XXX工作日。例如,“day-of-week”字段中的“6#3”或“FRI#3”的值表示“每月的第三个星期五”。

这里还有一些表达式及其含义的示例-您可以在JavaDoc中找到org.quartz.CronExpression的更多示例。

“0 0/5 * * * ?”

“10 0/5 * * * ?”

“0 30 10-13 ? * WED,FRI”

“0 0/30 8-9 5,20 * ?”

注意,某些计划要求太过复杂而无法用单个触发器来表达,例如“上午9:00至上午10:00之间每5分钟一次,下午1:00至10:00下午每20分钟一次”。这种情况下的解决方案是简单地创建两个触发器,并注册两个触发器以运行相同的作业。

2、springboot整合quartz实操

Spring Boot为使用Quartz调度程序提供了许多便利,包括spring-boot-starter-quartz“ Starter”。如果Quartz可用,则自动配置Scheduler(通过SchedulerFactoryBean抽象)。

以下类型的Bean将自动被拾取并与Scheduler关联:

  • JobDetail: 定义一个特定的作业。可以使用JobBuilder API构建JobDetail实例。

  • Calendar: 定义排除特定日期触发作业。

  • Trigger: 定义何时触发特定作业。

默认情况下,使用内存中的JobStore。但是,如果您的应用程序中有可用的DataSource bean,并且如果spring.quartz.job-store-type属性已相应配置,则可以配置基于JDBC的存储,如以下示例所示:

spring.quartz.job-store-type=jdbc

使用JDBC存储时,可以在启动时初始化模式,如以下示例所示:

spring.quartz.jdbc.initialize-schema=always

默认情况下,使用Quartz库随附的标准脚本检测并初始化数据库。这些脚本删除现有表,并在每次重新启动时删除所有触发器。还可以通过设置spring.quartz.jdbc.schema属性来提供自定义脚本。

要让Quartz使用应用程序的主DataSource之外的DataSource,请声明一个DataSource bean,并用@QuartzDataSource注释其@Bean方法。这样做可以确保SchedulerFactoryBean和模式初始化都使用特定于Quartz的数据源。

默认情况下,通过配置创建的作业将不会覆盖从持久性作业存储中读取的已注册作业。要启用覆盖现有作业定义的功能,请设置spring.quartz.overwrite-existing-jobs属性。

可以使用spring.quartz属性和SchedulerFactoryBeanCustomizer bean来定制Quartz Scheduler配置,这允许以编程方式进行SchedulerFactoryBean定制。可以使用spring.quartz.properties.* 自定义高级Quartz配置属性。

特别是,Executor bean与scheduler没有关联,因为Quartz提供了一种通过spring.quartz.properties配置调度程序的方法。如果需要自定义任务执行器,请考虑实现SchedulerFactoryBeanCustomizer。

作业可以定义setters以注入data map属性。常规bean也可以类似的方式注入,如以下示例所示:

public class SampleJob extends QuartzJobBean {

    private MyService myService;

    private String name;

    // Inject "MyService" bean
    public void setMyService(MyService myService) { ... }

    // Inject the "name" job data property
    public void setName(String name) { ... }

    @Override
    protected void executeInternal(JobExecutionContext context)
            throws JobExecutionException {
        ...
    }

}

 

public class MyQuartzJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}
@Configuration
public class QuartzConfig {

    @Bean
    @QuartzDataSource
    public DataSource quartzDataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setJdbcUrl("jdbc:mysql://localhost:3306/myDS?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
        ds.setUsername("myUsername");
        ds.setPassword("myPassword");
        return ds;
    }

    @Bean
    public Calendar myCalendar() {
        HolidayCalendar holidayCalendar = new HolidayCalendar();
        holidayCalendar.addExcludedDate(DateBuilder.dateOf(15,12,30));
        return holidayCalendar;
    }

    @Bean
    public JobDetail myJobDetail() {
        return JobBuilder.newJob(MyQuartzJob.class).storeDurably().build();
    }

    @Bean
    public Trigger myTrigger() {
        return TriggerBuilder.newTrigger()
                .forJob(myJobDetail())
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(1)
                        .repeatForever())
                .build();
    }
}
spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=always
spring.quartz.properties.org.quartz.scheduler.instanceName=myQuartzScheduler

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=myUsername
spring.datasource.password=myPassword

 

你可能感兴趣的