适用于:
动态修改定时任务,根据数据库的定时任务进行任务的激活和暂停,带参定时任务,指定时间和执行次数的定时任务等。
1、概述:
在开发中有的时候需要去手动禁止和启用定时任务,修改定时任务的cron表达式然后再让其动态生效,之前有过SSM的类似的业务的开发但是忘记写下来了。。。只好重新温习了一次,加上最近比较流行springBoot所以升级了一下用springBoot来完成.
2、关联技术:
SpringBoot、Quartz、mysql、thymeleaf (好像就这么多)
3、涉及核心API 类:
Scheduler – 调度器,定时任务的指派和停止、启用、删除、列举都由他得出;
Job – 通过scheduler执行任务,任务类需要实现的接口;
JobDetail – 定义Job的实例;
Trigger – 触发Job的执行,旗下分 CronTrigger 根据Cron表达式动态执行和SimpleTrigger 根据指定时间和间隔去执行;
JobBuilder – 定义和创建JobDetail实例的接口;
TriggerBuilder – 定义和创建Trigger实例的接口;
4、具体流程
1)首先去手动创建一个调度器工厂对象-SchedulerFactoryBean;其实应该不用手动创建的但是为了顾及到业务的复杂性所以还是创建一个好用。
1 @Bean 2 public SchedulerFactoryBean schedulerFactory(){ 3 SchedulerFactoryBean factoryBean = new SchedulerFactoryBean(); 4 /*用于Quartz集群,启动时更新已存在的Job*/ 5 factoryBean.setOverwriteExistingJobs(true); 6 /*定时任务开始启动后延迟5秒开始*/ 7 factoryBean.setStartupDelay(5); 8 return factoryBean; 9 }
2)继承job,实现方法execute。此处手写的原因是因为我们需要对定时任务的功能进行扩展,比如文中提到的参数注入和执行日志记录.注意jobExecutionContext.getMergedJobDataMap() 它可以获得 JobDataMap这个是Job实例化的一些信息,可以用于定时任务的带参。如下:
1 import com.study.www.enums.ConfigEnum; 2 import com.study.www.model.Config; 3 import com.study.www.model.Logger; 4 import com.study.www.model.mapper.LoggerRepository; 5 import com.study.www.utils.TaskUtils; 6 import org.quartz.DisallowConcurrentExecution; 7 import org.quartz.Job; 8 import org.quartz.JobExecutionContext; 9 import org.quartz.JobExecutionException; 10 import org.springframework.scheduling.annotation.Async; 11 import org.springframework.stereotype.Component; 12 13 import java.util.ArrayList; 14 import java.util.List; 15 16 17 //当上一个任务未结束时下一个任务需进行等待 18 @DisallowConcurrentExecution 19 @Component 20 public class MyJob implements Job { 21 22 //注意此处的 Dao 直接 @Autowired 注入是获取不到的,我们可以通过Spring容器去进行手动注入 23 static LoggerRepository loggerRepository; 24 //定时任务日志落地 25 static List<Logger> loggers=new ArrayList<>(); 26 //每10条日志进行一下落地 27 private final static Integer SIZE=9; 28 29 //execute会根据cron的规则进行执行 30 @Override 31 public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { 32 Config config = (Config) jobExecutionContext.getMergedJobDataMap().get(ConfigEnum.SCHEDULEJOB.getCode()); 33 Long beginTime=null; 34 if (config.getIsLogger()){ 35 beginTime=System.currentTimeMillis(); 36 } 37 TaskUtils.invokMethod(config); 38 if (beginTime != null) { 39 //手动注入 Dao 40 if (MyJob.loggerRepository == null){ 41 MyJob.loggerRepository = SpringUtils.getBean("loggerRepository"); 42 } 43 saveSysLog(beginTime,config); 44 } 45 } 46 47 48 @Async 49 void saveSysLog(Long beginTime, Config config) { 50 Logger logger = new Logger(); 51 logger.setBeginTime(beginTime); 52 logger.setEndTime(System.currentTimeMillis()); 53 logger.setClassPath(config.getClassPath()); 54 logger.setMethName(config.getMethodName()); 55 logger.setName(config.getName()); 56 logger.setGroupName(config.getGroup()); 57 logger.setTime(System.currentTimeMillis()-beginTime); 58 logger.setParams(config.getReqParms()); 59 if (loggers.size() > SIZE) { 60 synchronized (MyJob.class) { 61 loggerRepository.save(loggers); 62 loggers.clear(); 63 } 64 }else{ 65 loggers.add(logger); 66 } 67 } 68 69 }
3)获取到调度器-Scheduler和JobBuilder以及TriggerBuilder
1 /***************** 此三者均可复用,类似于 Factory 一样。故提出做公用 ********/ 2 //调度器 3 private static Scheduler scheduler; 4 //JobBuilder 5 private static JobBuilder jobBuilder; 6 //触发器Builder 7 private static TriggerBuilder<Trigger> triggerBuilder ; 8 9 10 //毫秒的间隔时间 11 private static final Integer MILLISECONDS=1000; 12 13 @PostConstruct 14 public void init(){ 15 //因为Job 被自己给实现了故此处应去指定对应的Job类去创建一个调度器 16 QuartzTableManager.jobBuilder= JobBuilder.newJob(MyJob.class); 17 QuartzTableManager.scheduler = schedulerFactoryBean.getScheduler(); 18 triggerBuilder = TriggerBuilder.newTrigger(); 19 }
4)实现动态新增、删除定时任务的方法.此处的 trigger 触发机制有两种 一种是Cron 一种是 选择时间和时间间隔的。因为有一种业务情况为客户去手动选择时间进行定时任务发送。
1 /** 2 * 增加任务 3 * 4 * @param :com.study.www.model.config 5 * @Date: 2018/2/26 9:57 6 * @return: void 7 */ 8 void addJob(Config config) throws SchedulerException { 9 //得到调度器 10 JobKey jobKey = this.getJobKey(config); 11 //获得触发器 12 TriggerKey triggerKey = TriggerKey.triggerKey(config.getName(), config.getGroup()); 13 Trigger trigger = scheduler.getTrigger(triggerKey); 14 //判断触发器是否存在(如果存在说明之前运行过但是在当前被禁用了,如果不存在说明一次都没运行过) 15 if (trigger == null) { 16 //新建一个工作任务 指定任务类型为串接进行的 17 JobDetail jobDetail = jobBuilder.withIdentity(jobKey).build(); 18 //将工作添加到工作任务当中去 19 JobDataMap jobDataMap = jobDetail.getJobDataMap(); 20 jobDataMap.put(ConfigEnum.SCHEDULEJOB.getCode(), config); 21 trigger = getTrigger(config, triggerKey); 22 //在调度器中将触发器和任务进行组合 23 scheduler.scheduleJob(jobDetail, trigger); 24 } else { 25 //按照新的规则进行 26 trigger = getTrigger(config, triggerKey); 27 //重启 28 scheduler.rescheduleJob(triggerKey, trigger); 29 } 30 } 31 32 /** 33 * 注意 simpleTrigger 和 cronTrigger 互斥只可以选择一个 34 */ 35 private Trigger getTrigger(Config config,TriggerKey triggerKey ){ 36 Trigger trigger =null; 37 if(config.getCount() == null){ 38 //将cron表达式进行转换 39 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(config.getCron()); 40 trigger = triggerBuilder.withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build(); 41 }else{ 42 //指定的时间和时间段来进行定时任务 43 MutableTrigger build = SimpleScheduleBuilder.simpleSchedule() 44 .withRepeatCount(config.getCount()) 45 .withIntervalInMilliseconds(config.getIntervalSecond() * MILLISECONDS) 46 .build(); 47 build.setStartTime(config.getStartTime()); 48 build.setEndTime(config.getEndTime()); 49 build.setKey(triggerKey); 50 trigger = (Trigger)build; 51 } 52 return trigger; 53 } 54 55 /** 56 * 删除任务 57 * 58 * @param : com.study.www.model.config 59 * @Date: 2018/2/24 18:23 60 * @return: void 61 */ 62 void deleteJob(Config config) throws SchedulerException { 63 //找到key值 64 JobKey jobKey = this.getJobKey(config); 65 //从触发器找到此任务然后进行删除 66 scheduler.deleteJob(jobKey); 67 } 68 69 /** 70 * 根据name和group得到任务的key 71 * 72 * @param :com.study.www.model.config 73 * @Date: 2018/2/24 18:27 74 * @return: org.quartz.JobKey 75 */ 76 JobKey getJobKey(Config config) { 77 return getJobKey(config.getName(), config.getGroup()); 78 } 79 80 JobKey getJobKey(String name, String group) { 81 return JobKey.jobKey(name, group); 82 }
5)创建对定时任务的方法进行反射执行的方法。如下:
1 import com.alibaba.fastjson.JSON; 2 import com.alibaba.fastjson.JSONObject; 3 import com.study.www.config.SpringUtils; 4 import com.study.www.model.Config; 5 import org.springframework.core.LocalVariableTableParameterNameDiscoverer; 6 7 import java.lang.reflect.Method; 8 import java.lang.reflect.Parameter; 9 10 public class TaskUtils { 11 12 private static LocalVariableTableParameterNameDiscoverer localVariableTableParameterNameDiscoverer; 13 14 public static void invokMethod(Config config) { 15 Object obj = null; 16 Class clazz = null; 17 //通过Spring上下文去找 也有可能找不到 18 try { 19 obj = SpringUtils.getBean(config.getClassPath().split("\\.")[config.getClassPath().split("\\.").length - 1]); 20 if (obj == null) { 21 clazz = Class.forName(config.getClassPath()); 22 obj = clazz.newInstance(); 23 } else { 24 clazz = obj.getClass(); 25 } 26 } catch (Exception e) { 27 throw new RuntimeException("ERROR:TaskUtils is Bean Create please check the classpath is`t right or not"); 28 } 29 //方法执行 30 try { 31 //入参 32 JSONObject jsonObject = null; 33 String reqParms = config.getReqParms(); 34 if (reqParms != null && reqParms.length() > 0) { 35 jsonObject = JSON.parseObject(reqParms); 36 } 37 //获得方法名 38 Method[] methods = clazz.getMethods(); 39 for (Method method1 : methods) { 40 //不带参的 41 if ((config.getMethodName().equals(method1.getName()) && jsonObject== null) ){ 42 method1.invoke(obj); 43 return; 44 } 45 //带参的 46 if ((config.getMethodName().equals(method1.getName()) && method1.getParameterCount()== jsonObject.size()) ){ 47 Object[] params = new Object[method1.getParameterCount()]; 48 if (localVariableTableParameterNameDiscoverer == null){ 49 localVariableTableParameterNameDiscoverer= new LocalVariableTableParameterNameDiscoverer(); 50 } 51 String[] parameterNames = localVariableTableParameterNameDiscoverer.getParameterNames(method1); 52 Parameter[] parameters = method1.getParameters(); 53 for (int i = 0; i < parameterNames.length; i++) { 54 Class<?> type = parameters[i].getType(); 55 Object object = jsonObject.getObject(parameterNames[i], type); 56 params[i]=object; 57 } 58 method1.invoke(obj,params); 59 return; 60 } 61 } 62 } catch (Exception e) { 63 throw new RuntimeException("ERROR:TaskUtils is Bean the method execute please check the methodName is`t right or not"); 64 } 65 } 66 }
提示: 页面用户修改Cron 表达式如果需要对Cron表达式进行校验可以使用如下方法。
//Cron表达式解析 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(config.getCron());
提示: 如果定时任务的带参为不固定的比如是另外的一个请求等,可以修改 步骤 5中的规则。例如可以在Config 中加个用来进行参数类型判断的字段,然后参数为 一个http请求这样可以做到在定时任务中进行一个请求后再任务的执行。
若时间充裕推荐这封博客其对Quartz将的更加详细,配合示例看可以有着事半功倍的效果。http://ifeve.com/quartz-tutorial-using-quartz/
代码例子:
码云:https://gitee.com/zhuyanpengWorld/springboots/blob/master/quartz.rar