在日常的工作中,定时任务也是一个非常常见的需求,可以使用注解实现,但是使用这种方式有几个问题,首先就是如果修改比较麻烦,而且没有提供特定的页面对定时任务进行可视化管理。所以quartz就应运而生。本文将介绍如何实现springboot与quartz的整合。
1.目录结构
- 配置类 配置SchedulerFactory
- 固化定时任务和日志的业务类
- 定时任务类
- 运行配置
2.原理
- scheduler是一个计划调度器容器(总部),容器里面可以盛放众多的JobDetail和trigger,当容器启动后,里面的每个JobDetail都会根据trigger按部就班自动去执行。
- JobDetail是一个可执行的工作,它本身可能是有状态的。
- Trigger代表一个调度参数的配置,什么时候去调。
- 当JobDetail和Trigger在scheduler容器上注册后,形成了装配好的作业(JobDetail和Trigger所组成的一对儿),就可以伴随容器启动而调度执行了。
- scheduler是个容器,容器中有一个线程池,用来并行调度执行每个作业,这样可以提高容器效率
3.表结构
自带有11张系统表和2张业务表(自建),其中绿色为自己建立的表
4.整合springboot
1.pom文件
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> </dependency>
2.ScheduleConfig
配置工厂类的参数。
@Configuration public class ScheduleConfig { @Bean public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setDataSource(dataSource); // quartz参数 Properties prop = new Properties(); prop.put("org.quartz.scheduler.instanceName", "QUARTZ"); prop.put("org.quartz.scheduler.instanceId", "AUTO"); // 线程池配置 prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); prop.put("org.quartz.threadPool.threadCount", "20"); prop.put("org.quartz.threadPool.threadPriority", "5"); // JobStore配置 prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); // 集群配置 prop.put("org.quartz.jobStore.isClustered", "true"); prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); // sqlserver 启用 prop.put("org.quartz.jobStore.misfireThreshold", "12000"); prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); factory.setQuartzProperties(prop); factory.setSchedulerName("QUARTZ"); // 延时启动 factory.setStartupDelay(1); factory.setApplicationContextSchedulerContextKey("applicationContextKey"); // 可选,QuartzScheduler // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 factory.setOverwriteExistingJobs(true); // 设置自动启动,默认为true factory.setAutoStartup(true); return factory; } }
3.业务类
主要是为页面提供接口。
@RestController @RequestMapping("/qrtzJob") public class QrtzJobController { @Autowired private QrtzJobService qrtzJobService; /** * 检查表达式 * * @param cron * @return */ @GetMapping("/check") public boolean checkCron(String cron) { try { return CronExpression.isValidExpression(cron); } catch (Exception e) { return false; } } /** * 通过对象查询数据 * * @param qrtzJobVO 请求对象 * @return 数据集合 */ @GetMapping("/selectQrtzJob") @ApiOperation(value = "通过实体查询", notes = "通过实体查询") public Result<List<QrtzJobVO>> selectQrtzJob(@ApiParam(name = "入参对象", value = "实体", required = false) QrtzJobVO qrtzJobVO) { Result<List<QrtzJobVO>> resultVO = new Result<>(); // 业务查询 List<QrtzJobVO> resultListQrtzJob = qrtzJobService.queryQrtzJobAll(qrtzJobVO); resultVO.setData(resultListQrtzJob); return resultVO; } /** * 通过对象增加数据 * * @param qrtzJobVO 请求对象 * @return */ @PostMapping("/createQrtzJob") @ApiOperation(value = "通过实体添加", notes = "通过实体添加") public Result createQrtzJob(@ApiParam(name = "入参对象", value = "实体", required = false) @RequestBody QrtzJobVO qrtzJobVO) { Result resultVO = new Result(); // 业务处理 qrtzJobService.createScheduleJob(qrtzJobVO); return resultVO; } /** * 通过对象修改 * * @param qrtzJobVO 请求对象 * @return */ @PutMapping("/updateQrtzJob") @ApiOperation(value = "通过实体修改", notes = "通过实体修改") public Result updateQrtzJob(@ApiParam(name = "入参对象", value = "实体", required = false) @RequestBody QrtzJobVO qrtzJobVO) { Result resultVO = new Result(); // 业务处理 qrtzJobService.updateScheduleJob(qrtzJobVO); return resultVO; } /** * 通过对象删除 * * @param qrtzJobVO 请求对象 * @return */ @DeleteMapping("/deleteQrtzJob") @ApiOperation(value = "通过实体删除", notes = "通过实体删除") public Result deleteQrtzJob(@ApiParam(name = "入参对象", value = "实体", required = false) @RequestBody QrtzJobVO qrtzJobVO) { Result resultVO = new Result(); // 业务处理 qrtzJobService.deleteScheduleJob(qrtzJobVO); return resultVO; } /** * 运行定时任务 * * @param qrtzJobVO * @return */ @GetMapping("/runJob") public Result runJob(@ApiParam(name = "入参对象", value = "实体", required = false) @RequestBody List<QrtzJobVO> qrtzJobVO) { Result resultVO = new Result(); // 业务处理 qrtzJobService.run(qrtzJobVO); return resultVO; } /** * 暂停定时任务 * * @param qrtzJobVO * @return */ @GetMapping("/pauseJob") public Result pauseJob(@ApiParam(name = "入参对象", value = "实体", required = false) @RequestBody List<QrtzJobVO> qrtzJobVO) { Result resultVO = new Result(); // 业务处理 qrtzJobService.pause(qrtzJobVO); return resultVO; } @GetMapping("resumeJob") public Result resumeJob(@ApiParam(name = "入参对象", value = "实体", required = false) @RequestBody List<QrtzJobVO> qrtzJobVO) { Result resultVO = new Result(); // 业务处理 qrtzJobService.resume(qrtzJobVO); return resultVO; } }
4.运行配置
ScheduleUtils:将新添加的trigger,jobDetail放入scheduler中。
public class ScheduleUtils { private static final Logger log = LoggerFactory.getLogger(ScheduleUtils.class); private static final String JOB_NAME_PREFIX = "TASK_"; private static TriggerKey getTriggerKey(Long jobId) { return TriggerKey.triggerKey("TASK_" + jobId); } private static JobKey getJobKey(Long jobId) { return JobKey.jobKey("TASK_" + jobId); } public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) { try { return (CronTrigger)scheduler.getTrigger(getTriggerKey(jobId)); } catch (SchedulerException e) { log.error("获取Cron表达式失败", e); return null; } } public static void createScheduleJob(Scheduler scheduler, QrtzJobVO scheduleJob) { try { JobDetail jobDetail = JobBuilder.newJob(com.job.util.ScheduleJob.class) .withIdentity(getJobKey(scheduleJob.getJobId())).build(); CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression()) .withMisfireHandlingInstructionDoNothing(); CronTrigger trigger = (CronTrigger)TriggerBuilder.newTrigger() .withIdentity(getTriggerKey(scheduleJob.getJobId())).withSchedule(scheduleBuilder).build(); jobDetail.getJobDataMap().put("JOB_PARAM_KEY", scheduleJob); scheduler.scheduleJob(jobDetail, trigger); if (scheduleJob.getStatus().equals(CommonConstant.QUARTZ_PAUSE)) { pauseJob(scheduler, scheduleJob.getJobId()); } } catch (SchedulerException e) { log.error("创建定时任务失败", e); } } public static void updateScheduleJob(Scheduler scheduler, QrtzJobVO scheduleJob) { try { TriggerKey triggerKey = getTriggerKey(scheduleJob.getJobId()); CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression()) .withMisfireHandlingInstructionDoNothing(); CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getJobId()); if (trigger == null) { throw new SchedulerException("获取Cron表达式失败"); } trigger = (CronTrigger)trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); trigger.getJobDataMap().put("JOB_PARAM_KEY", scheduleJob); scheduler.rescheduleJob(triggerKey, trigger); if (scheduleJob.getStatus().equals(CommonConstant.QUARTZ_PAUSE)) { pauseJob(scheduler, scheduleJob.getJobId()); } } catch (SchedulerException e) { log.error("更新定时任务失败", e); } } public static void run(Scheduler scheduler, QrtzJobVO scheduleJob) { try { JobDataMap dataMap = new JobDataMap(); dataMap.put("JOB_PARAM_KEY", scheduleJob); scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap); } catch (SchedulerException e) { log.error("执行定时任务失败", e); } } public static void pauseJob(Scheduler scheduler, Long jobId) { try { scheduler.pauseJob(getJobKey(jobId)); } catch (SchedulerException e) { log.error("暂停定时任务失败", e); } } public static void resumeJob(Scheduler scheduler, Long jobId) { try { scheduler.resumeJob(getJobKey(jobId)); } catch (SchedulerException e) { log.error("恢复定时任务失败", e); } } public static void deleteScheduleJob(Scheduler scheduler, Long jobId) { try { scheduler.deleteJob(getJobKey(jobId)); } catch (SchedulerException e) { log.error("删除定时任务失败", e); } } }
5.ScheduleJob
用于执行定时任务。
public class ScheduleJob extends QuartzJobBean { private static final Logger log = LoggerFactory.getLogger(ScheduleJob.class); private ExecutorService service = Executors.newSingleThreadExecutor(); @Override protected void executeInternal(JobExecutionContext context) { QrtzJobVO scheduleJob = (QrtzJobVO)context.getMergedJobDataMap().get("JOB_PARAM_KEY"); QrtzJobLogService scheduleJobLogService = (QrtzJobLogService) SpringContextUtil.getBean(QrtzJobLogService.class); QrtzJobLogVO jobLog = new QrtzJobLogVO(); jobLog.setJobId(scheduleJob.getJobId()); jobLog.setBeanName(scheduleJob.getBeanName()); jobLog.setMethodName(scheduleJob.getMethodName()); if (CommonUtil.isEmpty(scheduleJob.getParams())) { jobLog.setParams(""); } else { jobLog.setParams(scheduleJob.getParams()); } long startTime = System.currentTimeMillis(); try { log.info("任务准备执行,任务ID:{}", scheduleJob.getJobId()); ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(), scheduleJob.getParams()); Future<?> future = this.service.submit(task); future.get(); long times = System.currentTimeMillis() - startTime; jobLog.setTimes(Long.valueOf(times)); jobLog.setStatus("0"); log.info("任务执行完毕,任务ID:{} 总共耗时:{} 毫秒", scheduleJob.getJobId(), Long.valueOf(times)); } catch (Exception e) { log.error("任务执行失败,任务ID:" + scheduleJob.getJobId(), e); long times = System.currentTimeMillis() - startTime; jobLog.setTimes(Long.valueOf(times)); jobLog.setStatus("1"); jobLog.setError(StringUtils.substring(e.toString(), 0, 2000)); } finally { scheduleJobLogService.insertQrtzJobLog(jobLog); } } }
6.ScheduleRunnable
通过反射回去需要调用的类与方法。
public class ScheduleRunnable implements Runnable { private static final Logger log = LoggerFactory.getLogger(ScheduleRunnable.class); private Object target; private Method method; private String params; ScheduleRunnable(String beanName, String methodName, String params) throws NoSuchMethodException, SecurityException { this.target = SpringContextUtil.getBean(beanName); this.params = params; if (StringUtils.isNotBlank(params)) { this.method = this.target.getClass().getDeclaredMethod(methodName, new Class[] {String.class}); } else { this.method = this.target.getClass().getDeclaredMethod(methodName, new Class[0]); } } @Override public void run() { try { ReflectionUtils.makeAccessible(this.method); if (StringUtils.isNotBlank(this.params)) { this.method.invoke(this.target, new Object[] {this.params}); } else { this.method.invoke(this.target, new Object[0]); } } catch (Exception e) { log.error("执行定时任务失败", e); } } }
5.使用
调用以上接口 ,参数如下。
"beanName":"任务类",
"methodName":"任务方法",
"params":"参数",
"cronExpression":"表达式",