1. 引言
quartz对数据库支持非常强大,但是用起来并没有SpringTask那么简单。
因此,个人造了一点小轮子,让SpringTask拥有类似quartz的功能(当然没有那么完善)。
转载请注明出处,欢迎留言交流。
2. 数据表设计
设计思路:
exec_time字段提供对固定时间执行一次的支持,也可以通过cron字段,实现任意触发时间。
method_name字段表示需要触发的方法名;
args则是method_name对应方法的参数值;
args_type则是args的具体类型(暂时仅支持基本数据类型以及包装类)。
3. 代码
注入TaskScheduler
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
//线程池大小
scheduler.setPoolSize(10);
//线程名字前缀
scheduler.setThreadNamePrefix("spring-task-thread");
return scheduler;
}
}
执行引擎
@Component
public class JobEngine {
@Autowired
TaskScheduler scheduler;
public void execute(Runnable runnable, Date date){
// SpringTask不支持年 Seconds Minutes Hours DayofMonth Month DayofWeek
String cron = DateUtil.format(date, "ss mm HH dd MM ?");
scheduler.schedule(runnable,new CronTrigger(cron));
}
public void execute(Runnable runnable, String cron){
scheduler.schedule(runnable,new CronTrigger(cron));
}
}
执行器
/**
* 定时扫描数据库执行任务
*/
@Slf4j
@Component
public class JobService {
@Autowired
AppJobMapper jobMapper;
@Autowired
JobEngine jobEngine;
@Scheduled(cron = "*/5 * * * * ?")
public void execute() {
// 查询出所有未执行的任务 isExec=0 && now<execTime
List<AppJob> jobs = jobMapper.findTask();
if (jobs.size() != 0) {
jobs.forEach(job -> {
// 获取数据库数据
String[] strArgs = job.getArgs().split(",");
Date date = job.getExecTime();
String methodName = job.getMethodName();
String[] classTypes = job.getArgsType().split(",");
// 反射装配参数以及对应类型
List<Class<?>> argsTypeList = new ArrayList<>();
List<Object> argsList = new ArrayList<>();
for(int i=0;i<strArgs.length;i++){
String classType = classTypes[i];
if ("string".equalsIgnoreCase(classType)) {
classType = "java.lang.String";
argsList.add(String.valueOf(strArgs[i]));
}else if ("integer".equalsIgnoreCase(classType) || "int".equalsIgnoreCase(classType)) {
classType = "java.lang.Integer";
argsList.add(Integer.valueOf(strArgs[i]));
}else if ("long".equalsIgnoreCase(classType)) {
classType = "java.lang.Long";
argsList.add(Long.valueOf(strArgs[i]));
}else if ("double".equalsIgnoreCase(classType)) {
classType = "java.lang.Double";
argsList.add(Double.valueOf(strArgs[i]));
}else if ("boolean".equalsIgnoreCase(classType)) {
classType = "java.lang.Boolean";
argsList.add(Boolean.valueOf(strArgs[i]));
}
try {
Class<?> aClass = Class.forName(classType);
argsTypeList.add(aClass);
} catch (ClassNotFoundException e) {
log.error("只支持基本的数据类型以及包装类,非法类型:{}",classType);
}
}
// List转为Array,invoke要求
Object[] args = new Object[argsList.size()];
Class<?>[] classes = new Class[argsTypeList.size()];
argsTypeList.toArray(classes);
argsList.toArray(args);
// 创建定时任务
log.info("创建定时任务{}", job);
Runnable runnable = () -> {
// 可能已经被执行,检查isExec是否为1
if (jobMapper.beforeExec(job.getJobId()) !=null) {
log.info("定时任务{}已被执行", job);
return;
}
try {
Method method = this.getClass().getMethod(methodName, classes);
if (strArgs.length > 0) {
method.invoke(this, args);
} else {
method.invoke(this);
}
// 更新状态 设isExec=1
if (jobMapper.afterExec(job.getJobId()) != 1) {
log.error("定时任务{}执行完毕后状态更新失败", job);
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
log.error("定时任务{}执行完毕失败:{}", job,e.getMessage());
}
};
if (date == null) {
jobEngine.execute(runnable, job.getCron());
} else {
jobEngine.execute(runnable, date);
}
});
}
}
public void test(Integer a, Double b) {
System.out.println("扫描数据库执行测试方法,参数:" + a + b);
}
}
4. 用途
- 将时间跨度较高的任务加到数据表中(比如一个月执行一次),由JobService.execute方法,定时扫描数据库执行,能够避免服务停止导致的定时任务丢失。
- 可以将JobService.execute方法中的代码抽出,作为其他工具类使用(比如按扫描频度、执行功能等增加多种不同的execute)
- 本人目前的使用步骤:
- 在JobService类中添加可能的方法
- 在满足创建定时任务的地方,通过JobMapper创建定时任务保存到数据库
- 设置JobService.execute扫描时间
5. 待完善:
-
如果扫描频度范围内可能多次扫描数据库(比如上述方法我设置的是每5秒执行一次扫描,那么在数据表中的人物肯定会被多次扫描),那么任务会被重复创建。可以通过队列解决
-
若要支持非基本类型,可以考虑传入Class类型
-
反射执行的方法必须是JobService中的方法,传入全类名然后反射即可
现在比较忙,后续会把这个补充完成。