SpringBoot手动注册定时任务
一、背景
项目存在这样一个场景。程序启动过程中,在Spring的Bean组件注册完毕后,会初始化一些基础数据到数据库中,而项目中有部分定时任务需要依赖这些基础数据才能正常运行。如果直接使用@Scheduled注解标注定时任务方法,会导致定时任务提前执行且执行失败。
基于以上背景,需要将定时任务的注册执行放在数据初始化以后,那么这部分定时任务就需要手动注册且与基础数据初始化操作保持同步执行,保证定时任务的执行一定晚于数据初始化。
二、方案
自定义配置一个定时任务注册器和定时任务执行器,用于处理我们的需求场景。
三、编码实现
通过阅读Spring实现的定时任务源码(篇幅有限,不展开),得知其底层使用的执行器org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
,使用方法org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler#schedule(java.lang.Runnable, org.springframework.scheduling.Trigger)
来配置启动定时任务。
因此,我们也使用这个类来启动需要控制执行时机的定时任务,实现代码如下:
package com.jacks.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
/**
* 配置自定义定时任务执行器
*
* @author Jacks丶
* @since 2025-03-16
*/
@Configuration
@Slf4j
public class ScheduledConfig implements SchedulingConfigurer {
private TaskScheduler scheduler;
/**
* 配置定时任务注册器,注册器中自定义执行器用于手动执行定时任务
*
* @param taskRegistrar the registrar to be configured
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
scheduler = buildScheduler();
taskRegistrar.setScheduler(scheduler);
}
/**
* 自定义定时任务执行器,用于执行定时任务
*
* @return 定时任务执行器对象
*/
@Bean
public TaskScheduler buildScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("thread-task-t-");
scheduler.setPoolSize(2);
return scheduler;
}
/**
* 初始化定时任务,提供给其他Bean对象手动注册定时任务
*/
public void initScheduledTask() {
scheduler.schedule(() -> System.out.println("定时任务执行了..."), new CronTrigger("*/2 * * * * *"));
}
}
此时我们的初始化数据的逻辑如下:
package com.jacks.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import java.time.Duration;
/**
* SpringBoot启动阶段执行
*
* @author Jacks丶
* @since 2025-03-16
*/
@Slf4j
public class SpringTask implements SpringApplicationRunListener {
public SpringTask(SpringApplication application, String[] args) {
log.info("hello init SpringTask.");
}
/**
* Springboot应用已就绪,可在此初始化基础数据
*
* @param context 上下文对象,包含Bean对象,可用于获取定时任务配置Bean,然后调用init方法
* @param timeTaken 应用准备所花费时间,可用于了解性能
*/
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
log.info("开始初始化 ....");
try {
// 模拟数据初始化耗时
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("结束初始化 ...");
// 获取定时任务配置类bean对象执行初始化方法启动定时任务
context.getBean(ScheduledConfig.class).initScheduledTask();
}
}
启动类如下:
package com.jacks;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 启动入口类
*
* @author Jacks丶
* @since 2025-03-16
*/
@EnableScheduling
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
此时可见控制台输出:
2025-03-16 18:43:10.489 INFO 17176 --- [ main] com.jacks.DemoApplication : Started DemoApplication in 0.88 seconds (JVM running for 1.185)
2025-03-16 18:43:10.490 INFO 17176 --- [ main] com.jacks.task.SpringTask : 开始初始化 ....
2025-03-16 18:43:12.503 INFO 17176 --- [ main] com.jacks.task.SpringTask : 结束初始化 ...
2025-03-16 18:43:14.006 INFO 17176 --- [thread-task-t-1] com.jacks.task.ScheduledConfig : 定时任务执行了...
2025-03-16 18:43:16.001 INFO 17176 --- [thread-task-t-1] com.jacks.task.ScheduledConfig : 定时任务执行了...
2025-03-16 18:43:18.014 INFO 17176 --- [thread-task-t-1] com.jacks.task.ScheduledConfig : 定时任务执行了...
四、代码优化
如何优雅的将以上逻辑写入到项目中呢?那就需要引入依赖倒置、单一职责等思想。以上代码中,定时任务是直接定义在initScheduledTask
方法中的,当我们需要添加定时任务时,那么就会侵入式的修改com.jacks.task.ScheduledConfig#initScheduledTask
。
基于单一职责思想,定时任务配置类就只负责初始化注册器、执行器和启动定时任务就好,定时任务的定义逻辑就需要提取出去。
基于依赖倒置的思想,我们需要将注册时依赖的具体实现类优化成接口,所以需要抽取接口,让注册依赖接口,而不是具体实现,我们在新增定时任务时,只需要继承接口编写定时任务逻辑即可,无需侵入式的修改原逻辑。
1、定义接口获取定时任务
package com.jacks.service;
import org.springframework.scheduling.config.CronTask;
import java.util.List;
/**
* 定时任务接口,用于提供可执行的定时任务对象
*
* @author Jacks丶
* @since 2025-03-16
*/
public interface IScheduled {
/**
* 提供定时任务执行对象
*
* @return 定时任务执行对象列表
*/
List<CronTask> getScheduledTasks();
}
2、在实现类定义定时任务逻辑
package com.jacks.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.config.CronTask;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 定时任务方法
*
* @author Jacks丶
* @since 2025-03-16
*/
@Slf4j
@Service
public class MyService implements IScheduled {
/**
* 定时任务逻辑
*/
public void task() {
log.info("task1 exec..");
}
/**
* 提供定时任务执行对象,用于初始化注册
*
* @return 定时任务列表
*/
@Override
public List<CronTask> getScheduledTasks() {
List<CronTask> cronTasks = new ArrayList<>();
cronTasks.add(new CronTask(this::task, "*/2 * * * * *"));
return cronTasks;
}
}
3、单一职责,定时任务抽取为方法入参
package com.jacks.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import java.util.List;
/**
* 配置自定义定时任务执行器
*
* @author Jacks丶
* @since 2025-03-16
*/
@Configuration
@Slf4j
public class ScheduledConfig implements SchedulingConfigurer {
private TaskScheduler scheduler;
/**
* 配置定时任务注册器,注册器中自定义执行器用于手动执行定时任务
*
* @param taskRegistrar the registrar to be configured
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
scheduler = buildScheduler();
taskRegistrar.setScheduler(scheduler);
}
/**
* 自定义定时任务执行器,用于执行定时任务
*
* @return 定时任务执行器对象
*/
@Bean
public TaskScheduler buildScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("thread-task-t-");
scheduler.setPoolSize(2);
return scheduler;
}
/**
* 初始化定时任务,提供给其他Bean对象手动注册定时任务
*
* @param cronTasks 待执行的定时任务列表
*/
public void initScheduledTask(List<CronTask> cronTasks) {
cronTasks.forEach(task -> scheduler.schedule(task.getRunnable(), task.getTrigger()));
}
}
4、依赖倒置,注册时依赖接口,而不是具体实现
注册定时任务时获取所有com.jacks.service.IScheduled
的实现类,并调用getScheduledTasks
方法获取定时任务。
package com.jacks.task;
import com.jacks.service.IScheduled;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.scheduling.config.CronTask;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
/**
* SpringBoot启动阶段执行
*
* @author Jacks丶
* @since 2025-03-16
*/
@Slf4j
public class SpringTask implements SpringApplicationRunListener {
public SpringTask(SpringApplication application, String[] args) {
log.info("hello init SpringTask.");
}
/**
* Springboot应用已就绪,可在此初始化基础数据
*
* @param context 上下文对象,包含Bean对象,可用于获取定时任务配置Bean,然后调用init方法
* @param timeTaken 应用准备所花费时间,可用于了解性能
*/
@Override
public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
log.info("开始初始化 ....");
try {
// 模拟数据初始化耗时
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("结束初始化 ...");
// 获取定时任务配置类bean对象执行初始化方法启动定时任务
Collection<IScheduled> scheduledServiceList = context.getBeansOfType(IScheduled.class).values();
List<CronTask> cronTasks = scheduledServiceList.stream()
.flatMap(service -> service.getScheduledTasks().stream())
.collect(Collectors.toList());
context.getBean(ScheduledConfig.class).initScheduledTask(cronTasks);
}
}
五、结语
以上就是手动控制注册定时任务时机的一种实现方案,并且这种方案不会影响@Scheduled
注解注册的定时任务。
希望本篇博客对你有所帮助。