众所周知SpringBoot中通过@Scheduled即可使用定时任务,但是我们有需求需要动态的定时导出报表这可怎么办呢,当然市面上还有很多的任务调度器也是不错的.本文主要教大家使用Nacos配置中心和SpringBoot实现动态任务调度.
话不多说,代码奉上.
public interface AbstractDynamicSchedule extends Runnable {
/**
* 任务名称
* @return 返回执行任务名称用于打日志
*/
String taskName();
/**
* 任务的cron表达式yaml key
* @return yaml key
*/
String cronKey();
}
采用策略模式来实现我们的主体.动态的创建任务肯定却不了 Runnable 从Scheduled的源码中我们可以了解到 可以通过 .schedule(Runnable,Cron)来创建一个新的定时任务,通过 .cancel可以取消一个定时任务 那么我们就可以从这里入手来做更改操作了.
import cn.timesgroup.market.backend.common.schedule.AbstractDynamicSchedule;
import com.alibaba.nacos.spring.context.event.config.NacosConfigReceivedEvent;
import com.alibaba.nacos.spring.util.parse.DefaultYamlConfigParse;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
@Component
@Slf4j
public class NacosCronDataIdChangeListener implements ApplicationListener<NacosConfigReceivedEvent>, SchedulingConfigurer {
private static final String CRON_YAML = "application-dynamic-schedule.yml";
private static final DefaultYamlConfigParse parse = new DefaultYamlConfigParse();
private ScheduledTaskRegistrar taskRegistrar;
private static final ConcurrentHashMap<String, ScheduledFuture<?>> SCHEDULED_FUTURES = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, CronTask> CRON_TASKS = new ConcurrentHashMap<>();
@Resource
private ApplicationContext applicationContext;
/**
* 存储所有动态定时任务处理类
*/
@Resource
private Map<String, AbstractDynamicSchedule> scheduleMap;
@Override
public void onApplicationEvent(NacosConfigReceivedEvent event) {
if (!CRON_YAML.equals(event.getDataId())) {
log.info("CRON_YAML is not {}", CRON_YAML);
return;
}
Properties properties = parse.parse(event.getContent());
Collection<AbstractDynamicSchedule> editCronScheduleMap = Lists.newArrayList();
// 对比每一个配置
scheduleMap.keySet().forEach(schedule -> {
AbstractDynamicSchedule bean = (AbstractDynamicSchedule) applicationContext.getBean(schedule);
String cronKey = bean.cronKey();
CronTask cronTask = CRON_TASKS.get(cronKey);
if (Objects.isNull(cronTask)) {
// 新增
editCronScheduleMap.add(scheduleMap.get(schedule));
return;
}
String oldCronValue = cronTask.getExpression();
String newCronValue = properties.getProperty(cronKey);
if (Objects.equals(oldCronValue, newCronValue)) {
log.info("task time not change , cronKey={}, oldCronValue={}", cronKey, oldCronValue);
return;
}
// 发生了变化
editCronScheduleMap.add(scheduleMap.get(schedule));
});
this.refreshTasks(editCronScheduleMap);
}
public void refreshTasks(Collection<AbstractDynamicSchedule> tasks) {
tasks.forEach(schedule -> {
String cronKey = schedule.cronKey();
// 取消已经删除的策略任务
if ("-".equals(cronKey) || StringUtils.isBlank(cronKey)) {
cancel(cronKey);
log.info("取消已经删除的策略任务, taskName={}, cronKey={}", schedule.taskName(), schedule.cronKey());
return;
}
String cronNewValue = applicationContext.getEnvironment().getProperty(cronKey);
// 新的值为取消定时任务
if ("-".equals(cronNewValue) || StringUtils.isBlank(cronNewValue)) {
cancel(cronKey);
log.info("定时任务关闭, taskName={}, cronKey={}", schedule.taskName(), schedule.cronKey());
return;
}
// 定时任务没有发生任何变化
if (SCHEDULED_FUTURES.containsKey(cronKey) && CRON_TASKS.get(cronKey).getExpression().equals(cronNewValue)) {
log.info("定时任务没有发生任何变化, taskName={}, cronKey={}", schedule.taskName(), schedule.cronKey());
return;
}
// 如果策略执行时间发生了变化,则取消当前策略的任务
boolean isUpdate = SCHEDULED_FUTURES.containsKey(cronKey) && cancel(cronKey);
CronTask task = new CronTask(schedule, cronNewValue);
CRON_TASKS.put(cronKey, task);
ScheduledFuture<?> future = Optional.ofNullable(taskRegistrar.getScheduler())
.orElse(new ConcurrentTaskScheduler()).schedule(task.getRunnable(), task.getTrigger());
SCHEDULED_FUTURES.put(cronKey, future);
if (isUpdate) {
log.info("定时任务修改, taskName={}, cronKey={}, taskNewCron={}",
schedule.taskName(), cronKey, cronNewValue);
} else {
log.info("定时任务新增, taskName={}, cronKey={}, taskCron={}",
schedule.taskName(), cronKey, cronNewValue);
}
});
}
private boolean cancel(String cronKey) {
ScheduledFuture<?> future = SCHEDULED_FUTURES.get(cronKey);
if (Objects.nonNull(future)) {
SCHEDULED_FUTURES.get(cronKey).cancel(false);
}
SCHEDULED_FUTURES.remove(cronKey);
CRON_TASKS.remove(cronKey);
return true;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
this.taskRegistrar = taskRegistrar;
this.refreshTasks(scheduleMap.values());
}
}
这里监听NacosConfigReceivedEvent事件即可得知什么时候配置文件发生了更改,在更改的时候我们将对应的定时任务也进行匹配并更改即可.这里我们通过cronKey来进行匹配. 当然不要忘了 监听配置更改会监听到所有 这里我们只关注该关注的即可.
而SchedulingConfigurer只是用于来拿取所需的ScheduledTaskRegistrar这样即可进行对应的监听
现在我们可以来测试下
@Component
@Slf4j
public class TestSchedule1 implements AbstractDynamicSchedule {
@Override
public String taskName() {
return "测试定时任务";
}
@Override
public String cronKey() {
return "test-dynamic-schedule";
}
@Override
public void run() {
log.debug(new Date() + "----1");
}
}
Yaml文件:
test-dynamic-schedule: 0 * * * * ?
本文到这结束啦.
原作: imyzt 感恩思为~ 感恩imyzt~
灵感来自job
思路来自这: https://my.oschina.net/serge/blog/864162