quartz的学习与使用

目录

quartz的学习与使用

概念与应用场景描述

quartz的概念

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由java语言开发,支持分布式、集群部署,且具有丰富的调度方式。

quartz应用场景描述

quartz主要是用于进行定时任务的执行。

  • 例如哔哩哔哩的视频播放量需要每隔30秒刷新一次。则30秒执行一次刷新播放量的定时任务(这是打个比方)
  • 服务器需要每隔1天,发送一个邮件给开发者,内容为当前服务器的日志信息。这个发送邮件的操作,在该场景下就也是一个定时任务。

quartz的核心概念

quartz的学习与使用

Job对象

实现该接口后,重写其中的execute方法,该方法就是我们需要执行的任务(业务逻辑代码)。

JobDetail对象

该对象的作用是绑定一个Job对象,然后对其进行描述,主要有如下的几个属性

  • name 任务的名称,默认是一串随机的字符串
  • group 任务所在的组,默认为字符串:deafult
  • description 任务描述
  • jobClass 任务类,用于指定Job对象
  • jobDataMap 用于传递一些自定义的任务参数

Tigger对象

该对象是一个触发器,在绑定任务后,设置任务的执行时间,结束时间,执行间隔,执行频率等。
该触发器主要有4中指定触发规则的Tigger。

  • SimpleTrigger 简单的Tigger触发器
  • CronTrigger 使用cron表达式的触发器
  • DataIntervalTrigger 按照时间间隔进行任务触发的一个触发器。可以解决misfired失火问题。
  • NthIncludedTrigger 已经不建议使用了,具体细节可以自行查阅资料

Scheduler对象

该对象用于绑定JobDetail和Tigger,以此来进行任务的调度(执行)。
quartz官方提供了2种创建Scheduler对象的工厂类

  • DirectSchedulerFactory(硬编码方式创建Scheduler对象,不推荐)
  • StdSchedulerFactory(使用配置文件来创建Scheduler对象,推荐)

quartz的入门基础使用小demo

创建一个jar类型的maven项目,并引入如下的依赖

quartz使用的是slf4j日志门面,但是还没有具体的实现。
所以这里引入Logback的日志实现,在pom文件中新增依赖。

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

        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

在resources目录下创建一个logback.xml文件(设置一下日志的输出格式)

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false" >

    <!-- 日志级别 -->
    <property name="logLevel" value="INFO"/>

    <!-- 配置输出格式 -->
    <property name="logPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger - %msg%n"/>

    <!-- 控制台打印日志的相关配置 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- 日志格式 -->
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${logPattern}</pattern>
        </encoder>
    </appender>

    <root level="${logLevel}">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

创建一个Job任务,输出当前时间

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String currentDate = LocalDateTime.now().format(dtf);
        System.out.println(currentDate);
    }
}

创建一个QuartzTest测试类。


import com.it.job.DateJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzTest {
    public static void main(String[] args) throws SchedulerException {
        // 创建任务详情JobDetail,绑定任务,并且指定任务的名称和组名
        JobDetail jobDetail = JobBuilder.newJob(DateJob.class)
                                   .withIdentity("job1","g1")
                                   .build();

        // 创建一个触发器,绑定JobDetail
        // 设置了触发器的名称、组名、以及立即开始、并且指定触发规则为5秒执行一次,无限重复
        Trigger trigger = TriggerBuilder.newTrigger()
                                        .withIdentity("t1","g1")
                                        .forJob(jobDetail)
                                        .startNow()
                                        .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
                                        .build();

        // 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.scheduleJob(jobDetail, trigger);

        scheduler.start();
    }
}

运行结果

quartz的学习与使用

本次小demo的几个需要注意的事项

  1. Scheduler每次进行调度任务时,会通过jobDetail来找到对应的Job,每次执行任务时,都会new一个新的Job实例。所以Job默认是无状态的。

  2. Scheduler对象进行任务调度时,可以只指定一个Trigger触发器,而不指定JobDetail,前提是JobDetail已经交由Scheduler管理

quartz为JobDetail和Trigger自定义数据,并在Job中获取

修改QuartzTest文件中的代码,存储自定义数据

quartz的学习与使用

在Job任务中获取到对应的自定义数据的第一种方式,通过JobExecutionContext

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 获取当前的JobDetail中的自定义数据
        String JobDetailMyKey1 = jobExecutionContext.getJobDetail().getJobDataMap().getString("myKey1");
        System.out.println("JobDetail中的Key1:" + JobDetailMyKey1);

        // 获取当前的JobDetail中的自定义数据
        String TriggerMyKey1 = jobExecutionContext.getTrigger().getJobDataMap().getString("myKey1");
        System.out.println("Trigger中的Key1:" + TriggerMyKey1);

        // 获取JobDetail与Trigger合并后的自定义数据,如果两者key有冲突,则会使用Trigger的自定义数据覆盖JobDetail的自定义数据
        String myKey1 = jobExecutionContext.getMergedJobDataMap().getString("myKey1");
        System.out.println("合并后出现重复的key的数据:" + myKey1);
    }

在Job任务中获取到对应的自定义数据的第二种方式,依赖注入

quartz的学习与使用

运行效果

quartz的学习与使用

quartz的配置文件

  • quartz应用程序默认在启动时,会在类路径查找quartz.properties文件作为配置文件的加载。

  • 但是如果类路径下没有该配置文件,则会默认加载在org/quartz/文件夹下的quartz.properties文件
    quartz的学习与使用

  • 默认的配置文件如下,我只是加了个不检查版本更新的配置。

# 如果使用集群模式,则该实例名则是区分集群的唯一标识
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
# 如果希望Quartz Scheduler通过RMI作为服务器导出本身,则为true。
org.quartz.scheduler.rmi.export = false
# 如果要连接(使用)远程服务的调度程序,则为true。还必须指定RMI注册表进程的主机和端口 - 通常是“localhost”端口1099
org.quartz.scheduler.rmi.proxy = false
# 设置这项为true使我们在调用job的execute()之前能够开始一个UserTransaction。
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
# 程序运行期间,跳过quartz版本更新的检查
org.quartz.scheduler.skipUpdateCheck=true

# 指定的线程池
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

quartz整合Spring完成任务的调度(基于配置的方式)

创建一个jar类型的maven项目,引入spring与quartz的jar包

    <properties>
        <spring.version>5.2.3.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!-- spring ioc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- 注意别少了这个包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
        <!-- 提供一些quartz的支持工具,例如发送邮件的定时任务 -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.2.1</version>
        </dependency>
    </dependencies>

创建一个Job任务

  • 其实这里也可以将实现Job接口替换成继承QuartzJobBean
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.context.ApplicationContext;

public class MySimpleJob implements Job{

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 获取到Spring容器
        ApplicationContext ac = (ApplicationContext) jobExecutionContext.getMergedJobDataMap().get("applicationContext");
        System.out.println(ac);
        
        // 获取到自定义的参数
        String key1 = jobExecutionContext.getJobDetail().getJobDataMap().getString("key1");
        System.out.println("key1的值为:" + key1);
    }
}

在类路径下配置Spring的配置文件

  1. 创建JobDetail,绑定任务Job,注意,这里使用的是JobDetailFactoryBean
  2. 创建Trigger触发器
  3. 创建Scheduler调度器,对任务进行调度
    <!-- 定义JobDetail ,这里使用JobDetailFactoryBean,也可以使用MethodInvokingJobDetailFactoryBean ,配置类似-->
    <bean id="jobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
        <!-- 指定job的名称 -->
        <property name="name" value="job1"/>
        <!-- 指定job的分组 -->
        <property name="group" value="group1"/>
        <!-- 指定具体的job类 -->
        <property name="jobClass" value="com.it.job.MySimpleJob"/>
        <!-- 必须设置为true,如果为false,当没有活动的触发器与之关联时会在调度器中会删除该任务  -->
        <property name="durability" value="true"/>
        <!-- 指定spring容器的key,如果不设定在job中的jobDataMap中是获取不到spring容器的 -->
        <property name="applicationContextJobDataKey" value="applicationContext"/>
        <!-- 定义自定义的数据 -->
        <property name="jobDataAsMap">
            <map>
                <entry key="key1" value="value1"/>
            </map>
        </property>
    </bean>

    <!-- 配置Trigger触发器 -->
    <!-- 第一种 SimpleTriggerBean,只支持按照一定频度调用任务,如每隔30分钟运行一次。配置方式如下: -->
    <!-- <bean id="simpleTrigger"
        class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
        <property name="jobDetail" ref="jobDetail"></property>
        <property name="startDelay" value="3000"></property>
        <property name="repeatInterval" value="2000"></property>
    </bean> -->

    <!-- 第二种 CronTriggerBean,支持到指定时间运行一次,每隔2秒执行一次。配置方式如下: -->
    <bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="jobDetail" ref="jobDetail" />
        <!-- <!—每天12:00运行一次 —> -->
        <property name="cronExpression" value="0/2 * * * * ?" />
    </bean>

    <!--  配置调度工厂scheduler,进行任务的调度 -->
    <bean id="schedulerFactoryBean" lazy-init="true"
          class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <!-- <ref bean="testTrigger"></ref> -->
                <ref bean="cronTrigger" />
            </list>
        </property>
    </bean>

解决在Job示例中使用@Autowired注解,注入失败的问题

创建一个Spring的工具类,实现ApplicationContextAware接口

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtil implements ApplicationContextAware {
 

    private static ApplicationContext applicationContext;
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (SpringContextUtil.applicationContext == null) {
            SpringContextUtil.applicationContext = applicationContext;
        }
    }
 
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 通过bean的id属性获取bean
     * @param name bean的id
     * @param <T> 强制转换的类型
     * @return
     */
    public static <T> T getBean(String name) {
        return (T) getApplicationContext().getBean(name);
    }

    /**
     * 通过类型获取到bean
     * @param clazz 该类型的Class
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * 通过bean的id以及类型获取到bean
     * @param name
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }
 
}

Spring的配置文件记得添加context:component-scan 扫描该类

使用方式(在JOB中)

- 前提是这个id为user的bean已经加入到了spring的容器中

    public User user = SpringContextUtil.getBean("user");

Test测试类(其实就是加载一下Spring的配置文件)

public class MyTest {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

    }
}

Spring整合quartz(基于MethodInvokingJobDetailFactoryBean配置JobDetail)

首先随意修改一下Job类为如下,无需再实现Job

import com.it.entity.User;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;

//@Component ,由于在spring配置文件中配置过了,所以不用配置该注解
public class MySimpleJob2{

    @Autowired
    private User user;

    @Autowired
    public Scheduler scheduler;

    public void show() {
        // 这是自动注入的User类,我这里随便的User类为:   class User{}
        System.out.println("依赖注入的User对象: " + user);
        // 当前项目的Scheduler对象
        System.out.println("Scheduler对象:" + scheduler);
    }
    
}

修改Spring的配置文件,主要修改如下内容

    <bean id="user" class="com.it.entity.User"/>

    <!-- 定义需要执行的job类 -->
    <bean id="myJob" class="com.it.job.MySimpleJob2" />

    <!-- 定义JobDetail 使用MethodInvokingJobDetailFactoryBean -->
    <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <!-- 指定job的名称 -->
        <property name="name" value="job1"/>
        <!-- 指定job的分组 -->
        <property name="group" value="group1"/>
        <!-- 指定具体的job类 -->
        <property name="targetObject" ref="myJob"/>
        <!-- 指定job类中的方法为定时任务 -->
        <property name="targetMethod" value="show" />
    </bean>

Spring整合quartz完成持久化

创建一个jar类型的maven项目,引入如下的依赖

    <dependencies>
        <!-- spring ioc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- 注意别少了这个包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- 数据库连接 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.25</version>
        </dependency>

        <!-- quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
        <!-- 提供一些quartz的支持工具,例如发送邮件的定时任务 -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.2.1</version>
        </dependency>

        <!-- 非必须,如果想要查看quartz日志,可以引入,然后在类路径下配置一个logback.xml文件-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>

在类路径下添加spring的配置文件,主要是为了配置数据源以及Scheduler

    <!-- 引入配置文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <context:component-scan base-package="com.it"/>

    <!-- 配置数据源 -->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>


    <!--  配置调度工厂scheduler,进行任务的调度 -->
    <bean name="quartzScheduler" lazy-init="false"
          class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 可以通过scheduler.getContext().get("applicationContextKey") 获取到Spring容器对象 -->
        <property name="applicationContextSchedulerContextKey"  value="applicationContextKey" />
        <!-- 是否自动启动 -->
        <property name="autoStartup" value="true" />
        <!-- 其实也可以直接指定quartz的配置文件,然后在配置文件中配置,本质是一样的 -->
        <property name="quartzProperties">
            <props>
                <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
                <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop>
            </props>
        </property>
    </bean>

编写一个简单的job任务

public class MyJob1 extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("你好呀,我很好,是的");
    }
}

编写一个测试类(我这里是通过spring加载bean的机制进行的任务调度,其实可以通过使用junit整合spring来进行单元测试)

@Component
public class JobInit {

    @Autowired
    private Scheduler scheduler;

    @PostConstruct
    public void init() throws Exception {
        // 创建任务详情JobDetail,绑定任务,并且指定任务的名称和组名
        JobDetail jobDetail = JobBuilder.newJob(MyJob1.class)
                .withIdentity("job4","g1")
                .build();

        // 创建一个触发器,绑定JobDetail
        // 设置了触发器的名称、组名、以及立即开始、并且指定触发规则为5秒执行一次,无限重复
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("t3","g1")
                .forJob(jobDetail)
                .startNow()
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
                .build();

        // 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
        scheduler.scheduleJob(jobDetail, trigger);

        scheduler.start();
    }

}

spring整合quartz完成集群的配置

首先创建一个jar类型的maven项目,引入如上spring整合quartz进行持久化的依赖

在类路径下添加spring的配置文件,内容和刚刚spring整合quartz持久化差不多,只是修改了如下片段

<!--  配置调度工厂scheduler,进行任务的调度 -->
    <bean name="quartzScheduler" lazy-init="false"
          class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- 可以通过scheduler.getContext().get("applicationContextKey") 获取到Spring容器对象 -->
        <property name="applicationContextSchedulerContextKey"  value="applicationContextKey" />
        <!-- 是否自动启动 -->
        <property name="autoStartup" value="true" />
        <!-- 其实也可以直接指定quartz的配置文件,然后在配置文件中配置,本质是一样的 -->
        <property name="quartzProperties">
            <props>
                <!-- 持久化配置-->
                <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop>
                <prop key="org.quartz.jobStore.driverDelegateClass">org.quartz.impl.jdbcjobstore.StdJDBCDelegate</prop>

                <!-- 集群配置 -->
                <prop key="org.quartz.jobStore.isClustered">true</prop>
                <!-- 注意这个相当于集群的名字,而quartz的集群是通过所连接的数据库来判定的 -->
                <prop key="org.quartz.scheduler.instanceName">myCluster</prop>
                <prop key="org.quartz.scheduler.instanceId">AUTO</prop>
            </props>
        </property>
    </bean>

创建一个任务类

public class MyJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        // 获取当前系统时间
        String thisDateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        System.out.println(thisDateTime + " 我是一个任务,任务名为:" + jobExecutionContext.getJobDetail().getKey().getName());
    }
}

创建一个Test测试类,用于加载任务(只运行一次)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:applicationContext.xml" })
public class JTest {

    @Autowired
    private Scheduler scheduler;
  
    @Test
    public void addJob() throws Exception{
        JobDetail job1 = JobBuilder.newJob(MyJob.class).withIdentity("job1").build();
        JobDetail job2 = JobBuilder.newJob(MyJob.class).withIdentity("job2").build();
        JobDetail job3 = JobBuilder.newJob(MyJob.class).withIdentity("job3").build();

        Trigger trigger1 = TriggerBuilder.newTrigger().forJob(job1).withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)).build();
        Trigger trigger2 = TriggerBuilder.newTrigger().forJob(job2).withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)).build();
        Trigger trigger3 = TriggerBuilder.newTrigger().forJob(job3).withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5)).build();

        scheduler.scheduleJob(job1, trigger1);
        scheduler.scheduleJob(job2, trigger2);
        scheduler.scheduleJob(job3, trigger3);

        scheduler.start();
    }  
}  

启动项目(对了,记得把当前项目复制一份,然后2台都启动)

然后就会看到集群环境下任务的执行了。至于时间,大概是因为刚好卡点。

  • 可以看出一台集群的机器执行了job1和job3,那么另一台肯定就只会在同一时间执行了job2
    quartz的学习与使用
    quartz的学习与使用

值得一提的quartz集群分布式问题。

想一下,我们配置redis集群时,是不是需要指定集群的ip地址,而我们使用quartz却并没有指定。

  • 原因其实是因为,quartz的集群是根据连接的数据库来分配的。
  • 也就是说,只要不同机器的quartz连接的是同一个数据库,就会根据数据库中的信息来确认集群关系。

Job的并发执行问题以及Job状态问题

@DisallowConcurrentExecution禁止并发执行的注解作用

  • 作用:顾名思义,禁止并发执行,也就是说一个Trigger在触发任务时,如果上一次任务没有执行完,则等待上一次任务执行完毕后再进行触发执行。

示例(不添加该注解时)

  • 任务类
import org.quartz.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class StatusJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        try {
            // 演示并发问题,主要是任务执行的时间为3秒,但是间隔执行时间为2秒,这个时候会出现的问题有哪些
            String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            System.out.println(nowTime + "  我是job任务,嘿嘿");
            Thread.sleep(3000);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 测试方法
import com.it.job.StatusJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class MyTest {
    public static void main(String[] args) throws Exception {
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.start();

        JobDetail jobDetail = JobBuilder
                .newJob(StatusJob.class)
                .build();

        Trigger trigger = TriggerBuilder
                .newTrigger()
                .startNow()
                .forJob(jobDetail)
                .withSchedule(
                        SimpleScheduleBuilder.repeatSecondlyForever(2)
                )
                .build();

        scheduler.scheduleJob(jobDetail, trigger);
    }
}
  • 执行结果,从结果可以看出,我们一个任务需要执行3秒,但是执行的间隔依然是2秒一次。
    • 问题分析:如果我们第二次任务的执行,需要用到第一次任务执行后传递的数据,那么就会出现数据不一致问题。所以quartz提供了该注解解决此问题。
      quartz的学习与使用

示例(添加该注解时)

  • 只是在Job任务类上添加上@DisallowConcurrentExecution注解,其他的未作改动
    quartz的学习与使用
  • 执行结果,可以看出任务是挨个执行的,没有并发现象
    quartz的学习与使用

@PersistJobDataAfterExecution注解实现Job的有状态(其实就是任务之间的数据传递)

前提概要,一些注意事项

  • 由于Scheduler在帮助执行我们的任务时,每一次通过Trigger触发任务时,都会初始化一个Job,所以导致Job任务之间的数据不共享。

  • 虽然我们可以通过静态变量来解决此问题,但是quartz为我们提供了@PersistJobDataAfterExecution注解来帮我们实现了Job任务之间的数据共享。主要是通过JobDetail的JobDataMap来完成数据的共享。

  • 在Job类上加上该注解时,JobDetail中的JobDataMap中的数据就可以传递给下一个任务,否则无法传递

  • 注意一件事:Trigger中的JobDataMap数据并不会进行传递。

示例(未添加注解)

  • Job任务类
import org.quartz.*;

public class StatusJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
            // 演示数据共享问题
            // 获取自定义的count数据
            JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
            int count = jobDataMap.getInt("count");
            System.out.println("当前Job中的count的值为:" + count);

            // 将JobDataMap中的count数据 加1
            jobDataMap.put("count", ++count);
    }
}
  • 运行测试类(侧重JobDetail中添加的自定义数据count)
import com.it.job.StatusJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;


public class MyTest {
    public static void main(String[] args) throws Exception {
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.start();

        JobDetail jobDetail = JobBuilder
                .newJob(StatusJob.class)
                .usingJobData("count",1)
                .build();

        Trigger trigger = TriggerBuilder
                .newTrigger()
                .startNow()
                .forJob(jobDetail)
                .withSchedule(
                        SimpleScheduleBuilder.repeatSecondlyForever(2)
                )
                .build();

        scheduler.scheduleJob(jobDetail, trigger);
    }
}
  • 运行结果,可以看出数据是不共享的
    quartz的学习与使用

示例(添加了该注解在Job类上时)

quartz的学习与使用

  • 运行结果
    quartz的学习与使用

2个注解配合使用的注意点

  • 如果使用了@PersistJobDataAfterExecution注解,那么就要考虑是否使用@DisallowConcurrentExecution注解。
  • 因为@PersistJobDataAfterExecution注解只有在当前任务执行完毕后数据才会生效。
  • 例如:如果第一个任务还没执行完(数据还没重新更新),这个时候并发执行了第二次该任务,那么就会出现取不到第一次任务触发时更新的数据。
  • 因此使用@PersistJobDataAfterExecution时需要考虑搭配@DisallowConcurrentExecution

misfire失火策略

在讲失火策略之前,需要讲一个quartz的配置

# 最大能忍受的触发超时时间,单位为毫秒,默认为60000(60秒)
# 当任务执行时间到了时,由于某些原因导致的任务没有按时执行,并且超过了最大的触发超时时间,则认定为该任务失火
# 例如任务本应该在 09:00执行,但是却超过了默认的失火时间60秒都没有触发执行,也就是在09:01时还没有执行,则该任务失火
# 这里我为了演示效果改为1000毫秒 (1秒)
org.quartz.jobStore.misfireThreshold = 1000

注意一点: 如果没有超过失火时间(也就是任务没有失火),那么scheduler则会在可触发任务的同一时间直接执行未触发(多个)的任务。

创建一个Job

public class SimpleJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

        System.out.println(nowTime + " 这是一条普通的任务");
    }
}

创建测试类(使用默认)

package com.it.test;

import com.it.job.SimpleJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Date;
import java.util.Calendar;

/**
 * @author dengqixing
 * @date 2021/8/28
 */
public class MyTest {
    public static void main(String[] args) throws SchedulerException {
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.start();

        JobDetail jobDetail = JobBuilder.newJob(SimpleJob.class).build();

        // 使任务在前3秒执行。
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.SECOND,calendar.get(Calendar.SECOND) - 5);
        Date date = calendar.getTime();

        System.out.println(date);
        System.out.println(new Date());


        Trigger trigger = TriggerBuilder
                .newTrigger()
                .startAt(date)
                // 每2秒执行一次,重复3次,所以就是执行4次,
                // 由于这里每2秒执行一次,所以按照如上设 置的前5秒为开始时间,则会失火3次
                .withSchedule(
                        SimpleScheduleBuilder.repeatSecondlyForever(2).withRepeatCount(3)
                )
                .forJob(jobDetail)
                .build();

        scheduler.scheduleJob(jobDetail, trigger);

    }
}

quartz的学习与使用
-- 默认失火策略(不理睬任务的失火,从当前时间重新执行任务)
quartz的学习与使用

记住一点,失火策略的分类,SimpleScheduleBuilder中有6种,CronScheduleBuilder有3种

SimpleScheduleBuilder中有6种(简单讲几种)

withMisfireHandlingInstructionFireNow 不管任务失火几次,只补一次,然后正常执行

quartz的学习与使用

withMisfireHandlingInstructionIgnoreMisfires 有机会就一瞬间执行完所有失火的任务(忽略掉失火,直接执行)

quartz的学习与使用

withMisfireHandlingInstructionNextWithExistingCount 只执行未失火的任务,失火的任务直接抛弃掉了

quartz的学习与使用

withMisfireHandlingInstructionNowWithExistingCount 与默认一致。(不理睬任务的失火,从当前时间重新执行任务)

quartz的学习与使用

当执行的Job任务出现异常

默认处理情况

默认即便任务抛出异常了,那么也会当作一次正确的任务触发,不会影响接下来的任务触发

当任务出现异常时,解决问题并重新调度一次任务

首先编写一个Job任务

@PersistJobDataAfterExecution
public class SimpleJob implements Job {


    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String nowTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

        try {
            // 获取自定义数据boolean类型的flag
            if (context.getJobDetail().getJobDataMap().getBoolean("flag")) {
                System.out.println(nowTime + " 这是一条普通的任务");
            } else {
                throw new Exception("flag不为true");
            }

        } catch (Exception e) {
            // 解决掉异常问题,记得别忘了修改job的状态(添加注解)
            context.getJobDetail().getJobDataMap().put("flag", true);
            // 重新调用一次任务
            JobExecutionException jobExecutionException = new JobExecutionException(e);
            // 立即执行一次
            jobExecutionException.setRefireImmediately(true);
            // 抛出该quartz提供的异常
            throw jobExecutionException;
        }

    }
}

Test测试类

public class MyTest {
    public static void main(String[] args) throws SchedulerException {
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.start();

        JobDetail jobDetail = JobBuilder
                .newJob(SimpleJob.class)
                .usingJobData("flag",false)
                .build();



        Trigger trigger = TriggerBuilder
                .newTrigger()
                .startNow()
                // 每2秒执行一次,一共4次
                .withSchedule(
                        SimpleScheduleBuilder.repeatSecondlyForever(2).withRepeatCount(3)
                )
                .forJob(jobDetail)
                .build();

        scheduler.scheduleJob(jobDetail, trigger);

    }
}

运行结果

quartz的学习与使用

当任务出现异常时,取消之后任务的执行

// 取消所有关联了该Job任务的Trigger的触发
jobExecutionException.setUnscheduleFiringTrigger(true);
// 取消正在触发该任务的Trigger的触发
jobExecutionException.setUnscheduleFiringTrigger(true);

quartz的任务中断,类似于Thread的中断

首先编写一个job,实现InterruptableJob接口

public class MyInterruptJob implements InterruptableJob {

    private boolean interruptFlag;

    @Override
    public void interrupt() throws UnableToInterruptJobException {
        interruptFlag = true;
        System.out.println("该任务已经被中断");
    }

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {

        for (int i = 0; i < 5; i++) {

            if (!interruptFlag) {
                System.out.println("i等于: " + i);

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }
}

编写一个测试类

public class InterruptTest {

    public static void main(String[] args) throws SchedulerException, InterruptedException {
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();
        scheduler.start();

        JobDetail jobDetail = JobBuilder
                .newJob(MyInterruptJob.class)
                .build();



        Trigger trigger = TriggerBuilder
                .newTrigger()
                .startNow()
                .forJob(jobDetail)
                .build();



        scheduler.scheduleJob(jobDetail, trigger);

        // 注意这里睡了2秒,然后中断了任务
        Thread.sleep(2000);
        // 在这儿的作用就是中断第一次任务触发
        scheduler.interrupt(jobDetail.getKey());
    }
}

运行结果

quartz的学习与使用

注意了,interrupt方法只能中断当前正在触发的任务

quartz的日期排除

为什么需要进行日期排除?因为有些时间我们不需要进行任务的触发,例如节假日不上班呐之类的。

首先需要一个Job任务

public class MyJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("Job任务执行了");
    }
}

编写测试类,对了,这里提醒一下,进行日期排除日历是可以相互关联的,通过setBaseCalendar方法

记住步骤

  • 创建日历
  • 添加日历的排除规则
  • 将日历添加到scheduler中
  • 在trigger创建时指定日历
public class QuartzTest {
    public static void main(String[] args) throws SchedulerException {

        // 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();

        // 创建一个日历,这个是quartz提供的,包含月和日,不包含年,还有另外几个就不做阐述了
        AnnualCalendar annualCalendar = new AnnualCalendar();

        // 创建需要被排除的日历 第一个参数为年,其实可以随意填,因为AnnualCalendar 不包含年,这里主要是想要表示为8月28日
        Calendar instance = new GregorianCalendar(2005,Calendar.AUGUST,28);

        // 设置需要排除的日历
        annualCalendar.setDayExcluded(instance,true);

        // 将该日历添加到当前的Scheduler中
        scheduler.addCalendar("holiday",annualCalendar,true,false);

        // 创建任务详情JobDetail,绑定任务,并且指定任务的名称和组名
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class).build();

        // 创建一个触发器,绑定JobDetail
        // 设置了触发器的名称、组名、以及立即开始、并且指定触发规则为5秒执行一次,无限重复
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .startNow()
                // 每10秒执行一次
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10))
                .modifiedByCalendar("holiday")
                .build();



        scheduler.scheduleJob(jobDetail, trigger);

        scheduler.start();
    }
}

可以实现日期排除的5种日历

quartz的学习与使用

监听器

监听器的分类

  1. JobListener

    • public void jobToBeExecuted() 在job任务触发前执行

    • public void jobExecutionVetoed() 在job任务被Trigger的监听器拒绝执行时触发,后续会讲到,当然,执行该方法的时候,就不会再执行该监听器的另外2个方法。

    • public void jobWasExecuted() Job任务执行后执行

  2. TriggerListener

    • public void triggerFired()当与监听器相关联的Trigger被触发,Job上的execute()方法将被执行时,Scheduler就调用该方法。该方法在vetoJobExecution方法之后。

    • public boolean vetoJobExecution() 如果返回true,则当前触发任务的execute方法不执行

    • public void triggerMisfired() 当该触发任务失火时,调用该方法

    • public void triggerComplete() 当任务的execute方法执行完毕后调用该方法

  3. SchedulerListener

    • jobScheduled方法:用于添加部署一个Trigger

    • jobUnscheduled方法:用于卸载一个Trigger触发器

    • triggerFinalized方法:当一个 Trigger 来到了再也不会触发的状态时调用这个方法。除非这个 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。

    • triggersPaused方法:Scheduler 调用这个方法是发生在一个 Trigger 或 Trigger 组被暂停时。假如是 Trigger 组的话,triggerName 参数将为 null。

    • triggersResumed方法:Scheduler 调用这个方法是发生成一个 Trigger 或 Trigger 组从暂停中恢复时。假如是 Trigger 组的话,假如是 Trigger 组的话,triggerName 参数将为 null。参数将为 null。

    • jobsPaused方法:当一个或一组 JobDetail 暂停时调用这个方法。

    • jobsResumed方法:当一个或一组 Job 从暂停上恢复时调用这个方法。假如是一个 Job 组,jobName 参数将为 null。

    • schedulerError方法:在 Scheduler 的正常运行期间产生一个严重错误时调用这个方法。

    • schedulerStarted方法:当Scheduler 开启时,调用该方法

    • schedulerInStandbyMode方法: 当Scheduler处于StandBy模式时,调用该方法

    • schedulerShutdown方法:当Scheduler停止时,调用该方法

    • schedulingDataCleared方法:当Scheduler中的数据被清除时,调用该方法。

监听器的基础使用,这里以JobListener做演示

定义一个job

public class MyJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("Job任务执行了");
    }
}

编写一个监听器

public class MyJobListener implements JobListener {

        @Override
        public String getName() {
            return "我是个假的监听器哦";
        }

        @Override
        public void jobToBeExecuted(JobExecutionContext context) {
            System.out.println("job执行前");
        }

        @Override
        public void jobExecutionVetoed(JobExecutionContext context) {
            System.out.println("job被listener拒绝执行时执行");
        }

        @Override
        public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
            System.out.println("job执行后");

        }
}

测试类

public class QuartzTest {
    public static void main(String[] args) throws SchedulerException {

        // 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
        Scheduler scheduler = new StdSchedulerFactory().getScheduler();

        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("job2","group1").build();
        
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .startNow()
                // 每10秒执行一次
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10))
                .build();

        // 添加监听器
        JobListener jobListener = new MyJobListener();
        scheduler.getListenerManager().addJobListener(jobListener);


        // 部署到scheduler中
        scheduler.scheduleJob(jobDetail, trigger);
        // 开启任务调度
        scheduler.start();
    }
}
提醒一下,添加监听器的时候,可以设置匹配的规则,在添加监听器的时有第二个参数Matcher。

例如:

  • NameMatcher 根据名称匹配,其中有根据job的名称,或是trigger的名称

  • GroupMacher 根据组进行匹配,同样有job和trigger

  • EverythingMatcher 匹配全部的job或是匹配全部的trigger

  • AndMatcher 将两个Matcher相结合,并且是and条件

  • OrMatcher 将两个Matcher相结合,并且是or条件

  • NotMatcher 表示 非 的意思,也就是要求匹配不满足该条件的

quartz中插件的使用

在配置文件中配置插件即可,配置的插件必须是实现了SchedulerPlugin接口的类
quartz为我们提供了几个

使用插件的方法

在quartz的配置文件中进行如下配置,这个插件是quartz提供的,用job的日志

# 配置插件,这个myJobHistory名称是自定义的
org.quartz.plugin.myJobHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin
# 配置该插件的其他属性,至于配置嘛,可以在具体的插件类中找到(成员属性)
org.quartz.plugin.triggerHistory.name=heiheihei

对了,有一个XMLSchedulingDataProcessorPlugin插件,可以通过读取配置文件完成Trigger和JobDetail的装载。

Quartz的其他一些细节

如果需要进行线程的隔离,例如有3个监控任务,而其他的都是正常任务

3个监控任务肯定是要定时执行的。
但是如果其他的正常任务占用了所有的线程,或者是执行时间过长。可能会导致监控任务的失火。
那么这个时候,就可以创建2个Scheduler。每个Scheduler使用一个配置文件

JobDetail中的requestRecovery()方法

1、必须在持久化的方式下运行。
2、效果其实就是如果本次任务的执行,并没有执行完毕,而服务器宕机了,那么当服务器启动后,是否继续执行之前没有执行完毕的任务。

Trigger的优先级

想一个场景,如果只有3个线程,而同一时间有5个任务需要被触发。那么quartz该如何选择先执行谁。
答:按照当前Trigger的优先级。
源码中有一句话:

比较触发器的下一次触发时间的比较器,换句话说,根据最早的下一次触发时间对它们进行排序。 如果触发次数相同,则触发器按优先级排序(最高值在前),如果优先级相同,则按key排序。

quartz的服务端与客户端

服务端配置

服务端:负责执行任务
添加如下配置

# 开启远程方法调用
org.quartz.scheduler.rmi.export: true
# 暴露的地址
org.quartz.scheduler.rmi.registryHost:localhost
# 暴露的端口
org.quartz.scheduler.rmi.registryPort: 1099
# 是否要注册
org.quartz.scheduler.rmi.createRegistry: true

启动服务的测试类

public class ServerTest {
    public static void main(String[] args) throws SchedulerException {

        // 创建Scheduler对象进行任务与Trigger的协调,并开启任务调度
        Scheduler scheduler = new StdSchedulerFactory("server.properties").getScheduler();

        // 开启任务调度
        scheduler.start();
    }
}

客户端的配置

客户端:负责分配任务
添加如下配置

# 开启远程方法代理
org.quartz.scheduler.rmi.proxy: true
# 需要注册到的远程地址
org.quartz.scheduler.rmi.registryHost:localhost
# 需要注册到的远程地址中暴露的端口
org.quartz.scheduler.rmi.registryPort: 1099
  • 服务启动类,这里的这个Job类自己随便写一个就好
public class ClientTest {
    public static void main(String[] args) throws SchedulerException {
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class).build();

        Trigger trigger = TriggerBuilder.newTrigger()
                                        .forJob(jobDetail)
                                        .startNow()
                                        .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(5))
                                        .build();



        Scheduler scheduler = new StdSchedulerFactory("client.properties").getScheduler();
        scheduler.scheduleJob(jobDetail, trigger);

        scheduler.start();
    }
}

运行效果

quartz的学习与使用
quartz的学习与使用

quartz的学习与使用

上一篇:winform 与 html 交互 简单案例


下一篇:【题解】CF1010F Tree