How to restart scheduled task on runtime with EnableScheduling annotation in spring?

后端 未结 4 510
南笙
南笙 2021-01-01 21:25

I have been investigating how to change the frequency of a job on runtime with Java 8 and spring. This question was very useful but it did not totally solve my issue.

<
4条回答
  •  执笔经年
    2021-01-01 21:59

    The following, an improved version of this code, seems a working POC based on Spring Boot. You can start and stop the scheduled tasks any number of times based on a table configuration. But you can't start a stopped job from where it was stopped.

    1) In the main class, make sure scheduling is enabled, and perhaps configure a ThreadPoolTaskScheduler with size more than one so scheduled tasks may run in parallel.

    @SpringBootApplication
    @EnableScheduling
    
     @Bean
    public TaskScheduler poolScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("ThreadPoolTaskScheduler");
        scheduler.setPoolSize(10);
        scheduler.initialize();
        return scheduler;
    }
    

    2) an object that contains the schedule configuration, e.g. a cron like configuration in this case:

    public class ScheduleConfigVo {
    //some constructors, getter/setters
        private String  taskName;
        private String  configValue; // like */10 * * * * * for cron
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ScheduleConfigVo that = (ScheduleConfigVo) o;
            return taskName.equals(that.taskName) &&
                configValue.equals(that.configValue) ;
        }
    
        @Override
        public int hashCode() {
            return Objects.hash(taskName, configValue);
        }
    }
    

    equals and hashCode are needed since object comparison will be conducted.

    3) I use mybatis, so the sheduled selection is something like:

    @Mapper
    public interface ScheduleConfigMapper {
        List getAllConfigure();
    }
    

    and

    public class ScheduleConfigMapperImpl implements ScheduleConfigMapper {
        @Override
        public ListgetAllConfigure() {
            return getAllConfigure();
        }
    }
    

    with a simple companion mybatis xml configuration (not shown here but can find it anywhere in the internet).

    4) create a table and populate it with a record

    CREATE TABLE "SCHEDULER" 
    ( "CLASS_NAME" VARCHAR2(100), --PK
    "VALUE" VARCHAR2(20 BYTE) --not null
    )
    

    and populated it with a record class_name=Task1, value=*/10 * * * * * etc. => run like a cron every ten seconds

    5) the scheduler part:

    @Service
    public class DynamicScheduler implements SchedulingConfigurer {
    
    @Autowired
    private ScheduleConfigMapper repo;
    
    @Autowired
    private Runnable [] tsks;
    
    @Autowired
    private TaskScheduler tsch;
    
    private ScheduledTaskRegistrar scheduledTaskRegistrar;
    private ScheduledFuture future;
    
    private Map futureMap = new ConcurrentHashMap<>(); // for the moment it has only class name
    List oldList = new ArrayList<>();
    List newList;
    List addList = new ArrayList<>();
    List removeList = new ArrayList<>();
    
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        if (scheduledTaskRegistrar == null) {
            scheduledTaskRegistrar = taskRegistrar;
        }
        if (taskRegistrar.getScheduler() == null) {
            taskRegistrar.setScheduler(tsch);
        }
        updateJobList();
    
    }
    
    @Scheduled(fixedDelay = 5000)
    public void updateJobList() {
        newList = repo.getConfigure()== null ? new ArrayList<>() : repo.getConfigure();
        addList.clear();
        removeList.clear();
    
        if (!newList.isEmpty()) {
            //compare new List with oldList
            if (!oldList.isEmpty()) {
                addList = newList.stream().filter(e -> !oldList.contains(e)).collect(Collectors.toList());
                removeList = oldList.stream().filter(e -> !newList.contains(e)).collect(Collectors.toList());
            } else {
                addList = new ArrayList<>(newList); // nothing to remove
            }
        } else { // nothing to add
            if (!oldList.isEmpty()) {
                removeList = new ArrayList<>(oldList);
            } // else removeList = 0
        }
        log.info("addList="+ addList.toString());
        log.info("removeList="+ removeList.toString());
        //re-schedule here
    
        for ( ScheduleConfigVo conf : removeList ) {
            if ( !futureMap.isEmpty()){
                future = futureMap.get(conf.getTaskName());
                if (future != null) {
                    log.info("cancelling task "+conf.getTaskName() +" ...");
                    future.cancel(true);
                    log.info(conf.getTaskName() + " isCancelled = " + future.isCancelled());
                    futureMap.remove(conf.getTaskName());
                }
            }
        }
        for ( ScheduleConfigVo conf : addList ) {
            for (Runnable o: tsks) {
                if (o.getClass().getName().contains(conf.getTaskName())) { // o has fqn whereas conf has class name only
                    log.info("find " + o.getClass().getName() + " to add to scheduler");
                    future = scheduledTaskRegistrar.getScheduler().schedule(o, (TriggerContext a) -> { 
                        CronTrigger crontrigger = new CronTrigger(conf.getConfigValue());
                        return crontrigger.nextExecutionTime(a);
                    });
                    futureMap.put(o.getClass().getName().substring(o.getClass().getName().lastIndexOf('.')+1), future);
                }
            }
        }
    
        oldList.clear();
        oldList= newList;
    }
    

    6) one or more Runnable tasks that actually does the cron work, for instance:

    @Slf4j
    @Service
    public class Task1 implements Runnable {
        @Override
        public void run() {
            log.info("Task1 is running...");
        }
    }
    

    Once the application is started, the cron job will run. The running interval changes as the value in the table changes, and the job stops as the table entry is removed.

    Note that if the job runs longer than the cron interval, the next run is after the previous job finishes. You can simulate this situation by adding, for instance, sleep 15 seconds in Task1 above to test it. Sometimes after being cancelled, a job maybe still run till it's done.

    ***Just edit to add that if folks like lambda to save some lines, the above removeList and addList can be modified as:

    removeList.stream().filter(conf -> {
            future = futureMap.get(conf.getTaskName());
            return future != null;
        }).forEach((conf) -> {
            log.info("cancelling task " + conf.getTaskName() + " ...");
            future.cancel(true);
            log.info(conf.getTaskName() + " isCancelled = " + future.isCancelled());
        });
    

    and

    Arrays.stream(tsks).forEach(task -> {
            addList.stream().filter(conf -> task.getClass().getName().contains(conf.getTaskName())).forEach(conf -> {
                log.info("find " + task.getClass().getName() + " to add to scheduler");
                future = scheduledTaskRegistrar.getScheduler().schedule(task, (TriggerContext a) -> {
                    CronTrigger crontrigger = new CronTrigger(conf.getConfigValue());
                    return crontrigger.nextExecutionTime(a);
                });
                futureMap.put(task.getClass().getName().substring(task.getClass().getName().lastIndexOf('.') + 1), future);
            });
        });
    

提交回复
热议问题