文章目录

  • ​​Quartz介绍​​
  • ​​自定义CommandLineRunner类:​​
  • ​​创建、更新定时任务​​
  • ​​service层​​
  • ​​自定义QuartzJobBean​​
  • ​​智能调度组件​​
  • ​​定时任务实体类:​​
  • ​​mapper接口:​​
  • ​​时间触发器的一些写法​​

之前与定时任务相关的一个文章记录:
​springboot使用@Scheduled作定时任务详细用法​​

Quartz介绍

Quartz是一个完全由java编写的功能丰富的开源作业调度库,可以集成到几乎任何Java应用程序中,小到独立应用程序,大到大型的电子商务系统。Quartz可以用来创建执行数十,数百乃至数万个作业的简单或复杂的计划;作业的任务被定义为标准的Java组件,它可以执行几乎任何你可能编程的任务。而且Quartz Scheduler包含许多企业级功能,例如支持JTA事务和集群。

了解一下Quartz中涉及到的几个类概念:

  • SchedulerFactory:调度器工厂。这是一个接口,用于调度器的创建和管理。示例中使用的是Quartz中的默认实现。
  • Scheduler:任务调度器。它表示一个Quartz的独立运行容器,里面注册了多个触发器(Trigger)和任务实例(JobDetail)。两者分别通过各自的组(group)和名称(name)作为容器中定位某一对象的唯一依据,因此组和名称必须唯一(触发器和任务实例的组和名称可以相同,因为对象类型不同)。
  • Job:是一个接口,只有一个方法​​void execute(JobExecutionContext context)​​,开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。
  • JobDetail:Job实例。Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。
  • Trigger
    :触发器,描述触发Job执行的时间触发规则。
  • SimpleTrigger:当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择。
  • CronTrigger:通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等。
    整个Quartz运行调度的流程如下:
  1. 通过触发器工厂(SchedulerFactory的实现类)创建一个调度器(Scheduler);
  2. 创建一个任务实例(JobDetail),为它指定实现了Job接口的实现类(示例中的HelloWord.class),并指定唯一标识(Identity)组(示例中的“group1”)和名称(示例中的“myJob”);
  3. 创建一个触发器(Trigger),为它指定时间触发规则(示例中的​​simpleSchedule()​​生成的SimpleTrigger),并指定唯一标识(Identity)组(示例中的“group1”)和名称(示例中的“myTrigger”)
  4. 最后通过调度器(Scheduler)将任务实例(JobDetail)和触发器(Trigger)绑定在一起,并通过​​start()​​方法开启任务调度。

自定义CommandLineRunner类:

关于CommandLineRunner:平常开发中有可能需要实现在项目启动后执行的功能,SpringBoot提供的一种简单的实现方案就是添加一个model并实现CommandLineRunner接口,实现功能的代码放在实现的run方法中
这个类的run方法String… args是应用启动的时候可以传进来的参数,有两种方式可以传参

一种是命令行的方式传参,所以这个接口叫CommandLineRunner
另一种方法是通过IntelliJ IDEA配置参数
命令行传参
首先将应用打成jar包,然后运行如下命令行,这里传入三个参数

java -jar MyProject.jar my name is

或者在idea中配置:

Quartz详解和使用CommandLineRunner在项目启动时初始化定时任务_spring

Quartz详解和使用CommandLineRunner在项目启动时初始化定时任务_java_02

代码:

import com.itheima.pinda.entity.ScheduleJobEntity;
import com.itheima.pinda.mapper.ScheduleJobMapper;
import com.itheima.pinda.utils.ScheduleUtils;
import lombok.extern.slf4j.Slf4j;
import org.quartz.CronTrigger;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.List;

/**
* 在项目启动时初始化定时任务
*/
@Component
@Slf4j
public class DispatchCommandLineRunner implements CommandLineRunner {
@Autowired
private Scheduler scheduler;
@Autowired
private ScheduleJobMapper scheduleJobMapper;

@Override
public void run(String... args) throws Exception {
log.info("开始进行定时任务初始化...");
//查询定时任务表schedule_job中所有的数据
List<ScheduleJobEntity> list = scheduleJobMapper.selectList(null);//查询数据库
for (ScheduleJobEntity scheduleJobEntity : list) {
//获得触发器对象,调用ScheduleUtils中的方法
CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJobEntity.getId());
if(cronTrigger == null){
//触发器对象为空,说明对应的定时任务还没有创建
ScheduleUtils.createScheduleJob(scheduler,scheduleJobEntity);
}else{
//触发器对象不为空,说明对应的定时任务已经存在了,此时只需要更新
ScheduleUtils.updateScheduleJob(scheduler,scheduleJobEntity);
}
}
}
}

创建、更新定时任务

上面自定义的CommandLineRunner类中调用了工具类的方法来真正运行任务,需要写一个定时任务工具类,方便通过jobid得到触发器key和jobkey、获得表达式触发器
在这里创建、更新定时任务、立即执行某个特定任务

/**
* 定时任务工具类
*
* @author
*/
public class ScheduleUtils {
private final static String JOB_NAME = "TASK_";
/**
* 任务调度参数key
*/
public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY";

/**
* 获取触发器key
*/
public static TriggerKey getTriggerKey(String jobId) {
return TriggerKey.triggerKey(JOB_NAME + jobId);
}

/**
* 获取jobKey
*/
public static JobKey getJobKey(String jobId) {
return JobKey.jobKey(JOB_NAME + jobId);
}

/**
* 获取表达式触发器
*/
public static CronTrigger getCronTrigger(Scheduler scheduler, String jobId) {
try {
return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
} catch (SchedulerException e) {
throw new PdException("getCronTrigger ERROR", e);
}
}

/**
* 创建定时任务
*/
public static void createScheduleJob(Scheduler scheduler, ScheduleJobEntity scheduleJob) {
try {
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getId())).build();

//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();

//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(scheduleJob.getId())).withSchedule(scheduleBuilder).build();

//放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(JOB_PARAM_KEY, scheduleJob);

scheduler.scheduleJob(jobDetail, trigger);

//暂停任务
if (scheduleJob.getStatus() == ScheduleStatus.PAUSE.getValue()) {
pauseJob(scheduler, scheduleJob.getId());
}
} catch (SchedulerException e) {
throw new PdException("CREATE ERROR", e);
}
}

/**
* 更新定时任务
*/
public static void updateScheduleJob(Scheduler scheduler, ScheduleJobEntity scheduleJob) {
try {
TriggerKey triggerKey = getTriggerKey(scheduleJob.getId());

//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();

CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getId());

//按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();

//参数
trigger.getJobDataMap().put(JOB_PARAM_KEY, scheduleJob);

scheduler.rescheduleJob(triggerKey, trigger);

//暂停任务
if (scheduleJob.getStatus() == ScheduleStatus.PAUSE.getValue()) {
pauseJob(scheduler, scheduleJob.getId());
}

} catch (SchedulerException e) {
throw new PdException("UPDATE ERROR", e);
}
}

/**
* 立即执行任务
*/
public static void run(Scheduler scheduler, ScheduleJobEntity scheduleJob) {
try {
//参数
JobDataMap dataMap = new JobDataMap();
dataMap.put(JOB_PARAM_KEY, scheduleJob);

scheduler.triggerJob(getJobKey(scheduleJob.getId()), dataMap);
} catch (SchedulerException e) {
throw new PdException("RUN ERROR", e);
}
}

/**
* 暂停任务
*/
public static void pauseJob(Scheduler scheduler, String jobId) {
try {
scheduler.pauseJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new PdException("PAUSE ERROR", e);
}
}

/**
* 恢复任务
*/
public static void resumeJob(Scheduler scheduler, String jobId) {
try {
scheduler.resumeJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new PdException("RESUME ERROR", e);
}
}

/**
* 删除定时任务
*/
public static void deleteScheduleJob(Scheduler scheduler, String jobId) {
try {
scheduler.deleteJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new PdException("DELETE ERROR", e);
}
}
}

service层

@Service
public class ScheduleJobServiceImpl extends ServiceImpl<ScheduleJobMapper, ScheduleJobEntity> implements IScheduleJobService {
@Autowired
private Scheduler scheduler;
// 在service层中调用ScheduleUtils.run的方法来立即执行任务
@Override
@Transactional(rollbackFor = Exception.class)
public void run(String[] ids) {
for (String id : ids) {
ScheduleUtils.run(scheduler, baseMapper.selectById(id));
}
}
}

在controller中调用service的方法

@PutMapping("/run/{id}")
@ApiOperation("立即执行")
public Result run(@PathVariable String id) {
scheduleJobService.run(new String[]{id});

return Result.ok();
}

自定义QuartzJobBean

【QuartzJobBean】:
Quartz Job 接口的简单实现,应用传入的 JobDataMap 和 SchedulerContext 作为 bean 属性值。 这是合适的,因为每次执行都会创建一个新的 Job 实例。 JobDataMap 条目将使用相同的键覆盖 SchedulerContext 条目。
例如,假设 JobDataMap 包含一个值为“5”的键“myParam”:然后,Job 实现可以公开一个 int 类型的 bean 属性“myParam”来接收这样的值,即方法“setMyParam(int)” . 这也适用于复杂类型,如业务对象等。
在上面的 ScheduleUtils中使用JobBuilder.newJob(ScheduleJob.class)构建了job信息,需要拓展QuartzJobBean自定义一个定时任务类
ScheduleJob 继承自QuartzJobBean,程序会进入executeInternal来执行定时任务

/**
* 定时任务类,进行智能调度操作
*/
public class ScheduleJob extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(getClass());

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
ScheduleJobEntity scheduleJob =
(ScheduleJobEntity) context.getMergedJobDataMap().
get(ScheduleUtils.JOB_PARAM_KEY);
System.out.println(new Date() + "定时任务开始执行...,定时任务id:" + scheduleJob.getId());

//记录定时任务相关的日志信息
//封装日志对象
ScheduleJobLogEntity logEntity = new ScheduleJobLogEntity();
logEntity.setId(IdUtils.get());
logEntity.setJobId(scheduleJob.getId());
logEntity.setBeanName(scheduleJob.getBeanName());
logEntity.setParams(scheduleJob.getParams());
logEntity.setCreateDate(new Date());

long startTime = System.currentTimeMillis();

try{
//通过反射调用目标对象,在目标对象中封装智能调度核心逻辑
logger.info("定时任务准备执行,任务id为:{}",scheduleJob.getId());

//获得目标对象
Object target = SpringContextUtils.getBean(scheduleJob.getBeanName());
//获得目标方法对象
Method method = target.getClass().getDeclaredMethod("run", String.class, String.class, String.class, String.class);

//通过反射调用目标对象的方法
method.invoke(target,scheduleJob.getBusinessId(),scheduleJob.getParams(),scheduleJob.getId(),logEntity.getId());

logEntity.setStatus(1);//成功
}catch (Exception ex){
logEntity.setStatus(0);//失败
logEntity.setError(ExceptionUtils.getErrorStackTrace(ex));
logger.error("定时任务执行失败,任务id为:{}",scheduleJob.getId());
}finally {
int times = (int) (System.currentTimeMillis() - startTime);
logEntity.setTimes(times);//耗时

IScheduleJobLogService scheduleJobLogService = SpringContextUtils.getBean(IScheduleJobLogService.class);
scheduleJobLogService.save(logEntity);
}
}
}

智能调度组件

编写智能调度组件DispatchTask,封装的是智能调度的核心逻辑,此类中需要提供run方法,在前面的ScheduleJob定时任务中通过反射调用此run方法来完成智能调度。

/**
* 智能调度组件
*/
@Slf4j
@Component("dispatchTask")
public class DispatchTask{
/**
* 智能调度
* @param businessId
* @param params
* @param jobId
* @param logId
*/
public void run(String businessId, String params, String jobId, String logId) {
log.info("智能调度正在执行,参数为:{},{},{},{}", businessId, params, jobId, logId);
}
}

定时任务实体类:

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("schedule_job")
public class ScheduleJobEntity implements Serializable {

/**
* id
*/
@TableId(value = "id", type = IdType.INPUT)
private String id;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long creator;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createDate;
/**
* spring bean名称
*/
private String beanName;
/**
* 参数
*/
private String params;
/**
* cron表达式
*/
private String cronExpression;
/**
* 任务状态 0:暂停 1:正常
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 业务id 机构id
*/
private String businessId;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updater;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateDate;
}

mapper接口:

/**
* 定时任务 Mapper 接口
*/
@Component
@Mapper
public interface ScheduleJobMapper extends BaseMapper<ScheduleJobEntity> {

}

定时任务在配置成功后启动时会查询数据库,在控制台输出类似如下的相应内容:(会查询数据库,执行用户定义的定时任务)

Quartz详解和使用CommandLineRunner在项目启动时初始化定时任务_定时任务_03

时间触发器的一些写法

具体时间设定可参考
“0/10 * * * * ?” 每10秒触发
“0 0 12 * * ?” 每天中午12点触发
“0 15 10 ? * *” 每天上午10:15触发
“0 15 10 * * ?” 每天上午10:15触发
"0 15 10 * * ? " 每天上午10:15触发
“0 15 10 * * ? 2005” 2005年的每天上午10:15触发
“0 * 14 * * ?” 在每天下午2点到下午2:59期间的每1分钟触发
“0 0/5 14 * * ?” 在每天下午2点到下午2:55期间的每5分钟触发
“0 0/5 14,18 * * ?” 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
“0 0-5 14 * * ?” 在每天下午2点到下午2:05期间的每1分钟触发
“0 10,44 14 ? 3 WED” 每年三月的星期三的下午2:10和2:44触发
“0 15 10 ? * MON-FRI” 周一至周五的上午10:15触发
“0 15 10 15 * ?” 每月15日上午10:15触发
“0 15 10 L * ?” 每月最后一日的上午10:15触发
“0 15 10 ? * 6L” 每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6L 2002-2005” 2002年至2005年的每月的最后一个星期五上午10:15触发
“0 15 10 ? * 6#3” 每月的第三个星期五上午10:15触发
每隔5秒执行一次:
/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?