如何在Spring中使用EnableScheduling注释在运行时重新启动计划任务?

时间:2015-08-12 15:15:29

标签: java spring java-8 scheduled-tasks jobs

我一直在研究如何使用Java 8和spring在运行时更改作业的频率。 This question非常有用,但它并没有完全解决我的问题。

我现在可以配置下次执行作业的日期。但如果将延迟设置为1年,那么我需要在考虑新配置之前等待1年。

我的想法是在配置值发生变化时停止计划任务(所以从另一个类开始)。然后在下次执行任务时重新计算。也许有一种更简单的方法可以做到这一点。

这是我到目前为止的代码。

@Configuration
@EnableScheduling
public class RequestSchedulerConfig implements SchedulingConfigurer {

    @Autowired
    SchedulerConfigService schedulerConfigService;

    @Bean
    public RequestScheduler myBean() {
        return new RequestScheduler();
    }

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

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
        taskRegistrar.addTriggerTask(
                new Runnable() {
                    @Override public void run() {
                        myBean().startReplenishmentComputation();
                    }
                },
                new Trigger() {
                    @Override public Date nextExecutionTime(TriggerContext triggerContext) {
                        Duration d = schedulerConfigService.getIntervalFromDB();
                        return DateTime.now().plus(d).toDate();
                    }
                }
        );
    }
}

这就是我想做的事。

@RestController
@RequestMapping("/api/config/scheduler")
public class RequestSchedulerController {

    @Autowired
    ApplicationConfigWrapper applicationConfigWrapper;

    @RequestMapping("/set/")
    @ResponseBody
    public String setRequestSchedulerConfig(@RequestParam(value = "frequency", defaultValue = "") final String frequencyInSeconds){
        changeValueInDb(frequencyInSeconds);
        myJob.restart();
        return "Yeah";
    }

}

4 个答案:

答案 0 :(得分:7)

  1. 创建一个获取注入TaskScheduler的单例bean。这将作为状态变量保留所有ScheduledFutures,如private ScheduledFuture job1;
  2. 在部署时,从数据库加载所有计划数据并启动作业,填写所有状态变量,如job1
  3. 在更改计划数据时,cancel对应Future(例如job1),然后使用新的计划数据再次启动它。
  4. 这里的关键思想是在创建Future时对其进行控制,以便将它们保存在某些状态变量中,这样当调度数据中的某些内容发生变化时,您可以取消它们。

    以下是工作代码:

    的applicationContext.xml

    <task:annotation-driven />
    <task:scheduler id="infScheduler" pool-size="10"/>
    

    单例bean,它包含Future s

    @Component
    public class SchedulerServiceImpl implements SchedulerService {
    
            private static final Logger logger = LoggerFactory.getLogger(SchedulerServiceImpl.class);
    
            @Autowired
            @Qualifier(value="infScheduler")
            private TaskScheduler taskScheduler;
    
            @Autowired
            private MyService myService;
    
            private ScheduledFuture job1;//for other jobs you can add new private state variables
    
            //Call this on deployment from the ScheduleDataRepository and everytime when schedule data changes.
            @Override
            public synchronized void scheduleJob(int jobNr, long newRate) {//you are free to change/add new scheduling data, but suppose for now you only want to change the rate
                    if (jobNr == 1) {//instead of if/else you could use a map with all job data
                            if (job1 != null) {//job was already scheduled, we have to cancel it
                                    job1.cancel(true);
                            }
                            //reschedule the same method with a new rate
                            job1 = taskScheduler.scheduleAtFixedRate(new ScheduledMethodRunnable(myService, "methodInMyServiceToReschedule"), newRate);
                    }
            }
    }
    

答案 1 :(得分:0)

一种简单的方法是只添加新任务,而不是尝试取消或重新启动调度程序。

每次配置更改时,只需使用新配置添加新任务。

然后,每当任务运行时,它必须首先检查某个状态(通过查询数据库,或在并发映射中查找,或其他任何)来确定它是否是最新版本。如果是,那么它应该继续。否则,它应立即结束。

唯一的缺点是,如果您经常更改作业配置与其运行频率相比,那么当然,计划任务列表将在内存中不断增长。

答案 2 :(得分:0)

使用Set<ScheduledTask> ScheduledTaskRegistrar.getScheduledTasks()获取所有计划任务并调用ScheduledTask::cancel()怎么样? 或执行ThreadPoolTaskScheduler::shutdown() 并重新创建ThreadPoolTask​​Scheduler并在ScheduledTaskRegistrar中再次进行设置?

答案 3 :(得分:0)

以下是this code的改进版本,似乎是基于Spring Boot的有效POC。您可以基于表配置多次启动和停止计划的任务。但是您不能从停止的地方开始停止的工作。

1)在主类中,请确保已启用调度,并可能将ThreadPoolTask​​Scheduler配置为大于一个的大小,以便调度的任务可以并行运行。

@SpringBootApplication
@EnableScheduling

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

2)包含计划配置的对象,例如在这种情况下类似cron的配置:

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);
    }
}

等于和hashCode,因为将进行对象比较。

3)我使用mybatis,所以选择的选择类似于:

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

public class ScheduleConfigMapperImpl implements ScheduleConfigMapper {
    @Override
    public List<ScheduleConfigVo>getAllConfigure() {
        return getAllConfigure();
    }
}

具有简单的伴随mybatis xml配置(此处未显示,但可以在Internet上的任何位置找到)。

4)创建一个表并用记录填充它

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

并用记录class_name = Task1,value = * / 10 * * * * *等填充它=>每十秒钟就会像cron一样运行

5)调度程序部分:

@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<String, ScheduledFuture> futureMap = new ConcurrentHashMap<>(); // for the moment it has only class name
List<ScheduleConfigVo> oldList = new ArrayList<>();
List<ScheduleConfigVo> newList;
List<ScheduleConfigVo> addList = new ArrayList<>();
List<ScheduleConfigVo> 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)实际执行cron工作的一个或多个可运行任务,例如:

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

启动应用程序后,cron作业将运行。运行间隔随表中值的更改而变化,作业因表条目被删除而停止。

请注意,如果作业的运行时间超过cron间隔,则下一次运行将在上一个作业完成之后进行。您可以通过在上面的Task1中添加例如15秒钟的睡眠时间进行测试来模拟这种情况。有时,在取消之后,一项工作可能仍会运行直到完成。

***只需编辑即可,如果人们喜欢lambda来保存一些行,则可以将上述removeList和addList修改为:

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());
    });

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);
        });
    });