我们在日常工作中避免不了使用定时任务做一些事情。除了可以使用类似XXL-job之类的中间件添加。我们还可以利用Spring的SchedulingConfigurer来实现。下面来从代码的角度介绍SchedulingConfigurer的用法。
要实现Spring的定时任务,我们都知道,可以直接使用注解来实现的@Scheduled(cron = "* * * * * ?")。但我们不能因为修改个定时时间,再上线一次吧。我们也不能动态的来添加定时任务。
使用SchedulingConfigurer 方式来实现定时任务,代码如下
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
//笔者这里配合数据库来实现动态添加。方便查询数据库这里使用JdbcTemplate
@Autowired
JdbcTemplate jdbcTemplate;
private ScheduledTaskRegistrar taskRegistrar;
private Set<ScheduledFuture<?>> scheduledFutures = null;
private Map<String, ScheduledFuture<?>> taskFutures = new ConcurrentHashMap<>();
/**
* 这个方法在Spring初始化的时候会帮我们执行,这里也会拉取数据库内需要执行的任务,进行添加到定时器里。
* @param scheduledTaskRegistrar
*/
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
List<TriggerTask> list= new ArrayList<>();
//查询出来当前数据库中存储的所有有效的任务
List<Map<String, Object>> maps = jdbcTemplate.queryForList("select * from corn where status=1");
//循环添加任务
maps.forEach(t->{
TriggerTask triggerTask = new TriggerTask(()->{
System.out.println("执行定时任务:"+ LocalDateTime.now().toLocalTime());
},triggerContext -> {
System.out.println("执行Cron:"+t.get("corn").toString()+",id="+t.get("id").toString());
//如果需要动态的指定当前定时任务的执行corn。这里可以增加一步,查询数据库操作。如果任务corn不需要精确修改,corn可进行缓存。到期在去查询数据库。这里根据读者的需求自行取舍。
return new CronTrigger(t.get("corn").toString()).nextExecutionTime(triggerContext);
});
list.add(triggerTask);
});
//将任务列表注册到定时器
scheduledTaskRegistrar.setTriggerTasksList(list);
this.taskRegistrar = scheduledTaskRegistrar;
}
/**
* 添加任务
* @param taskId
* @param triggerTask
*/
public void addTask(String taskId, TriggerTask triggerTask) {
//如果定时任务id已存在,则取消原定时器,从新创建新定时器,这里也是个更新定时任务的过程。
if (taskFutures.containsKey(taskId)) {
System.out.println("the taskId[" + taskId + "] 取消,重新添加");
cancelTriggerTask(taskId);
}
TaskScheduler scheduler = taskRegistrar.getScheduler();
ScheduledFuture<?> future = scheduler.schedule(triggerTask.getRunnable(), triggerTask.getTrigger());
getScheduledFutures().add(future);
taskFutures.put(taskId, future);
}
/**
* 获取任务列表
*/
private Set<ScheduledFuture<?>> getScheduledFutures() {
if (scheduledFutures == null) {
try {
scheduledFutures = (Set<ScheduledFuture<?>>) Utils.getProperty(taskRegistrar, "scheduledTasks");
} catch (NoSuchFieldException e) {
throw new SchedulingException("not found scheduledFutures field.");
}
}
return scheduledFutures;
}
/**
* 取消任务
*/
public void cancelTriggerTask(String taskId) {
ScheduledFuture<?> future = taskFutures.get(taskId);
if (future != null) {
future.cancel(true);
}
taskFutures.remove(taskId);
getScheduledFutures().remove(future);
}
}
config代码中使用到的反射工具类
public static Object getProperty(Object obj, String name) throws NoSuchFieldException {
Object value = null;
Field field = findField(obj.getClass(), name);
if (field == null) {
throw new NoSuchFieldException("no such field [" + name + "]");
}
boolean accessible = field.isAccessible();
field.setAccessible(true);
try {
value = field.get(obj);
} catch (Exception e) {
throw new RuntimeException(e);
}
field.setAccessible(accessible);
return value;
}
public static Field findField(Class<?> clazz, String name) {
try {
return clazz.getField(name);
} catch (NoSuchFieldException ex) {
return findDeclaredField(clazz, name);
}
}
public static Field findDeclaredField(Class<?> clazz, String name) {
try {
return clazz.getDeclaredField(name);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null) {
return findDeclaredField(clazz.getSuperclass(), name);
}
return null;
}
}
添加测试类
@RestController
public class test {
@Autowired
ScheduledConfig scheduledConfig;
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/test")
public void test(String id) throws NoSuchFieldException {
Map<String, ScheduledFuture<?>> taskRegistrar = (Map<String, ScheduledFuture<?>>) Utils.getProperty(scheduledConfig, "taskFutures");
System.out.println(taskRegistrar.size());
scheduledConfig.addTask(id,triggerTask(id,"* * * * * ?"));
//这里可根据需要自行存库jdbcTemplate.insert()。
}
private TriggerTask triggerTask(String taskId,String corn){
return new TriggerTask(()->{
System.out.println("执行定时任务:"+ LocalDateTime.now().toLocalTime());
},triggerContext -> {
System.out.println("执行corn:"+"* * * * * ?"+",id="+taskId);
//这里与config类的代码类似。根据需要自行处理
return new CronTrigger(corn).nextExecutionTime(triggerContext);
});
}
}
测试一下我们的定时任务:
项目启动时,定时任务注册。就会按照corn去执行。
修改corn的值。定时任务在下次执行的时候会加载新的时间。
任务生效,定时执行,如下图。
值得注意的是,我们并不能直接在数据库中修改status的值,使其生效。需要在定时器中移除这个任务,可以使任务失效,并且修改数据库status的值,使其下次加载的时候不生效即可。
这里仅供学习,真正生产环境。还是使用xxl-job之类的中间件比较好。毕竟我们这里需要考虑分布式环境下。任务多次并发执行的情况,需要加分布式锁。这样会使功能的实现复杂度更进一步。实现起来复杂,后期不易维护。