springboot + quartz 可视化 前后端分离版本 附源码
quartz介绍
本文是一个quartz的demo版本,下载源码后刷表并修改配置文件数据库连接即可运行。
quartz基本对象
-
Scheduler:调度器,start()之后才可以正常调度任务。 Jobdetail :维护job信息的对象,通过jobName,jobGroupName确定获得唯一任务对象。 CronTrigger:cron表达式类型触发器,通过triggerName,triggerGroupName确定获得唯一任务对象,根据cron表达式触发job。 JobDataMap相当于每个jobdetail的本地变量,可以存储key-value值。
demo基本功能描述
-
定时任务增删改查 立即执行 暂停/恢复
demo页面效果图
-
列表页 任务新增/编辑
demo后端实现过程
1.新建springboot 工程并引入所需依赖
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.44</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <!--换成自己的数据库驱动--> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.0.0</version> </dependency>
2. 3.配置quartz&mybatis
server: port: xxxx spring: #quartz属性 quartz: properties: org: quartz: #quartz数据库连接 dataSource: qzDS: URL: ***** driver: org.postgresql.Driver maxConnections: 10 password: ***** user: ***** #quartzjob存储方式 jobStore: #jdbc持久化 class: org.quartz.impl.jdbcjobstore.JobStoreTX #数据源连接名 dataSource: qzDS #驱动代表 driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate #触发器忍耐时间 超过5s则错过 misfireThreshold: 5000 #表前缀 tablePrefix: QRTZ_ useProperties: false scheduler: instanceName: DEMO_JOBS_1.0 threadPool: class: org.quartz.simpl.SimpleThreadPool makeThreadsDaemons: true threadCount: 5 threadPriority: 5 threadsInheritContextClassLoaderOfInitializingThread: true datasource: url: ***** username: ***** password: ***** driver-class-name: org.postgresql.Driver mybatis: mapper-locations: classpath*:mapper/*.xml type-aliases-package: com.demo.task.entity
4.实现定时任务增删改查
-
新增mapper与对应的xml文件,对于quartz的表只推荐查询操作。
package com.demo.job.mapper; import com.demo.job.entity.JobAndTrigger; import java.util.List; public interface JobAndTriggerMapper { List<JobAndTrigger> getJobAndTriggerDetails(); void updateTriggerPreTriggerTime(Long time); Integer queryJobByNameAndGroupName(String jobName,String jobGroupName); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.demo.job.mapper.JobAndTriggerMapper"> <resultMap id="jobdetail" type="com.demo.job.entity.JobAndTrigger"> <result property="jobName" jdbcType="VARCHAR" column="JOB_NAME"/> <result property="jobGroup" jdbcType="VARCHAR" column="JOB_GROUP"/> <result property="jobClassName" jdbcType="VARCHAR" column="JOB_CLASS_NAME"/> <result property="triggerName" jdbcType="VARCHAR" column="TRIGGER_NAME"/> <result property="triggerGroup" jdbcType="VARCHAR" column="TRIGGER_GROUP"/> <result property="prevFireTime" jdbcType="VARCHAR" column="PREV_FIRE_TIME"/> <result property="nextFireTime" jdbcType="VARCHAR" column="NEXT_FIRE_TIME"/> <result property="cronExpression" jdbcType="VARCHAR" column="CRON_EXPRESSION"/> <result property="triggerState" jdbcType="VARCHAR" column="TRIGGER_STATE"/> </resultMap> <select id="queryJobByNameAndGroupName" resultType="java.lang.Integer"> SELECT COUNT(1) FROM qrtz_job_details WHERE JOB_NAME = #{jobName} AND JOB_GROUP = #{jobGroupName} AND SCHED_NAME=DEMO_JOBS_1.0 </select> <select id="getJobAndTriggerDetails" resultMap="jobdetail"> SELECT a.JOB_NAME, a.JOB_GROUP, a.JOB_CLASS_NAME, a.JOB_DATA, b.TRIGGER_NAME, b.TRIGGER_GROUP, case WHEN b.PREV_FIRE_TIME =-1 THEN - else to_char(to_timestamp(b.PREV_FIRE_TIME/1000),yyyy-MM-dd HH24:MI:SS) END as PREV_FIRE_TIME, to_char(to_timestamp(b.NEXT_FIRE_TIME/1000),yyyy-MM-dd HH24:MI:SS) as NEXT_FIRE_TIME, b.TRIGGER_STATE, c.CRON_EXPRESSION, c.TIME_ZONE_ID FROM qrtz_job_details a LEFT JOIN qrtz_triggers b ON a.JOB_NAME = b.JOB_NAME AND b.TRIGGER_GROUP = a.JOB_GROUP LEFT JOIN qrtz_cron_triggers c ON b.TRIGGER_NAME = c.TRIGGER_NAME AND b.TRIGGER_GROUP = c.TRIGGER_GROUP </select> <update id="updateTriggerPreTriggerTime" parameterType="java.lang.Long"> update qrtz_triggers set PREV_FIRE_TIME = #{time} </update> </mapper>
entity
package com.demo.job.entity; import lombok.Data; /** * @description: * @author: jiangjie * @date: 2019/12/24 */ @Data public class JobAndTrigger { private String jobName; private String jobGroup; private String jobClassName; private String triggerName; private String triggerGroup; private String cronExpression; private String triggerState; private String prevFireTime; private String nextFireTime; }
-
新增service与controller
package com.demo.job.service.impl; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.demo.job.singleton.SystemCache; import com.demo.job.constants.CommonConstant; import com.demo.job.entity.JobAndTrigger; import com.demo.job.entity.TaskInfo; import com.demo.job.mapper.JobAndTriggerMapper; import com.demo.job.service.IJobService; import com.demo.job.vo.Result; import org.quartz.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.util.HashMap; import java.util.List; import java.util.Map; @Service public class JobServiceImpl implements IJobService { private static Logger log = LoggerFactory.getLogger(JobServiceImpl.class); @Autowired private Scheduler scheduler; @Resource private JobAndTriggerMapper jobAndTriggerMapper; @Override public Result getJobAndTriggerList(int pageNum, int pageSize) { Map<String, Object> map = new HashMap<>(); try { PageHelper.startPage(pageNum, pageSize); List<JobAndTrigger> list = jobAndTriggerMapper.getJobAndTriggerDetails(); PageInfo<JobAndTrigger> page = new PageInfo<JobAndTrigger>(list); List<TaskInfo> taskInfoList = (List<TaskInfo>) SystemCache.getInstance().getCacheMap().get(CommonConstant.TASK_CACHE_KEY); map.put("JobAndTriggerList", page.getList()); map.put("number", page.getTotal()); map.put("taskList", taskInfoList); } catch (Exception e) { e.printStackTrace(); Result.FAIL("查询失败"); } return Result.OK(map); } @Override public Result createJobAndTrigger(String jobClassName, String jobName, String jobGroupName,String cronExpression) { //构建job信息 JobDetail jobDetail; try { jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass()).withIdentity(jobName, jobGroupName).build(); //表达式调度构建器(即任务执行的时间) CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); //按新的cronExpression表达式构建一个新的trigger CronTrigger trigger = TriggerBuilder .newTrigger() .withIdentity(jobName, jobGroupName) .withSchedule(scheduleBuilder).build(); scheduler.scheduleJob(jobDetail, trigger); return Result.OK(); } catch (Exception e) { e.printStackTrace(); log.error("创建定时任务失败 ", e); return Result.FAIL("创建定时任务失败"); } } @Override public Result pauseJob(String jobName, String jobGroupName) { try { scheduler.pauseJob(JobKey.jobKey(jobName, jobGroupName)); return Result.OK(); } catch (SchedulerException e) { e.printStackTrace(); log.error("暂停定时任务失败", e); return Result.FAIL("暂停定时任务失败"); } } @Override public Result resumeJob(String jobClassName, String jobGroupName) { try { scheduler.resumeJob(JobKey.jobKey(jobClassName, jobGroupName)); return Result.OK(); } catch (SchedulerException e) { e.printStackTrace(); log.error("恢复定时任务失败", e); return Result.FAIL("恢复定时任务失败"); } } @Override public Result rescheduleJob(String jobName, String jobGroupName, String cronExpression) { try { TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName); CronTrigger cronTrigger = rebulidTriggerKey(jobName, jobGroupName, cronExpression); scheduler.rescheduleJob(triggerKey, cronTrigger); return Result.OK(); } catch (SchedulerException e) { e.printStackTrace(); log.error("更新定时任务失败", e); return Result.FAIL("更新定时任务失败"); } } @Override public Result deleteJob(String jobClassName, String jobGroupName) { try { scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName)); scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName)); scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName)); return Result.OK(); } catch (SchedulerException e) { e.printStackTrace(); log.error("删除定时任务失败", e); return Result.FAIL("删除定时任务失败"); } } @Override public Result triggerJobAtOnce(String jobName, String jobGroupName) { JobKey jobKey = new JobKey(jobName, jobGroupName); try { scheduler.triggerJob(jobKey); return Result.OK(); } catch (SchedulerException e) { log.error("立即执行任务时发生异常", e); return Result.FAIL("立即执行任务时发生异常"); } } @Override public Result validateJobNameAndGroupName(String jobName, String jobGroupName) { return Result.OK(jobAndTriggerMapper.queryJobByNameAndGroupName(jobName, jobGroupName) == 0); } private Job getClass(String classname) throws Exception { Class<?> clazz = Class.forName(classname); return (Job) clazz.newInstance(); } private CronTrigger rebulidTriggerKey(String jobName, String jobGroupName, String cronExpression) { //重置crontrigger TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName); // 表达式调度构建器 CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression); CronTrigger trigger = null; try { trigger = (CronTrigger) scheduler.getTrigger(triggerKey); } catch (SchedulerException e) { e.printStackTrace(); } // 按新的cronExpression表达式重新构建trigger trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build(); return trigger; } }
package com.demo.job.controller; import com.demo.job.service.IJobService; import com.demo.job.vo.JobAddOrUpdateReqVo; import com.demo.job.vo.JobOptReqVo; import com.demo.job.vo.Result; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping(value = "/job") public class JobController { @Autowired private IJobService jobService; @ResponseBody @PostMapping(value = "/addjob") public Result addjob(@RequestBody JobAddOrUpdateReqVo jobAddOrUpdateReqVo) { return jobService.createJobAndTrigger( jobAddOrUpdateReqVo.getJobClassName(), jobAddOrUpdateReqVo.getJobName(), jobAddOrUpdateReqVo.getJobGroupName(), jobAddOrUpdateReqVo.getCronExpression()); } @PostMapping(value = "/pausejob") public Result pausejob(@RequestBody JobOptReqVo jobOptReqVo) { return jobService.pauseJob(jobOptReqVo.getJobName(), jobOptReqVo.getJobGroupName()); } @PostMapping(value = "/resumejob") public Result resumejob(@RequestBody JobOptReqVo jobOptReqVo) { return jobService.resumeJob(jobOptReqVo.getJobName(), jobOptReqVo.getJobGroupName()); } @PostMapping(value = "/reschedulejob") public Result rescheduleJob(@RequestBody JobAddOrUpdateReqVo jobAddOrUpdateReqVo) { return jobService.rescheduleJob( jobAddOrUpdateReqVo.getJobName(), jobAddOrUpdateReqVo.getJobGroupName(), jobAddOrUpdateReqVo.getCronExpression()); } @PostMapping(value = "/deletejob") public Result deletejob(@RequestBody JobOptReqVo jobOptReqVo) { return jobService.deleteJob(jobOptReqVo.getJobName(), jobOptReqVo.getJobGroupName()); } @GetMapping(value = "/queryjob") public Result queryjob(@RequestParam(value = "pageNum") Integer pageNum, @RequestParam(value = "pageSize") Integer pageSize) { return jobService.getJobAndTriggerList(pageNum, pageSize); } @PostMapping(value = "/triggerjob") public Result triggerJobAtOnce(@RequestBody JobOptReqVo jobOptReqVo) { return jobService.triggerJobAtOnce(jobOptReqVo.getJobName(), jobOptReqVo.getJobGroupName()); } @PostMapping(value = "/validateJobNameAndGroupName") public Result validateJobNameAndGroupName(@RequestBody JobOptReqVo jobOptReqVo) { return jobService.validateJobNameAndGroupName(jobOptReqVo.getJobName(), jobOptReqVo.getJobGroupName()); } }
- 自定义任务名称实现
-
新增注解定义task任务名称
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TaskNode { String taskName(); }
-
新增系统缓存,用单例维护一个map缓存。
public class SystemCache { private SystemCache() { } private static SystemCache sysCache = new SystemCache(); private Map<String, Object> mapCache = new HashMap(); public static SystemCache getInstance() { return sysCache; } public Map<String, Object> getCacheMap() { return mapCache; } }
-
新增ApplicationRunner监听器系统启动时扫描task包下所有任务并放入缓存,查询任务列表时从缓存取出返回
@Component public class AppStaredListener implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { //获取系统缓存 Map<String, Object> cacheMap = SystemCache.getInstance().getCacheMap(); //获取task包下的所有任务 Set<Class<?>> taskSet = ClassUtil.getClasses("com.demo.job.task"); List<TaskInfo> taskInfoList = new ArrayList<>(); taskSet.stream().forEach(t -> { TaskNode taskNode = t.getAnnotation(TaskNode.class); if (taskNode != null) { TaskInfo taskInfo = new TaskInfo(); taskInfo.setTaskClassName(t.getName()); taskInfo.setTaskName(taskNode.taskName()); taskInfoList.add(taskInfo); } }); //将定时任务信息放入缓存中 cacheMap.put(CommonConstant.TASK_CACHE_KEY, taskInfoList); } }
6.新增任务
-
新增定时任务,加上注解 execute 为具体任务逻辑
@TaskNode(taskName = "普通任务2") public class Demo2Task implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("我是普通任务2"); } }
demo启动
运行JobApplication 访问http://127.0.0.1:8088/JobPage.html 新建定时任务并立即执行