编程式添加调度任务时Actuator系统状态缺失调度器数据
问题描述
当前技术栈为Java 11、Spring Boot,此前通过@Scheduled注解实现的调度任务会自动展示在Actuator的#/administration/systemstatus页面。
因业务需要新增UI端强制触发调度任务的能力,需求逻辑如下:
- 手动触发时调用
TaskScheduler的schedule方法,将任务调度为当前时间立即执行 - 任务执行结束后,按照
application.properties中配置的cron表达式重新调度任务(默认任务始终按cron规则运行,仅少数场景支持用户通过UI额外触发)
原生@Scheduled注解无法覆盖上述需求,因此编写了自定义调度组件,但出现副作用:Actuator系统状态页面不再展示任务及对应cron表达式的相关数据。
自定义调度核心代码如下(重调度逻辑未展示):
@Component @Slf4j public class MyTaskService { private final TaskScheduler scheduler; private final List<CustomTask> taskList; //runnable tasks private final RunningTasksFutureHolder runningTasksFutureHolder; public MyTaskService(TaskScheduler scheduler, List<CustomTask> taskList, RunningTasksFutureHolder runningTasksFutureHolder) { this.scheduler = scheduler; this.taskList = taskList; this.runningTasksFutureHolder = runningTasksFutureHolder; } @PostConstruct public void afterLoad() { taskList.forEach(this::addTaskToScheduler); } public void addTaskToScheduler(CustomTask task) { // 此处添加到调度器的任务无法在Actuator中展示 ScheduledFuture<?> scheduledTask = scheduler.schedule(task, new CronTrigger(task.getCronExpression())); runningTasksFutureHolder.add(task.getTaskType(), scheduledTask); } } @Configuration @EnableScheduling public class SchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { // 空实现,未做任何任务注册 } @Bean public TaskScheduler poolScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler"); scheduler.setPoolSize(5); scheduler.initialize(); return scheduler; } }
问题根因
Spring Boot Actuator的调度任务端点(schedules端点)的数据源是容器中所有实现了ScheduledTaskHolder接口的Bean所记录的任务信息。
当前实现存在两个问题导致任务无法被采集:
- 自定义任务直接调用
TaskScheduler.schedule()方法提交,没有被任何ScheduledTaskHolder实现类记录,Actuator无法感知到这些任务的存在 SchedulingConfigurer的configureTasks方法为空,自定义的ThreadPoolTaskScheduler没有和Spring内置的ScheduledTaskRegistrar(本身就是ScheduledTaskHolder的实现类)绑定,绕开了Spring原生的任务注册流程
修复方案
所有任务的注册、取消、重调度操作都通过ScheduledTaskRegistrar完成,保证任务信息被Spring的调度注册表记录,Actuator即可正常采集展示。
第一步:修改调度配置类
在configureTasks方法中完成线程池初始化、与ScheduledTaskRegistrar的绑定,同时对外暴露注册器实例供后续业务操作使用:
@Configuration @EnableScheduling public class SchedulingConfig implements SchedulingConfigurer { private ScheduledTaskRegistrar taskRegistrar; @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { this.taskRegistrar = taskRegistrar; // 初始化调度线程池 ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler"); scheduler.setPoolSize(5); scheduler.initialize(); taskRegistrar.setTaskScheduler(scheduler); } public ScheduledTaskRegistrar getTaskRegistrar() { return taskRegistrar; } public TaskScheduler getTaskScheduler() { return taskRegistrar.getScheduler(); } }
第二步:修改自定义任务服务类
初始任务注册、手动触发后的重调度都通过ScheduledTaskRegistrar添加Cron任务,不要直接绕过注册器调用TaskScheduler的调度方法:
@Component @Slf4j public class MyTaskService implements ApplicationListener<ContextRefreshedEvent> { private final SchedulingConfig schedulingConfig; private final List<CustomTask> taskList; private final RunningTasksFutureHolder runningTasksFutureHolder; private ScheduledTaskRegistrar taskRegistrar; private TaskScheduler scheduler; public MyTaskService(SchedulingConfig schedulingConfig, List<CustomTask> taskList, RunningTasksFutureHolder runningTasksFutureHolder) { this.schedulingConfig = schedulingConfig; this.taskList = taskList; this.runningTasksFutureHolder = runningTasksFutureHolder; } @Override public void onApplicationEvent(ContextRefreshedEvent event) { // 上下文刷新完成后获取注册器和调度器实例 this.taskRegistrar = schedulingConfig.getTaskRegistrar(); this.scheduler = schedulingConfig.getTaskScheduler(); // 初始化注册所有默认cron任务 taskList.forEach(this::addCronTaskToRegistrar); // 触发注册器提交所有任务到调度器 taskRegistrar.afterPropertiesSet(); } /** * 注册cron任务到注册器,会自动被Actuator采集 */ private void addCronTaskToRegistrar(CustomTask task) { taskRegistrar.addCronTask(task, task.getCronExpression()); } /** * UI端手动触发任务的示例逻辑 */ public void triggerTaskImmediate(CustomTask task) { // 1. 取消该任务原有的cron调度 taskRegistrar.getCronTaskList().stream() .filter(cronTask -> cronTask.getRunnable().equals(task)) .findFirst() .ifPresent(cronTask -> { ScheduledTask scheduledTask = taskRegistrar.scheduleCronTask(cronTask); if (scheduledTask != null) { scheduledTask.cancel(); } }); // 2. 提交立即执行的任务 ScheduledFuture<?> immediateTaskFuture = scheduler.schedule(task, Instant.now()); runningTasksFutureHolder.add(task.getTaskType(), immediateTaskFuture); try { // 等待任务执行完成 immediateTaskFuture.get(); } catch (InterruptedException | ExecutionException e) { log.error("手动触发任务执行异常", e); Thread.currentThread().interrupt(); } finally { // 3. 重新注册cron任务,恢复默认调度规则 addCronTaskToRegistrar(task); taskRegistrar.afterPropertiesSet(); } } }
配置校验
如果使用Spring Boot 2.7+版本,确认配置中没有禁用schedules端点:
management.endpoint.schedules.enabled=true
完成上述修改后,所有通过ScheduledTaskRegistrar注册的任务都会自动同步到Actuator的调度任务列表中,页面可正常展示任务名称、对应cron表达式等信息。
内容的提问来源于stack exchange,提问作者Frankio1
相关产品推荐
相关产品推荐

