Quartz定时任务

Qquartz定时任务

一、任务调度概念

1.1 什么是任务调度

任务调度是为了自动完成特定任务,在约定时刻去执行任务的过程。

如:

  • 每个月初对用户进行缴费提醒
  • 每天定时12:00给用户发放优惠券

1.2 为什么要使用分布式调度

在spring中提供了注解@Scheduled,也能够实现任务调度的功能。具体步骤:

  • 在业务类的方法上加上注解,写上cron表达式:
java 复制代码
@Scheduled(cron = "0/20 * * * * ?")
public void method(){
	//do task
}
  • 在启动类上加上@EnableScheduling注解开启顶定时调度功能

spring提供了单机版的定时任务调度,那为什么还要使用分布式定时任务Quartz呢?

  1. 高可用:
    单机版的定时任务调度只能在一台机器上运行,如果程序或者系统出现异常就会导致功能不可用。
  2. 方式重复执行:
    当我们部署了多台服务器,同时又每台都有定时任务,若不进行合理的控制确保只有一个定时任务启动执行,定时执行的结果就会存在混乱和错误。(如积分的重复累加)
  3. 单机处理极限:
    单机能力有限(CPU、内存和磁盘),会存在单机处理不过来的情况

二、Quartz快速上手

  1. 引入包
java 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
  1. 测试案例
java 复制代码
  import org.quartz.Scheduler;
  import org.quartz.SchedulerException;
  import org.quartz.impl.StdSchedulerFactory;
  import static org.quartz.JobBuilder.*;
  import static org.quartz.TriggerBuilder.*;
  import static org.quartz.SimpleScheduleBuilder.*;

  public class QuartzTest {

      public static void main(String[] args) {

          try {
              // 使用 StdSchedulerFactory.getDefaultScheduler() 方法从标准工厂中获取默认的调度器实例
              Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

              // 使用 scheduler.start() 方法启动调度器,使其能够调度和执行任务
              scheduler.start();
              
			//使用 scheduler.shutdown() 方法关闭调度器,停止所有正在进行的任务调度
              scheduler.shutdown();

          } catch (SchedulerException se) {
              se.printStackTrace();
          }
      }
  }
  1. 输出:

    到此,运行环境配置完成。

  2. 添加调度任务

任务类:

java 复制代码
package com.example.quartz_task.job;

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

public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("hello world");
    }
}
java 复制代码
package com.guo.quartztest;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import static org.quartz.JobBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;

public class QuartzTest {

    public static void main(String[] args) {

        try {
            // Grab the Scheduler instance from the Factory
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // and start it off
            scheduler.start();

            // 定义一个任务并将其与HelloJob类关联
            JobDetail job = newJob(HelloJob.class)  // 使用newJob方法创建一个JobDetail实例,指定任务类为HelloJob
                    .withIdentity("job1", "group1") // 设置任务的名称为"job1",组名为"group1"
                    .build();  // 构建JobDetail实例

            // 定义一个触发器,立即运行任务,然后每40秒重复一次
            Trigger trigger = newTrigger()  // 使用newTrigger方法创建一个Trigger实例
                    .withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
                    .startNow()  // 设置触发器立即生效
                    .withSchedule(simpleSchedule()  // 设置触发器的调度计划
                            .withIntervalInSeconds(40) // 设置任务执行的时间间隔为40秒
                            .repeatForever()) // 设置任务无限次重复执行
                    .build();  // 构建Trigger实例

            // 告诉Quartz使用我们的触发器调度任务
            scheduler.scheduleJob(job, trigger);  // 使用scheduleJob方法将任务和触发器注册到调度器


            scheduler.shutdown();

        } catch (SchedulerException se) {
            se.printStackTrace();
        }
    }
}

输出:


上面的任务调度流程是:定义任务和触发器;然后通过调度器关联任务和触发器。

我们也可以不通过调度器关联任务和触发器,在定义触发器的时候就指定触发的任务。

任务调度流程变成:

  • 定义任务:
    代码种添加.storeDurably()。将Job 设置为"持久的",不会因为没有 Trigger 而从调度器中移除。
    在没有立即调度的情况下添加一个 Job,必须将Job标记为持久的
  • 定义触发器并关联任务
    代码中添加.forJob("job","group1")
  • 使用调度器启动触发器。
    代码添加:scheduler.addJob(job, false); 将一个 JobDetail对象添加到调度器中。如果不这样做,调度器不会记录该 Job,因此在你试图调度或执行这个 Job时,调度器将无法找到它,并可能抛出异常。

代码如下:

java 复制代码
			// Grab the Scheduler instance from the Factory
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // and start it off
            scheduler.start();

            // 定义一个任务并将其与HelloJob类关联
            JobDetail job = newJob(HelloJob.class)  // 使用newJob方法创建一个JobDetail实例,指定任务类为HelloJob
                    .storeDurably()//设置为持久的
                    .withIdentity("job", "group1") // 设置任务的名称为"job1",组名为"group1"
                    .build();  // 构建JobDetail实例

            // 定义一个触发器,立即运行任务,然后每40秒重复一次
            Trigger trigger = newTrigger()  // 使用newTrigger方法创建一个Trigger实例
                    .withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
                    .forJob("job","group1")
                    .startNow()  // 设置触发器立即生效
                    .withSchedule(simpleSchedule()  // 设置触发器的调度计划
                            .withIntervalInSeconds(40) // 设置任务执行的时间间隔为40秒
                            .repeatForever()) // 设置任务无限次重复执行
                    .build();  // 构建Trigger实例


            //将job告诉调度器
            scheduler.addJob(job,false);

            // 告诉Quartz使用我们的触发器调度任务
            scheduler.scheduleJob(trigger); 

            Thread.sleep(20);

            scheduler.shutdown();

上面涉及到的任务、调度器、触发器的逻辑框架如下图:

  1. 创建任务调度器,去启动触发器
  2. 触发器通过相应的规则,去调度任务
  3. 任务的一些属性包装在JobDetail里

调度器、触发器、任务详情之间的关系:

  • 调度器可以调度多个触发器
  • 触发器只能调度一个任务
  • 一个JobDetail可以被多个触发器调度
  • 一个Job可以关联多个JobDetail

三、CronExpression

在定义触发器的时候,可以使用Cron表达式来替代函数式的定义触发规则:

java 复制代码
// 定义一个触发器,立即运行任务,然后每40秒重复一次
            Trigger trigger = newTrigger()  // 使用newTrigger方法创建一个Trigger实例
                    .withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
                    .startNow()  // 设置触发器立即生效
                    .withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ? *")) // 设置任务无限次重复执行
                    .build();  // 构建Trigger实例

* * * * * ? *解读:

符合解释:


四、传入变量,依赖注入

4.1 传入变量

场景:需要在任务调度的时候,给任务Job传递一些参数

  1. 使用usingJobData
    • JobDetail的usingJobData
    • 也可在trigger上使用usingJobData
java 复制代码
// 定义一个任务并将其与HelloJob类关联
JobDetail job = newJob(HelloJob.class)  // 使用newJob方法创建一个JobDetail实例,指定任务类为HelloJob
	.withIdentity("job1", "group1") // 设置任务的名称为"job1",组名为"group1"
	.usingJobData("key1","value1")//传递参数
	.build();  // 构建JobDetail实例
java 复制代码
// 定义一个触发器,立即运行任务,然后每40秒重复一次
            Trigger trigger = newTrigger()  // 使用newTrigger方法创建一个Trigger实例
                    .withIdentity("trigger1", "group1") // 设置触发器的名称为"trigger1",组名为"group1"
                    .usingJobData("key2","value2")
                    .startNow()  // 设置触发器立即生效
                    .withSchedule(simpleSchedule()  // 设置触发器的调度计划
                            .withIntervalInSeconds(40) // 设置任务执行的时间间隔为40秒
                            .repeatForever()) // 设置任务无限次重复执行
                    .build();  // 构建Trigger实例
  1. 任务类
java 复制代码
package com.example.quartz_task.job;

import org.quartz.*;

public class HelloJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
		//通过上下文获取任务详情
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
		//通过上下文获取触发器详情
        Trigger trigger = jobExecutionContext.getTrigger();
		
		//获取数据
        System.out.println(jobDetail.getJobDataMap().get("key1"));
        System.out.println(trigger.getJobDataMap().get("key2"));
    }
}

输出:

此外也可以通过jobExecutionContext.getMergedJobDataMap().get("key");来获取相应的键值对数据。

如果detailJob和trigger存在相同的key呢?

jobExecutionContext.getMergedJobDataMap()会获取哪一个的?

这里我们将两者的key都改成"key",最后输出的是"value2",因此在detailJob和trigger都存在相同的键,会优先获取trigger的键值对值。

如果某个key是经常使用的,可以将其作为Job类的属性。而不用通过get方法去获取了。

4.2 依赖注入

以上的方式都是固定死的传值方式。在实际应用中,我们通常会结合spring以获取一个动态变化的值。

如,下面存在一个service类,在实际应用中,希望将这个service的返回结果作为参数传入。

java 复制代码
package service;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
    public String hello(){
        return "say hello";
    }
}

在任务类:

java 复制代码
@Component
public class HelloJob implements Job {

    @Autowired
    private HelloService helloService;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(helloService);
    }
}

但是发现在启动Spring项目后,输出的却是null:

这是因为,触发器在触发任务的时候,quartz会对Job进行实例化;因为实例化是由quartz实例化的,因此Spring注解这些quartz是不能够识别的,因此不存在依赖注入。

我们可以通过Spring的上下文去获取的这个服务的输出:

  1. 先构建一个能够让外部获取上下文的类
java 复制代码
package com.example.quartz_task.util;

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 {
    public static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
  1. 通过类获取Spring的bean对象
java 复制代码
package com.example.quartz_task.job;

import com.example.quartz_task.util.SpringContextUtil;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.example.quartz_task.service.HelloService;
@Component
public class HelloJob implements Job {

    @Autowired
    private HelloService helloService;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        //通过Spring上下文获取Bean对象
        helloService = SpringContextUtil.applicationContext.getBean(HelloService.class);
        System.out.println(helloService.hello());
    }
}

输出:


五、Quartz配置

quartz存在一个配置文件quartz.properties,如果自己不定义就会采取默认值。quartz.properties内容如下:

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

#指定调度器实例的名称,默认值为 DefaultQuartzScheduler。
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

#指定线程池中的线程数,默认值为 10。
org.quartz.threadPool.threadCount: 10

#指定线程池中线程的优先级,范围是 1(最低)到 10(最高)。这里的优先级设置为 5。
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

#指定 Quartz 处理触发器错失(misfire)时间的阈值,单位为毫秒。这里设置为 60000 毫秒(即 60 秒)。
org.quartz.jobStore.misfireThreshold: 60000

#指定 Quartz 的作业存储类型,这里使用 RAMJobStore,即将所有调度数据保存在内存中,而不是持久化到数据库。
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore

配置参数详细信息可以看官网配置信息介绍

六、SpringBoot整合Quartz

官网文档

根据官网,Spring Boot会自动为我们配置调度器,所以我们不需要通过工厂的方式去定义一个调度器了。

接下来只需要我们手动的定义JobDetail和Trigger的Bean,Spring Boot会自动获取并与Scheduler关联。

6.1 定义Job和trigger--第一种方式

java 复制代码
class MySJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        
    }
}

QuartzJobBean是实现了Job的一个抽象类,里面对原来的execute进行了修改(添加了Spring的一些东西),然后再调用executeInternal。源码如下,因此我们从原来的方式改成:继承QuartzJobBean并重新executeInternal

public abstract class QuartzJobBean implements Job {
public QuartzJobBean() {
}

public final void execute(JobExecutionContext context) throws JobExecutionException {
    try {
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.addPropertyValues(context.getScheduler().getContext());
        pvs.addPropertyValues(context.getMergedJobDataMap());
        bw.setPropertyValues(pvs, true);
    } catch (SchedulerException var4) {
        throw new JobExecutionException(var4);
    }

    this.executeInternal(context);
}

protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
}

详细步骤:

  1. 定义Job
java 复制代码
package com.example.quartz_task.job;


import com.example.quartz_task.service.HelloService;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;


public class MyJob extends QuartzJobBean {

    @Autowired
    private HelloService helloService;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        System.out.println(helloService.hello());
    }
}
  1. 定义JobDetail和Trigger
java 复制代码
package com.example.quartz_task.config;

import com.example.quartz_task.job.MyJob;
import jakarta.annotation.PostConstruct;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class QuartzConfig {
    @Autowired
    private Scheduler scheduler;

    @PostConstruct
    public void initJob() throws SchedulerException {
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class).build();

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

        scheduler.scheduleJob(jobDetail,trigger);
    }
}
  1. Spring的服务类
java 复制代码
package com.example.quartz_task.service;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
    public String hello(){
        return "say hello";
    }
}
  1. 启动服务,输出:

官网中,提到了:

6.1 定义Job和trigger--第二种方式

所以提供了第二种编写JobDtail和Trigger的方式:

java 复制代码
package com.example.quartz_task.config;

import com.example.quartz_task.job.MyJob;
import jakarta.annotation.PostConstruct;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

@Configuration
public class QuartzConfig {
   @Bean
    public JobDetail SpringJobDetail(){
       return JobBuilder.newJob(MyJob.class)
               .withIdentity("springJobDetail")
               .storeDurably()
               .build();
   }

   @Bean
    public Trigger springJobTrigger(){
       return TriggerBuilder.newTrigger()
               .forJob("springJobDetail")
               .startNow()
               .build();
   }
}

七、Quartz持久化

相关推荐
Theodore_10222 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸3 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象3 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了4 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·4 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic4 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王4 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康5 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神5 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式
qq_327342735 小时前
Java实现离线身份证号码OCR识别
java·开发语言