前瞻
导入依赖
<!--引入quartz定时框架-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
先了解几个概念:
- Job,即要定时执行的 工作,要继承QuartzJobBean类
- trigger,触发器,用来触发Job
- Schedule,定时的时间表
我们可能有很多个定时任务,并且可能会随时更改,所以我们是肯定不能编写很多个Job,这样耦合太大。
所以我们应该再编写一些Task,让Task作为我们真正去执行的定时任务的代码。然后用一个Job去调用Task,并在task执行完毕后进行一些log记录等等操作。这样就能得到低耦合的代码
然后所有的Task的信息都应该放在数据库中,方便管理保存
那么Job整么知道要去执行哪个task呢?
Job继承了QuartzJobBean类,要实现这个方法
executeInternal(JobExecutionContext context)
其中context参数就可以给我们提供这些信息
初步理解过程
我们先拿手动触发一次定时任务为例
前端给我们传来了要执行的定时任务的id,我们再根据id查询定时任务的具体信息
public void runJobById(Long jobId) {
ScheduleUtils.run(scheduler, schedulerJobMapper.getJobById(jobId));
}
其中scheduler是SpringBoot帮我们创建好的bean,我们只需要注入一下就行。我们要靠他才能调用定时任务
@Autowired
Scheduler scheduler;
查询用的实体类
public class ScheduleJob {
public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY"; //任务调度参数key
private Long jobId;//任务id
private String beanName;//spring bean名称
private String methodName;//方法名
private String params;//参数
private String cron;//cron表达式
private Boolean status;//任务状态
private String remark;//备注
private Date createTime;//创建时间
}
手动触发的方法
public static void run(Scheduler scheduler, ScheduleJob scheduleJob) {
try {
//参数
JobDataMap dataMap = new JobDataMap();
dataMap.put(ScheduleJob.JOB_PARAM_KEY, scheduleJob);
scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
} catch (SchedulerException e) {
throw new RuntimeException("立即执行定时任务失败", e);
}
}
可以看到我们把task的相关信息放到了Map中,在scheduler调用triggerJob时把要执行的Job
public class ScheduleJob extends QuartzJobBean {
private ExecutorService service = Executors.newSingleThreadExecutor();
@Override
protected void executeInternal(JobExecutionContext context) {
top.naccl.entity.ScheduleJob scheduleJob = (top.naccl.entity.ScheduleJob) context.getMergedJobDataMap().get(top.naccl.entity.ScheduleJob.JOB_PARAM_KEY);
//获取spring bean
ScheduleJobService scheduleJobService = (ScheduleJobService) SpringContextUtils.getBean("scheduleJobServiceImpl");
//数据库保存任务执行记录
ScheduleJobLog jobLog = new ScheduleJobLog();
jobLog.setJobId(scheduleJob.getJobId());
jobLog.setBeanName(scheduleJob.getBeanName());
jobLog.setMethodName(scheduleJob.getMethodName());
jobLog.setParams(scheduleJob.getParams());
jobLog.setCreateTime(new Date());
//任务开始时间
long startTime = System.currentTimeMillis();
//执行任务
log.info("任务准备执行,任务ID:{}", scheduleJob.getJobId());
try {
ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(), scheduleJob.getParams());
Future<?> future = service.submit(task);
future.get();
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
jobLog.setTimes((int) times);
//任务执行结果
jobLog.setStatus(true);
log.info("任务执行成功,任务ID:{},总共耗时:{} 毫秒", scheduleJob.getJobId(), times);
} catch (Exception e) {
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
jobLog.setTimes((int) times);
//任务执行结果
jobLog.setStatus(false);
jobLog.setError(e.toString());
log.error("任务执行失败,任务ID:{}", scheduleJob.getJobId(), e);
} finally {
scheduleJobService.saveJobLog(jobLog);
}
}
}
ScheduleJob是将实际的操作交给了ScheduleRunnable来进行的。
public class ScheduleRunnable implements Runnable {
private Object target;
private Method method;
private String params;
public ScheduleRunnable(String beanName, String methodName, String params) throws NoSuchMethodException, SecurityException {
this.target = SpringContextUtils.getBean(beanName);
this.params = params;
if (StringUtils.hasText(params)) {
this.method = target.getClass().getDeclaredMethod(methodName, String.class);
} else {
this.method = target.getClass().getDeclaredMethod(methodName);
}
}
@Override
public void run() {
try {
ReflectionUtils.makeAccessible(method);
if (StringUtils.hasText(params)) {
method.invoke(target, params);
} else {
method.invoke(target);
}
} catch (Exception e) {
throw new RuntimeException("执行定时任务失败", e);
}
}
}
现在我们就实现了只通过ScheduleJob一个继承了QuartzJobBean类 就能处理多个不同的定时任务。
是利用了反射去执行这些Task的
容易混淆的地方
上面的代码中有一个继承了QuartzJobBean的ScheduleJob和实体类的ScheduleJob,只是名字一样而已,不要搞混了
完整的实现
从上面的代码中我们已经实现了将数据库中得到Job信息,并通过ScheduleJob这个继承了QuartzJobBean类来进行定时任务。
现在我们在系统初始化的时候就把Task的信息放到schedule中。
@PostConstruct
public void init() {
List<ScheduleJob> scheduleJobList = getJobList();
for (ScheduleJob scheduleJob : scheduleJobList) {
CronTrigger cronTrigger = ScheduleUtils.getCronTrigger(scheduler, scheduleJob.getJobId());
//如果不存在,则创建
if (cronTrigger == null) {
ScheduleUtils.createScheduleJob(scheduler, scheduleJob);
} else {
ScheduleUtils.updateScheduleJob(scheduler, scheduleJob);
}
}
}
创建
public static void createScheduleJob(Scheduler scheduler, ScheduleJob scheduleJob) {
try {
//构建job信息
// 构建时Job都是用的top.naccl.util.quartz.ScheduleJob.class,只是Identity用的是不同Jobkey
// 这样无论我们调用的是哪个Identity的Job,最终都会调用top.naccl.util.quartz.ScheduleJob.class
JobDetail jobDetail = JobBuilder.newJob(top.naccl.util.quartz.ScheduleJob.class).withIdentity(getJobKey(scheduleJob.getJobId())).build();
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCron()).withMisfireHandlingInstructionDoNothing();
//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(scheduleJob.getJobId())).withSchedule(scheduleBuilder).build();
//放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(ScheduleJob.JOB_PARAM_KEY, scheduleJob);
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduleJob.getStatus()) {
pauseJob(scheduler, scheduleJob.getJobId());
}
} catch (SchedulerException e) {
throw new RuntimeException("创建定时任务失败", e);
}
}
暂停
scheduler.pauseJob(getJobKey(jobId));
更新操作
public static void updateScheduleJob(Scheduler scheduler, ScheduleJob scheduleJob) {
try {
TriggerKey triggerKey = getTriggerKey(scheduleJob.getJobId());
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCron()).withMisfireHandlingInstructionDoNothing();
CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getJobId());
//按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//参数
trigger.getJobDataMap().put(ScheduleJob.JOB_PARAM_KEY, scheduleJob);
scheduler.rescheduleJob(triggerKey, trigger);
if (!scheduleJob.getStatus()) {
pauseJob(scheduler, scheduleJob.getJobId());
}
} catch (SchedulerException e) {
throw new RuntimeException("更新定时任务失败", e);
}
}