背景:
服务中配置了一组定时任务, 在运行过程中, 由于任务执行情况的不确定,
需要在调试或运行过程中能够动态调整定时任务的执行时间, 而不必每次修改执行时间都需要重启服务.
项目使用了nacos进行配置, 通过监听nacos配置修改, 来修改定时任务执行时间.
另外, 需要能够通过接口直接调起定时任务.
tips: 此处尝试使用nacos更新事件, 发现由于nacos更新后, RefreshScope没有执行完, 导致获取不到最新的完整配置, 此处改为监听RefreshScopeRefreshedEvent来保证配置完整(同时也带来若干次不必要的定时任务刷新, 代码中会对比任务crontab前后差异, 以此为据决定是否更新)
代码部分
启动类
package cn.cantabile.andante.schedule;
import org.springframework.boot.SpringApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableScheduling // 开启定时任务
@EnableAsync // 开启异步执行
public class TagPlatformScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class, args);
}
}
IJob
package cn.cantabile.andante.schedule.job;
import org.springframework.scheduling.annotation.Async;
import java.util.Map;
public interface IJob extends Runnable {
String getJobName();
@Async // 定时任务支持异步执行
public abstract void run();
void runWithParams(Map<String, Object> params);
}
AbstractJob
package cn.cantabile.andante.schedule.job;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
@Slf4j
public abstract class AbstractJob implements IJob {
@Override
public void run() {
// 获取定时任务执行参数
Map<String, Object> params = getRunParams();
// 执行任务
runWithParams(params);
}
public abstract void runWithParams(Map<String, Object> params) throws Exception;
public abstract Map<String, Object> getRunParams();
@Override
public String getJobName() {
return this.getClass().getSimpleName();
}
}
SampleJobA
package cn.cantabile.andante.schedule.job.impl;
import cn.cantabile.andante.schedule.job.AbstractJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.HashMap;
/**
* 定时任务测试验证
*/
@Slf4j
@Service
public class SampleJobA extends AbstractJob {
@Override
public void doJob_(Map<String, Object> params) throws Exception {
log.info("SampleJobA run with params: {}", params);
// 模型执行
try {
Thread.sleep(10000);
} catch (Exception e) {
log.error("SampleJobA run with params: {} failed, ex: ", params, e);
}
log.info("SampleJobA done.");
}
@Override
public Map<String, Object> getRunParams() {
return new HashMap<>();
}
}
动态任务scheduler
package cn.cantabile.andante.schedule;
import cn.cantabile.andante.schedule.config.JobConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.SchedulingException;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* 动态任务调度器
* 实现SchedulingConfigurer用户动态修改服务中定时任务
* 实现ApplicationListener<RefreshScopeRefreshedEvent>用于监控RefreshScopeRefreshedEvent事件
*/
@Slf4j
@Component
public class DynamicJobScheduler implements SchedulingConfigurer, ApplicationListener<RefreshScopeRefreshedEvent> {
// 当前定时任务配置
private static final ConcurrentHashMap<String, String> currentJobCronMap = new ConcurrentHashMap<>();
// 当前运行的定时任务ScheduledFuture
private static final ConcurrentHashMap<String, ScheduledFuture<?>> SCHEDULED_FUTURES = new ConcurrentHashMap<>();
ScheduledTaskRegistrar taskRegistrar;
private final JobConfig jobConfig;
public DynamicJobScheduler(JobConfig jobConfig) {
this.jobConfig = jobConfig;
}
@Resource
Map<String, IJob> jobMap;
private final Map<String, IJob> jobs = new ConcurrentHashMap<>();
public IJob getJobWithName(String jobName) {
if (jobs.get(jobName) != null) {
return jobs.get(jobName);
}
for (Map.Entry<String, IJob> entry : jobMap.entrySet()) {
IJob job = entry.getValue();
if (jobName.equals(job.getJobName())) {
jobs.put(jobName, job);
return job;
}
}
log.warn("没有找到任务: {}", jobName);
return null;
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
this.taskRegistrar = taskRegistrar;
refreshSchedule();
}
private void refreshSchedule() {
log.info("更新前定时任务的配置是: {}", currentJobCronMap);
Map<String, String> jobCronMap = jobConfig.getJobCronMap();
// 如果Nacos中的定时任务配置为空, 关闭当前所有定时任务
if (CollectionUtils.isEmpty(jobCronMap)) {
log.warn("Nacos中的定时任务配置为空, 关闭当前所有定时任务.");
currentJobCronMap.entrySet().removeIf(entry -> {
log.info("删除关闭的策略任务, jobName={}, cron={}", entry.getKey(), entry.getValue());
cancel(entry.getKey());
return true;
});
return;
}
log.info("Nacos中的定时任务配置是: {}", jobCronMap);
// 遍历当前列表, 清理需要关闭的Job
currentJobCronMap.entrySet().removeIf(entry -> {
String newCron = jobCronMap.get(entry.getKey());
if (createTrigger(newCron) == null) {
log.info("删除关闭或无效的策略任务, jobName={}, cron={}, newCron={}", entry.getKey(), entry.getValue(), newCron);
cancel(entry.getKey());
return true;
}
return false;
});
try {
jobCronMap.forEach((jobName, cron) -> {
IJob job = getJobWithName(jobName);
if (job != null) {
// String finalCron = Strings.isBlank(cron) ? "-" : cron.trim();
CronTrigger trigger = createTrigger(cron);
// 此处仅新增任务的trigger可能为null.
if (trigger == null) {
log.warn("新增定时任务失败, jobName={}, cron={}", jobName, cron);
return;
}
if (SCHEDULED_FUTURES.containsKey(jobName)) {
// 更新定时任务
if (trigger.getExpression().equals(currentJobCronMap.get(jobName))) {
log.info("定时任务没有发生任何变化, jobName={}, cron={}", jobName, currentJobCronMap.get(jobName));
return;
} else {
log.info("更新定时任务配置, jobName={}, cron={}, newCron={}", jobName, currentJobCronMap.get(jobName), cron);
reset(jobName, job, trigger);
}
} else {
// 新增定时任务
log.info("新增定时任务配置, jobName={}, newCron={}", jobName, cron);
addTriggerTask(jobName, job, trigger);
}
currentJobCronMap.put(jobName, trigger.getExpression());
}
});
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
log.info("最新的定时任务配置是: {}", currentJobCronMap);
}
}
/**
* 添加任务
*
* @param jobName 任务名称
* @param job 任务
* @param trigger 触发器
*/
public void addTriggerTask(String jobName, IJob job, CronTrigger trigger) {
if (SCHEDULED_FUTURES.containsKey(jobName)) {
throw new SchedulingException("the JobName[" + jobName + "] was added.");
}
TaskScheduler taskScheduler = Optional.ofNullable(taskRegistrar.getScheduler()).orElse(new ConcurrentTaskScheduler());
ScheduledFuture<?> future = taskScheduler.schedule(job, trigger);
SCHEDULED_FUTURES.put(jobName, future);
}
/**
* 取消任务
*
* @param jobName JobName
*/
public void cancel(String jobName) {
ScheduledFuture<?> future = SCHEDULED_FUTURES.get(jobName);
if (Objects.nonNull(future)) {
SCHEDULED_FUTURES.get(jobName).cancel(false);
}
SCHEDULED_FUTURES.remove(jobName);
}
/**
* 重置任务
*
* @param jobName JobName
* @param job 任务
* @param trigger 触发器
*/
public void reset(String jobName, IJob job, CronTrigger trigger) {
cancel(jobName);
addTriggerTask(jobName, job, trigger);
}
/**
* 获取正在调度的JobName列表
*
* @return 正在调度的JobName列表
*/
public Set<String> getJobNames() {
return SCHEDULED_FUTURES.keySet();
}
/**
* 是否存在任务
*
* @param jobName 任务名称
* @return 是否存在任务
*/
public boolean hasTask(String jobName) {
return SCHEDULED_FUTURES.containsKey(jobName);
}
/**
* 任务调度是否已经初始化完成
*
* @return 初始化完成
*/
public boolean inited() {
return this.taskRegistrar != null && this.taskRegistrar.getScheduler() != null;
}
@Override
public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
log.info("@RefreshScope注解的配置类已经刷新");
refreshSchedule();
}
/**
* 验证cron表达式是否有效
*
* @param cron cron表达式
* @return 是否有效
*/
private CronTrigger createTrigger(String cron) {
if (Strings.isBlank(cron) || "-".equals(cron.trim())) {
return null;
}
try {
return new CronTrigger(cron.trim());
} catch (IllegalArgumentException e) {
log.error("cron表达式不合法: {}", cron);
return null;
}
}
public Map<String, String> listAllJobs() {
return currentJobCronMap;
}
public String getJobInfo(String jobName) {
return currentJobCronMap.get(jobName);
}
}
crontab配置类
package cn.cantabile.andante.schedule.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@RefreshScope
@ConfigurationProperties(prefix = "job")
@Data
public class JobConfig {
private Map<String, String> jobCronMap;
}