闽公网安备 35020302035485号
fixedRate(period):固定频率执行,从任务启动之后,总是在固定的时刻执行,如果因为执行时间过长,造成错过某个时刻的执行(晚点),则任务会被立刻执行。
public interface IPollableService {
/**
* 执行方法
*/
void poll();
/**
* 获取周期表达式
*
* @return CronExpression
*/
default String getCronExpression() {
return null;
}
/**
* 获取任务名称
*
* @return 任务名称
*/
default String getTaskName() {
return this.getClass().getSimpleName();
}
}
最重要的便是getCronExpression()方法,每个定时服务实现可以自己控制自己的表达式,变与不变,自己说了算。至于从何处获取,怎么获取,请诸君自行发挥了。接下来,就是实现任务的动态注册:@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration implements SchedulingConfigurer, ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(SchedulingConfiguration.class);
private static ApplicationContext appCtx;
private final ConcurrentMap<String, ScheduledTask> scheduledTaskHolder = new ConcurrentHashMap<>(16);
private final ConcurrentMap<String, String> cronExpressionHolder = new ConcurrentHashMap<>(16);
private ScheduledTaskRegistrar taskRegistrar;
public static synchronized void setAppCtx(ApplicationContext appCtx) {
SchedulingConfiguration.appCtx = appCtx;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
setAppCtx(applicationContext);
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
this.taskRegistrar = taskRegistrar;
}
/**
* 刷新定时任务表达式
*/
public void refresh() {
Map<String, IPollableService> beanMap = appCtx.getBeansOfType(IPollableService.class);
if (beanMap.isEmpty() || taskRegistrar == null) {
return;
}
beanMap.forEach((beanName, task) -> {
String expression = task.getCronExpression();
String taskName = task.getTaskName();
if (null == expression) {
log.warn("定时任务[{}]的任务表达式未配置或配置错误,请检查配置", taskName);
return;
}
// 如果策略执行时间发生了变化,则取消当前策略的任务,并重新注册任务
boolean unmodified = scheduledTaskHolder.containsKey(beanName) && cronExpressionHolder.get(beanName).equals(expression);
if (unmodified) {
log.info("定时任务[{}]的任务表达式未发生变化,无需刷新", taskName);
return;
}
Optional.ofNullable(scheduledTaskHolder.remove(beanName)).ifPresent(existTask -> {
existTask.cancel();
cronExpressionHolder.remove(beanName);
});
if (ScheduledTaskRegistrar.CRON_DISABLED.equals(expression)) {
log.warn("定时任务[{}]的任务表达式配置为禁用,将被不会被调度执行", taskName);
return;
}
CronTask cronTask = new CronTask(task::poll, expression);
ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask);
if (scheduledTask != null) {
log.info("定时任务[{}]已加载,当前任务表达式为[{}]", taskName, expression);
scheduledTaskHolder.put(beanName, scheduledTask);
cronExpressionHolder.put(beanName, expression);
}
});
}
}
重点是保存ScheduledTask对象的引用,它是控制任务启停的关键。而表达式“-”则作为一个特殊的标记,用于禁用某个定时任务。当然,禁用后的任务通过重新赋予新的 Cron 表达式,是可以“复活”的。完成了上面这些,我们还需要一个定时任务来动态监控和刷新定时任务配置:@Component
public class CronTaskLoader implements ApplicationRunner {
private static final Logger log = LoggerFactory.getLogger(CronTaskLoader.class);
private final SchedulingConfiguration schedulingConfiguration;
private final AtomicBoolean appStarted = new AtomicBoolean(false);
private final AtomicBoolean initializing = new AtomicBoolean(false);
public CronTaskLoader(SchedulingConfiguration schedulingConfiguration) {
this.schedulingConfiguration = schedulingConfiguration;
}
/**
* 堆代码 duidaima.com
* 定时任务配置刷新
*/
@Scheduled(fixedDelay = 5000)
public void cronTaskConfigRefresh() {
if (appStarted.get() && initializing.compareAndSet(false, true)) {
log.info("定时调度任务动态加载开始>>>>>>");
try {
schedulingConfiguration.refresh();
} finally {
initializing.set(false);
}
log.info("定时调度任务动态加载结束<<<<<<");
}
}
@Override
public void run(ApplicationArguments args) {
if (appStarted.compareAndSet(false, true)) {
cronTaskConfigRefresh();
}
}
}
当然,也可以把这部分代码直接整合到SchedulingConfiguration中,但是为了方便扩展,这里还是将执行与触发分离了。毕竟除了通过定时任务触发刷新,还可以在界面上通过按钮手动触发刷新,或者通过消息机制回调刷新。这一部分就请大家根据实际业务情况来自由发挥了。@Service
public class CronTaskBar implements IPollableService {
@Override
public void poll() {
System.out.println("Say Bar");
}
@Override
public String getCronExpression() {
return "0/1 * * * * ?";
}
}
第二个任务是一个经常更换执行周期的任务,我们用一个随机数发生器来模拟它的善变:@Service
public class CronTaskFoo implements IPollableService {
private static final Random random = new SecureRandom();
@Override
public void poll() {
System.out.println("Say Foo");
}
@Override
public String getCronExpression() {
return "0/" + (random.nextInt(9) + 1) + " * * * * ?";
}
}
第三个任务就厉害了,它仿佛就像一个电灯的开关,在启用和禁用中反复横跳:@Service
public class CronTaskUnavailable implements IPollableService {
private String cronExpression = "-";
private static final Map<String, String> map = new HashMap<>();
static {
map.put("-", "0/1 * * * * ?");
map.put("0/1 * * * * ?", "-");
}
@Override
public void poll() {
System.out.println("Say Unavailable");
}
@Override
public String getCronExpression() {
return (cronExpression = map.get(cronExpression));
}
}
如果上面的步骤都做对了,日志里应该能看到类似这样的输出:定时调度任务动态加载开始>>>>>> 定时任务[CronTaskBar]的任务表达式未发生变化,无需刷新 定时任务[CronTaskFoo]已加载,当前任务表达式为[0/6 * * * * ?] 定时任务[CronTaskUnavailable]的任务表达式配置为禁用,将被不会被调度执行 定时调度任务动态加载结束<<<<<< Say Bar Say Bar Say Foo Say Bar Say Bar Say Bar 定时调度任务动态加载开始>>>>>> 定时任务[CronTaskBar]的任务表达式未发生变化,无需刷新 定时任务[CronTaskFoo]已加载,当前任务表达式为[0/3 * * * * ?] 定时任务[CronTaskUnavailable]已加载,当前任务表达式为[0/1 * * * * ?] 定时调度任务动态加载结束<<<<<< Say Unavailable Say Bar Say Unavailable Say Bar Say Foo Say Unavailable Say Bar Say Unavailable Say Bar Say Unavailable Say Bar小结