@RefreshScope останавливает @Scheduled task

У меня есть приложение для мониторинга, в котором я выполняю задачу fixedRate. Это подтягивает параметр конфигурации, настроенный с помощью Consul. Я хочу использовать обновленную конфигурацию, поэтому добавил @RefreshScope. Но как только я обновляю значение конфигурации в Consul, задача fixedRate перестает выполняться.

@Service
@RefreshScope
public class MonitorService {

    @Autowired
    private AppConfig appConfig;

    @PostConstruct
    public void postConstRun() {
        System.out.println(appConfig.getMonitorConfig());
    }

    @Scheduled(fixedRate = 1000)
    public void scheduledMonitorScan() {
        System.out.println("MonitorConfig:" + appConfig.getMonitorConfig());
    }
}

Класс AppConfig имеет только один параметр String:

@Configuration
@Getter
@Setter
public class AppConfig {

    @Value("${monitor-config:default value}")
    private String monitorConfig;
}

Как только я обновляю значение в consul, запланированная задача просто перестает выполняться (отображается в методе sheduledMonitorScan) и перестает отображаться.

Вы пробовали их разделить?

spencergibb 21.05.2018 02:22

Что разделять? Вы имеете в виду переместить RefreshScope из класса MonitorService?

ZCode 21.05.2018 02:24

Вам просто нужен @RefreshScope в AppConfig, и это обновит внедренный bean-компонент в вашу службу.

Amin Abu-Taleb 23.05.2018 17:01

Я пробовал добавить @ RefreshScope в AppConfig. Читая Документация Sprind, я обнаружил, что общедоступный метод обновления в аннотации предоставляется в конечной точке / refresh. Поэтому я добавил зависимость от пружинного привода. Теперь моя конфигурация обновляется, но @ Scheduled перестает работать, когда я добавляю пружинный привод.

ZCode 29.05.2018 04:50
3
4
3 445
4

Ответы 4

Я сделал обходной путь для такого сценария, реализовав интерфейс SchedulingConfigurer. Здесь я динамически обновляю свойство "scheduler.interval" из внешнего файла свойств, и планировщик работает нормально даже после обновления привода, поскольку я больше не использую @RefreshScope. Надеюсь, это поможет и вам в вашем случае.

public class MySchedulerImpl implements SchedulingConfigurer {

    @Autowired
    private Environment env;

    @Bean(destroyMethod = "shutdown")
    public Executor taskExecutor() {
        return Executors.newScheduledThreadPool(10);
    }

    @Override
    public void configureTasks(final ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(this.taskExecutor());
        taskRegistrar.addTriggerTask(() -> {
            //put your code here that to be scheduled
        }, triggerContext -> {
            final Calendar nextExecutionTime = new GregorianCalendar();
            final Date lastActualExecutionTime = triggerContext.lastActualExecutionTime();

            if (lastActualExecutionTime == null) {
                nextExecutionTime.setTime(new Date());
            } else {
                nextExecutionTime.setTime(lastActualExecutionTime);
                nextExecutionTime.add(Calendar.MILLISECOND, env.getProperty("scheduler.interval", Integer.class));
            }
            return nextExecutionTime.getTime();
        });
    }
}

Вот как мы решили эту проблему.

/**
 * Listener of Spring's lifecycle to revive Scheduler beans, when spring's
 * scope is refreshed.
 * <p>
 * Spring is able to restart beans, when we change their properties. Such a
 * beans marked with RefreshScope annotation. To make it work, spring creates
 * <b>lazy</b> proxies and push them instead of real object. The issue with
 * scope refresh is that right after refresh in order for such a lazy proxy
 * to be actually instantiated again someone has to call for any method of it.
 * <p>
 * It creates a tricky case with Schedulers, because there is no bean, which
 * directly call anything on any Scheduler. Scheduler lifecycle is to start
 * few threads upon instantiation and schedule tasks. No other bean needs
 * anything from them.
 * <p>
 * To overcome this, we had to create artificial method on Schedulers and call
 * them, when there is a scope refresh event. This actually instantiates.
 */
@RequiredArgsConstructor
public class RefreshScopeListener implements ApplicationListener<RefreshScopeRefreshedEvent> {
    private final List<RefreshScheduler> refreshSchedulers;

    @Override
    public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
        refreshSchedulers.forEach(RefreshScheduler::materializeAfterRefresh);
    }
}

Итак, мы определили интерфейс, который ничего особенного не делает, но позволяет нам вызывать обновленное задание.

public interface RefreshScheduler {
    /**
     * Used after refresh context for scheduler bean initialization
     */
    default void materializeAfterRefresh() {
    }
}

А вот и актуальная работа, параметр from.properties которой можно обновить.

public class AJob implements RefreshScheduler {
    @Scheduled(cron = "${from.properties}")
    public void aTask() {
        // do something useful
    }
}

ОБНОВЛЕНО: Конечно, bean-компонент AJob должен быть помечен @RefreshScope в @Configuration

@Configuration
@EnableScheduling
public class SchedulingConfiguration {
    @Bean
    @RefreshScope
    public AJob aJob() {
        return new AJob();
    }
}

Чтобы код заработал, мне пришлось добавить аннотацию @Component к RefreshScopeListener. Тогда код работает. Спасибо

Joe 17.04.2020 18:43
github.com/winster/SpringSchedulerDynamic упрощено. Нет необходимости в интерфейсе и отдельных компонентах
Winster 12.05.2020 15:20

Я чувствую, что весеннее облако должно делать это автоматически само. Но пока я думаю, это решение

Rémi Roy 17.08.2020 23:50

Мое решение состоит в прослушивании EnvironmentChangeEvent

@Configuration
public class SchedulingSpringConfig implements ApplicationListener<EnvironmentChangeEvent>, SchedulingConfigurer {

  private static final Logger LOGGER = LoggerFactory.getLogger(SchedulingSpringConfig.class);

  private final DemoProperties demoProperties;

  public SchedulingSpringConfig(DemoProperties demoProperties) {
    this.demoProperties = demoProperties;
  }

  @Override
  public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
    LOGGER.info("Configuring scheduled task with cron expression: {}", demoProperties.getCronExpression());
    taskRegistrar.addTriggerTask(triggerTask());
    taskRegistrar.setTaskScheduler(taskScheduler());
  }

  @Bean
  public TriggerTask triggerTask() {
    return new TriggerTask(this::work, cronTrigger());
  }

  private void work() {
    LOGGER.info("Doing work!");
  }

  @Bean
  @RefreshScope
  public CronTrigger cronTrigger() {
    return new CronTrigger(demoProperties.getCronExpression());
  }

  @Bean
  public ThreadPoolTaskScheduler taskScheduler() {
    return new ThreadPoolTaskScheduler();
  }

  @Override
  public void onApplicationEvent(EnvironmentChangeEvent event) {
    if (event.getKeys().contains("demo.config.cronExpression")) {
      ScheduledTasksRefresher scheduledTasksRefresher = new ScheduledTasksRefresher(triggerTask());
      scheduledTasksRefresher.afterPropertiesSet();
    }
  }
}

Затем я использую ContextLifecycleScheduledTaskRegistrar, чтобы воссоздать задачу.

public class ScheduledTasksRefresher extends ContextLifecycleScheduledTaskRegistrar {

  private final TriggerTask triggerTask;

  ScheduledTasksRefresher(TriggerTask triggerTask) {
    this.triggerTask = triggerTask;
  }

  @Override
  public void afterPropertiesSet() {
    super.destroy();
    super.addTriggerTask(triggerTask);
    super.afterSingletonsInstantiated();
  }
}

Определение свойств:

@ConfigurationProperties(prefix = "demo.config", ignoreUnknownFields = false)
public class DemoProperties {

  private String cronExpression;

  public String getCronExpression() {
    return cronExpression;
  }

  public void setCronExpression(String cronExpression) {
    this.cronExpression = cronExpression;
  }
}

Основное определение:

@SpringBootApplication
@EnableConfigurationProperties(DemoProperties.class)
@EnableScheduling
public class DemoApplication {

  public static void main(String[] args) {
    SpringApplication.run(DemoApplication.class, args);
  }
}

Спасибо. На мой взгляд, destroy () не уничтожает ранее запланированные задачи. Любая идея?

Winster 01.04.2020 21:57

После некоторой отладки, мое наблюдение. У объекта-регистратора нет запланированных заданий (сюрприз!). Я также попытался сохранить регистратор в переменной экземпляра из configuretasks и запустить на нем уничтожение. Но такое же поведение!

Winster 01.04.2020 22:39

Я успешно получаю и переопределяю значения с сервера конфигурации консула, используя RefreshScopeRefreshedEvent

import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
@RefreshScope
public class AlertSchedulerCron implements ApplicationListener<RefreshScopeRefreshedEvent> {

    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Value("${pollingtime}")
    private String pollingtime;
    
    /*
     * @Value("${interval}") private String interval;
     */
    @Scheduled(cron = "${pollingtime}")
    //@Scheduled(fixedRateString = "${interval}" )
    public void task() {

        System.out.println(pollingtime);
        System.out.println("Scheduler (cron expression) task with duration : " + sdf.format(new Date()));
    }

    @Override
    public void onApplicationEvent(RefreshScopeRefreshedEvent event) {
        // TODO Auto-generated method stub
        
    }

    
}

Это должно быть приемлемым решением. Безусловно, самый простой в использовании!

Rémi Roy 18.08.2020 00:11

После этого решения мой планировщик запускается два раза. Я вижу, что на самом деле похоже, что есть 2 экземпляра планировщика, один работает со старым расписанием, а другой - с новым расписанием.

user07 10.09.2020 18:37

Другие вопросы по теме