Springboot开启定时任务Spring Schedule(内涵业务场景代码)
一、简介
Spring Schedule是Spring框架提供的一种轻量级的任务调度框架,可以用来执行定时任务和周期性任务。在很多应用场景中,我们需要定时执行某些任务,比如定时备份数据库、定时清理缓存、定时发送邮件等等。Spring Schedule提供了很方便的任务调度解决方案,可以很容易地实现这些定时任务。
Spring Boot是Spring框架的一个子项目,它提供了很多开箱即用的特性,可以帮助我们更快速、更便捷地开发Spring应用。在Spring Boot应用中整合Spring Schedule也非常简单,本文将介绍如何在Spring Boot中整合Spring Schedule。
二、添加依赖
引入springboot依赖
三、配置定时任务
在Spring Boot中配置定时任务有两种方式:基于注解和基于XML配置。我们这里选择基于注解的方式。
首先,在启动类上添加@EnableScheduling注解,启用Spring Schedule:
@SpringBootApplication@EnableSchedulingpublicclassApplication{publicstaticvoidmain(String[] args){SpringApplication.run(Application.class, args);}}
然后,在需要执行定时任务的方法上添加@Scheduled注解,指定定时任务的执行时间:
@ServicepublicclassMyService{@Scheduled(cron ="0 0 0 * * ?")publicvoidbackupDatabase(){// 备份数据库的代码}}
这个例子中,我们定义了一个名为backupDatabase的方法,并且使用@Scheduled注解指定了它每天0点执行一次。
其中,cron参数是一个Cron表达式,用来指定任务的执行时间。
四、任务并发执行问题
默认情况下,Spring Schedule是单线程执行任务的,也就是说,如果一个任务还没执行完,下一个任务就必须等待。如果任务的执行时间较长,那么会影响其他任务的执行,甚至会导致任务积压,最终导致整个应用崩溃。
为了避免这种情况的发生,我们需要在配置文件中添加以下配置:
spring.task.scheduling.pool.size=10
一个大小为10的线程池来执行任务,也就是说,最多可以并发执行10个任务。这样就能避免任务积压和应用崩溃的问题了。
五、动态调整定时任务
有时候,我们需要动态调整定时任务的执行时间,比如说,某个任务需要紧急执行,我们就需要把它的执行时间提前。在Spring Boot中,我们可以使用@Scheduled注解的fixedRate和fixedDelay属性来实现动态调整定时任务。
fixedRate属性指定任务执行的频率,单位是毫秒。如果任务的执行时间大于fixedRate,那么任务会在执行完后立即再次执行;如果任务的执行时间小于fixedRate,那么任务会在fixedRate时间间隔后再次执行。
fixedDelay属性指定任务执行的延迟时间,单位也是毫秒。如果任务的执行时间大于fixedDelay,那么任务会在执行完后延迟fixedDelay时间后再次执行;如果任务的执行时间小于fixedDelay,那么任务会在fixedDelay-executionTime时间间隔后再次执行。
下面是一个动态调整定时任务执行时间的例子
@ServicepublicclassMyService{privatelong fixedRate =60*1000;// 默认执行间隔为1分钟@Scheduled(fixedRate ="#{myService.fixedRate}")publicvoiddoTask(){// 执行任务的代码}publicvoidupdateFixedRate(long fixedRate){this.fixedRate = fixedRate;}}
这个例子中,我们定义了一个名为doTask的方法,并且使用@Scheduled注解指定了它的执行频率为fixedRate。同时,我们还定义了一个名为updateFixedRate的方法,用来动态调整定时任务的执行频率。
在updateFixedRate方法中,我们可以更新fixedRate属性的值,从而改变定时任务的执行频率。注意,我们在@Scheduled注解的fixedRate属性中使用了SpEL表达式来获取fixedRate属性的值,这样就能实现动态调整定时任务的执行频率了。
六、业务场景
6.1 从数据库中获取cron表达式,启动定时任务
**可以通过在Spring Boot项目中定义一个
ScheduledTaskRegistrar
bean,并在其中注册一个
Runnable
对象,以实现在项目启动后手动创建 Spring Schedule 定时任务**。
同时,**使用
JdbcTemplate
从 MySQL 数据库中获取 cron 表达式并将其传递给
CronTrigger
对象以定义定时任务的调度时间**。
示例demo
@Configuration@EnableSchedulingpublicclassScheduleConfigimplementsSchedulingConfigurer{@AutowiredprivateJdbcTemplate jdbcTemplate;@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrar taskRegistrar){
taskRegistrar.addTriggerTask(newRunnable(){@Overridepublicvoidrun(){// 这里是您的定时任务执行的逻辑}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){String cronExpression =getCronExpressionFromDatabase();if(cronExpression ==null|| cronExpression.isEmpty()){returnnull;// 如果 cron 表达式为空,禁止执行定时任务}CronTrigger cronTrigger =newCronTrigger(cronExpression);return cronTrigger.nextExecutionTime(triggerContext);}});}privateStringgetCronExpressionFromDatabase(){// 从 MySQL 数据库中获取 cron 表达式// 使用 JdbcTemplate 执行 SQL 查询,返回 cron 表达式字符串// 示例代码:String sql ="SELECT cron_expression FROM my_table WHERE task_name = 'my_task'";String cronExpression = jdbcTemplate.queryForObject(sql,String.class);return cronExpression;}}
**在上面的代码中,
ScheduledTaskRegistrar
bean 会自动在项目启动后被创建,并通过
configureTasks
方法注册了一个定时任务。该任务使用
Runnable
对象定义了任务执行的逻辑,使用
Trigger
对象定义了任务的调度时间。**
**在
Trigger
对象中,我们通过调用
getCronExpressionFromDatabase
方法从 MySQL 数据库中获取 cron 表达式,并使用
CronTrigger
对象来定义该定时任务的调度时间。在
nextExecutionTime
方法中,
TriggerContext
参数可以提供有关上一次执行时间和上下文信息的详细信息。**
**请注意,
ScheduledTaskRegistrar
bean 的
@EnableScheduling
注解用于启用 Spring Schedule 功能。**
6.2 从数据库中获取cron表达式,启动定时任务,任务执行时间超过下次执行时间,继续执行
在6.1的基础上继续修改
@Configuration@EnableSchedulingpublicclassScheduleConfigimplementsSchedulingConfigurer{@AutowiredprivateJdbcTemplate jdbcTemplate;@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrar taskRegistrar){
taskRegistrar.addFixedDelayTask(newRunnable(){@Overridepublicvoidrun(){// 这里是您的定时任务执行的逻辑}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){String cronExpression =getCronExpressionFromDatabase();CronTrigger cronTrigger =newCronTrigger(cronExpression);Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);if(nextExecutionTime !=null){// 如果下一次执行时间不为空,计算执行时间与当前时间之间的差值long delay = nextExecutionTime.getTime()-System.currentTimeMillis();if(delay <0){// 如果下一次执行时间已经过去,设置延迟时间为 0
delay =0;}returnnewDate(System.currentTimeMillis()+ delay);}else{// 如果下一次执行时间为空,返回 nullreturnnull;}}});}privateStringgetCronExpressionFromDatabase(){// 从 MySQL 数据库中获取 cron 表达式// 使用 JdbcTemplate 执行 SQL 查询,返回 cron 表达式字符串// 示例代码:String sql ="SELECT cron_expression FROM my_table WHERE task_name = 'my_task'";String cronExpression = jdbcTemplate.queryForObject(sql,String.class);return cronExpression;}}
6.3 从数据库中获取定时任务,按照任务配置cron表达式启动,并监听数据库是否有新的定时任务生成
在6.2的基础上修改,原有的定时任务肯定还是不能动,那么就增加一个默认的定时任务来帮忙
- 获取定时任务
- 增加默认轮训定时任务- 去重,追加新的定时任务
- 增加定时任务
@Configuration@EnableSchedulingpublicclassScheduleConfigimplementsSchedulingConfigurer{@AutowiredprivateJdbcTemplate jdbcTemplate;/**
* 当前正在执行的定时任务(数据库可能突然插入)
*/privateList<Task> registerTaskList;@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrar taskRegistrar){// 待执行定时任务List<Task> tasks =findTasksToExecute();if(CollUtil.isEmpty(registerTaskList)){
registerTaskList =newArrayList<>();}// 追加定时任务列表synchronized(registerTaskList){
registerTaskList.addAll(tasks);}// 轮询追加定时任务
taskRegistrar.addFixedDelayTask(newRunnable(){@Overridepublicvoidrun(){// 追加新的定时任务(数据库插入新的定时任务,自动追加执行)addNewScheduleTask(taskRegistrar);}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){// 设置当前时间和时区Calendar nextExecutionTime =Calendar.getInstance();TimeZone timeZone =TimeZone.getTimeZone("Asia/Shanghai");// 设置为您所在的时区
nextExecutionTime.setTimeZone(timeZone);Date lastCompletionTime = triggerContext.lastCompletionTime();if(lastCompletionTime !=null){
nextExecutionTime.setTime(lastCompletionTime);}// 延迟1分钟启动
nextExecutionTime.add(Calendar.MINUTE,1);// 设置定时任务的触发规则(延迟1分钟启动,每5s执行一次)CronTrigger cronTrigger =newCronTrigger("5 * * * * ?", timeZone);return cronTrigger.nextExecutionTime(triggerContext);}});if(!CollectionUtil.isEmpty(tasks)){for(Task task : tasks){String cronExpression = task.getCronExpression();
taskRegistrar.addFixedDelayTask(newRunnable(){@Overridepublicvoidrun(){// 业务执行逻辑// executeScheduleTask(task);}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){CronTrigger cronTrigger =newCronTrigger(cronExpression);Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);if(nextExecutionTime !=null){// 如果下一次执行时间不为空,计算执行时间与当前时间之间的差值long delay = nextExecutionTime.getTime()-System.currentTimeMillis();if(delay <0){// 如果下一次执行时间已经过去,设置延迟时间为 0
delay =0;}returnnewDate(System.currentTimeMillis()+ delay);}else{// 如果下一次执行时间为空,返回 nullreturnnull;}}});}}}/**
* 追加新的定时任务.
*
* @param taskRegistrar 定时任务注册器
*/privatevoidaddNewScheduleTask(ScheduledTaskRegistrar taskRegistrar){// 数据库中新的定时任务List<Task> newTaskList =findNewTasksToExecute();if(CollUtil.isNotEmpty(newTaskList)){// 存在新的定时任务synchronized(registerTaskList){
registerTaskList.addAll(newTaskList);}for(Task newTask : newTaskList){String newCronExpression = newTask.getCronExpression();
taskRegistrar.addTriggerTask(()->{// 定时任务的逻辑executeScheduleTask(newTask);},
triggerContext ->{if(newCronExpression ==null|| newCronExpression.isEmpty()){// 如果 cron 表达式为空,禁止执行定时任务returnnull;}CronTrigger cronTrigger =newCronTrigger(newCronExpression);Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);if(nextExecutionTime !=null){// 如果下一次执行时间不为空,计算执行时间与当前时间之间的差值long delay = nextExecutionTime.getTime()-System.currentTimeMillis();if(delay <0){// 如果下一次执行时间已经过去,设置延迟时间为 0
delay =0;}returnnewDate(System.currentTimeMillis()+ delay);}else{// 如果下一次执行时间为空,返回 nullreturnnull;}});// 重新加载任务
taskRegistrar.afterPropertiesSet();}}}/**
* 执行定时任务.
*
* @param task 定时任务
*/voidexecuteScheduleTask(Task task){// ....}/**
* 获取定时任务.
*
* @return 定时任务
*/privateList<Task>findTasksToExecute(){String sql ="select * from task";List<Task> queryForList = jdbcTemplate.query(sql,newTaskRowMapper());return queryForList;}/**
* 获取数据库中新的定时任务.
*
* @return 定时任务集合
*/privateList<Task>findNewTasksToExecute(){List<Task> tasks =findTasksToExecute();if(CollUtil.isEmpty(tasks)){returnnewArrayList<>();}// 通过主键进行区分Map<Integer,Task> registerTaskMap = registerTaskList.stream().collect(Collectors.toMap(Task::getId,Function.identity()));
tasks = tasks.stream().filter(e ->!registerTaskMap.containsKey(e.getKeyId())).collect(Collectors.toList());return tasks;}}
6.4 启动定时任务,任务结束时,关闭定时任务
关闭定时任务
- 通过
ScheduledTaskRegistrar.destroy()
方法- 关闭ScheduledTaskRegistrar
中注册的所有定时任务(一键关闭) - 通过
ScheduledFuture.cancel(true)
方法关闭当前定时任务- 关闭ScheduledFuture
注册的定时任务(选择关闭)
基于
ScheduledTaskRegistrar.destroy()
基于代码6.1进行改造
@@Configuration@EnableSchedulingpublicclassScheduleConfigimplementsSchedulingConfigurer{@AutowiredprivateJdbcTemplate jdbcTemplate;@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrar taskRegistrar){Task task =newTask();
taskRegistrar.addTriggerTask(newRunnable(){@Overridepublicvoidrun(){// 这里是您的定时任务执行的逻辑excute(task);if(isClose(task)){// 符合关闭条件,关闭定时任务
taskRegistrar.destroy();}}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){String cronExpression =getCronExpressionFromDatabase();if(cronExpression ==null|| cronExpression.isEmpty()){returnnull;// 如果 cron 表达式为空,禁止执行定时任务}CronTrigger cronTrigger =newCronTrigger(cronExpression);return cronTrigger.nextExecutionTime(triggerContext);}});}privateStringgetCronExpressionFromDatabase(){// 从 MySQL 数据库中获取 cron 表达式// 使用 JdbcTemplate 执行 SQL 查询,返回 cron 表达式字符串// 示例代码:String sql ="SELECT cron_expression FROM my_table WHERE task_name = 'my_task'";String cronExpression = jdbcTemplate.queryForObject(sql,String.class);return cronExpression;}}
基于
ScheduledFuture.cancel(true)
基于代码6.1改造
定义
TaskScheduler
@Configuration@EnableSchedulingpublicclassTaskSchedulerConfig{@BeanpublicTaskSchedulertaskScheduler(){ThreadPoolTaskScheduler taskScheduler =newThreadPoolTaskScheduler();// 设置线程池大小
taskScheduler.setPoolSize(15);// 设置线程名前缀
taskScheduler.setThreadNamePrefix("MyTaskScheduler-");// 设置等待线程池关闭的时间
taskScheduler.setAwaitTerminationSeconds(3600);// 在关闭时等待所有任务完成
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);// 其他定制配置...return taskScheduler;}}
定义定时任务
@Configuration@EnableSchedulingpublicclassScheduleConfigimplementsSchedulingConfigurer{@AutowiredprivateJdbcTemplate jdbcTemplate;/**
* 定时任务(用于关闭某项定时任务)
*/@AutowiredprivateTaskScheduler taskScheduler;/**
* 定时任务(用于关闭某项定时任务)
*/privateScheduledFuture<?> scheduledFuture;@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrar taskRegistrar){
taskRegistrar.setTaskScheduler(taskScheduler);Task task =newTask();
scheduledFuture = taskScheduler.schedule(newRunnable(){@Overridepublicvoidrun(){// 这里是您的定时任务执行的逻辑excute(task);if(isClose(task)){// 符合关闭条件,关闭定时任务
scheduledFuture.close(true);}}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){String cronExpression =getCronExpressionFromDatabase();if(cronExpression ==null|| cronExpression.isEmpty()){returnnull;// 如果 cron 表达式为空,禁止执行定时任务}CronTrigger cronTrigger =newCronTrigger(cronExpression);return cronTrigger.nextExecutionTime(triggerContext);}});}privateStringgetCronExpressionFromDatabase(){// 从 MySQL 数据库中获取 cron 表达式// 使用 JdbcTemplate 执行 SQL 查询,返回 cron 表达式字符串// 示例代码:String sql ="SELECT cron_expression FROM my_table WHERE task_name = 'my_task'";String cronExpression = jdbcTemplate.queryForObject(sql,String.class);return cronExpression;}}
通过
taskScheduler.schedule
创建任务,依据判断条件
isClose()
关闭定时任务
6.5 从数据库中获取定时任务,按照任务配置cron表达式启动,并监听数据库是否有新的定时任务生成,任务执行完毕后,关闭当前定时任务.
基于6.3,6.4代码进行改造!!!
由于我们采用的是一次定义多任务,但关闭时要求只关闭当前任务,所以不能采用
ScheduledTaskRegistrar.destroy()
,它会将所有任务一起关闭,源码如下:
@Overridepublicvoiddestroy(){for(ScheduledTask task :this.scheduledTasks){
task.cancel();}if(this.localExecutor !=null){this.localExecutor.shutdownNow();}}
ScheduledTask.cancel
源码如下:
publicvoidcancel(){ScheduledFuture<?> future =this.future;if(future !=null){
future.cancel(true);}}
与我们的
ScheduledFutur.cancel一致
,定点删除,因此我们开始正题
定义
TaskScheduler
@Configuration@EnableSchedulingpublicclassTaskSchedulerConfig{@BeanpublicTaskSchedulertaskScheduler(){ThreadPoolTaskScheduler taskScheduler =newThreadPoolTaskScheduler();// 设置线程池大小
taskScheduler.setPoolSize(15);// 设置线程名前缀
taskScheduler.setThreadNamePrefix("MyTaskScheduler-");// 设置等待线程池关闭的时间
taskScheduler.setAwaitTerminationSeconds(3600);// 在关闭时等待所有任务完成
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);// 其他定制配置...return taskScheduler;}}
定义定时任务
@Configuration@EnableSchedulingpublicclassScheduleConfigimplementsSchedulingConfigurer{@AutowiredprivateJdbcTemplate jdbcTemplate;/**
* 当前正在执行的定时任务(数据库可能突然插入)
*/privateList<Task> registerTaskList;/**
* 定时任务(用于关闭某项定时任务)
*/@AutowiredprivateTaskScheduler taskScheduler;/**
* 存储所有的定时任务map<唯一标识,定时任务>
*/privateMap<String,ScheduledFuture> scheduledFutureMap;@OverridepublicvoidconfigureTasks(ScheduledTaskRegistrar taskRegistrar){// 待执行定时任务List<Task> tasks =findTasksToExecute();if(CollUtil.isEmpty(registerTaskList)){
registerTaskList =newArrayList<>();}// 追加定时任务列表synchronized(registerTaskList){
registerTaskList.addAll(tasks);}
taskRegistrar.setTaskScheduler(taskScheduler);// 轮询追加定时任务ScheduledFuture<?> defaultScheduledFuture = taskScheduler.schedule(newRunnable(){@Overridepublicvoidrun(){// 追加新的定时任务(数据库插入新的定时任务,自动追加执行)addNewScheduleTask(taskRegistrar);}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){// 设置当前时间和时区Calendar nextExecutionTime =Calendar.getInstance();TimeZone timeZone =TimeZone.getTimeZone("Asia/Shanghai");// 设置为您所在的时区
nextExecutionTime.setTimeZone(timeZone);Date lastCompletionTime = triggerContext.lastCompletionTime();if(lastCompletionTime !=null){
nextExecutionTime.setTime(lastCompletionTime);}// 延迟1分钟启动
nextExecutionTime.add(Calendar.MINUTE,1);// 设置定时任务的触发规则(延迟1分钟启动,每5s执行一次)CronTrigger cronTrigger =newCronTrigger("5 * * * * ?", timeZone);return cronTrigger.nextExecutionTime(triggerContext);}});// 管理默认任务
scheduledFutureMap.put("default", defaultScheduledFuture);if(!CollectionUtil.isEmpty(tasks)){for(Task task : tasks){String cronExpression = task.getCronExpression();ScheduledFuture<?> schedule = taskScheduler.schedule(newRunnable(){@Overridepublicvoidrun(){// 业务执行逻辑// executeScheduleTask(task);}},newTrigger(){@OverridepublicDatenextExecutionTime(TriggerContext triggerContext){CronTrigger cronTrigger =newCronTrigger(cronExpression);Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);if(nextExecutionTime !=null){// 如果下一次执行时间不为空,计算执行时间与当前时间之间的差值long delay = nextExecutionTime.getTime()-System.currentTimeMillis();if(delay <0){// 如果下一次执行时间已经过去,设置延迟时间为 0
delay =0;}returnnewDate(System.currentTimeMillis()+ delay);}else{// 如果下一次执行时间为空,返回 nullreturnnull;}}});// 通过主键作为唯一标识,退出时获取scheduledFuture退出String identity =String.valueOf(task.getId());
scheduledFutureMap.put(identity, schedule);}}}/**
* 追加新的定时任务.
*
* @param taskRegistrar 定时任务注册器
*/privatevoidaddNewScheduleTask(ScheduledTaskRegistrar taskRegistrar){// 数据库中新的定时任务List<Task> newTaskList =findNewTasksToExecute();if(CollUtil.isNotEmpty(newTaskList)){// 存在新的定时任务synchronized(registerTaskList){
registerTaskList.addAll(newTaskList);}for(Task newTask : newTaskList){String newCronExpression = newTask.getCronExpression();ScheduledFuture<?> schedule = taskScheduler.schedule(()->{// 定时任务的逻辑executeScheduleTask(newTask);},
triggerContext ->{if(newCronExpression ==null|| newCronExpression.isEmpty()){// 如果 cron 表达式为空,禁止执行定时任务returnnull;}CronTrigger cronTrigger =newCronTrigger(newCronExpression);Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);if(nextExecutionTime !=null){// 如果下一次执行时间不为空,计算执行时间与当前时间之间的差值long delay = nextExecutionTime.getTime()-System.currentTimeMillis();if(delay <0){// 如果下一次执行时间已经过去,设置延迟时间为 0
delay =0;}returnnewDate(System.currentTimeMillis()+ delay);}else{// 如果下一次执行时间为空,返回 nullreturnnull;}});// 通过主键作为唯一标识,退出时获取scheduledFuture退出String identity =String.valueOf(task.getId());
scheduledFutureMap.put(identity, schedule);// 重新加载任务
taskRegistrar.afterPropertiesSet();}}}/**
* 执行定时任务.
*
* @param task 定时任务
*/voidexecuteScheduleTask(Task task){// 定时任务// 判断是否关闭当前定时任务if(isClose(task)){String identity =String.valueOf(task.getId());if(scheduledFutureMap.containsKey(identity)){ScheduledFuture scheduledFuture = scheduledFutureMap.get(identity);// 取消当前任务
scheduledFuture.cancel(true);
scheduledFutureMap.remove(identity);return;}}}/**
* 获取定时任务.
*
* @return 定时任务
*/privateList<Task>findTasksToExecute(){String sql ="select * from task";List<Task> queryForList = jdbcTemplate.query(sql,newTaskRowMapper());return queryForList;}/**
* 获取数据库中新的定时任务.
*
* @return 定时任务集合
*/privateList<Task>findNewTasksToExecute(){List<Task> tasks =findTasksToExecute();if(CollUtil.isEmpty(tasks)){returnnewArrayList<>();}// 通过主键进行区分Map<Integer,Task> registerTaskMap = registerTaskList.stream().collect(Collectors.toMap(Task::getId,Function.identity()));
tasks = tasks.stream().filter(e ->!registerTaskMap.containsKey(e.getKeyId())).collect(Collectors.toList());return tasks;}}
七、总结
通过本文的介绍,我们可以看到,在Spring Boot应用中整合Spring Schedule非常简单,只
需要添加依赖、配置定时任务,就能实现定时任务的执行。同时,我们还介绍了如何解决任
务并发执行的问题,以及如何动态调整定时任务的执行时间,动态关闭定时任务。
希望这篇文章对您有所帮助。
版权归原作者 故乡的胡辣汤开张了吗 所有, 如有侵权,请联系我们删除。