Spring定时任务的秘密
Spring定时任务的秘密
在 Spring 框架中,定时任务主要通过 @Scheduled
注解或 TaskScheduler
接口实现。
1.基本使用
在 Spring Boot 项目中,通过 @EnableScheduling
注解启用定时任务功能:
@SpringBootApplication
@MapperScan("com.feng.tackle.dao")
@EnableScheduling
public class DateApplication {
public static void main(String[] args) {
SpringApplication.run(DateApplication.class, args);
}
}
然后在spring的组件中,使用 @Scheduled
注解标记任务方法
@Component
public class MyScheduler {
// 固定频率(每隔 5 秒执行一次)
@Scheduled(fixedRate = 5000)
public void task1() {
// 业务逻辑
}
// 固定延迟(任务结束后延迟 3 秒再执行)
@Scheduled(fixedDelay = 3000)
public void task2() {
// 业务逻辑
}
// Cron 表达式(每天 12 点执行)
@Scheduled(cron = "0 0 12 * * ?")
public void task3() {
// 业务逻辑
}
}
其实用起来特别地简单。
2. 原理解析
核心部分:
@EnableScheduling
入口注解,触发SchedulingConfiguration
配置类,注册核心后置处理器ScheduledAnnotationBeanPostProcessor
。ScheduledAnnotationBeanPostProcessor
负责扫描 Bean 中的@Scheduled
注解方法,解析并注册定时任务。TaskScheduler
任务调度接口,默认实现为ThreadPoolTaskScheduler
(基于ScheduledExecutorService
)。ScheduledTaskRegistrar
任务注册中心,管理所有定时任务的注册和执行。
一、构建任务
@EnableScheduling
,在启动类上面加了这么一个注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class) //============
@Documented
public @interface EnableScheduling {
}
看过SpringBoot原理分析的都知道,@Import(SchedulingConfiguration.class)
这个就是突破口了嘛。
继续往下
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {
@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor(); // 往容器里面放进了这样一个bean
}
}
想必那个类就是核心了,ScheduledAnnotationBeanPostProcessor源码中最开头的英文的翻译如下
Bean 后处理器,它注册带有 @Scheduled 注释的方法,以便由TaskScheduler根据通过 Comments 提供的“fixedRate”、“fixedDelay”或“cron”表达式调用。这个后处理器由 Spring 的 <task:annotation-driven> XML 元素以及 @EnableScheduling 注释自动注册。自动检测容器中的任何 SchedulingConfigurer 实例,允许自定义要使用的调度器或对任务注册进行精细控制(例如,注册 Trigger 任务)
既然是xxxBeanPostProcessor了,那么肯定是实现了BeanPostProcessor接口,重写了postProcessAfterInitialization方法
。
下面来解析这个实现类的具体方法
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 忽略 AOP 基础设施类(如 ScopedProxy)和任务调度器自身
if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
bean instanceof ScheduledExecutorService) {
return bean;
}
Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
if (!this.nonAnnotatedClasses.contains(targetClass) &&
AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
// 返回 Map<Method, Set<Scheduled>>,键为方法对象,值为该方法上的所有 @Scheduled 注解集合。
Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
Set<Scheduled> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class);
return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : null);
});
// 如果没有
if (annotatedMethods.isEmpty()) {
this.nonAnnotatedClasses.add(targetClass); //将不包含 @Scheduled 注解的类加入缓存,后续跳过扫描以提高性能。
if (logger.isTraceEnabled()) {
logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
}
}
else { // 如果有-------------------------------
annotatedMethods.forEach((method, scheduledAnnotations) ->
scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
if (logger.isTraceEnabled()) {
logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
"': " + annotatedMethods);
}
}
}
return bean;
}
经过上面的大致分析,发现else里面,才是我们想看的,各位也可以打断点调试。
annotatedMethods.forEach(
(method, scheduledAnnotations) ->
scheduledAnnotations.forEach(
scheduled -> processScheduled(scheduled, method, bean)
)
);
processScheduled()
方法,构建任务。
private final ScheduledTaskRegistrar registrar;
private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16);
protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
....
// 可以看出来了把,任务其实就是个runnable
Runnable runnable = createRunnable(bean, method);
.....
Set<ScheduledTask> tasks = new LinkedHashSet<>(4);
// 检查注解的延迟属性
long initialDelay = convertToMillis(scheduled.initialDelay(), scheduled.timeUnit());
.....
//cron表达式
String cron = scheduled.cron();
.....
tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
....
// 检查周期属性
long fixedDelay = convertToMillis(scheduled.fixedDelay(), scheduled.timeUnit());
...
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
...
String fixedDelayString = scheduled.fixedDelayString();
..........
tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
.........
// Finally register the scheduled tasks
synchronized (this.scheduledTasks) {
Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
regTasks.addAll(tasks); // 全加进去咯
}
.....
}
二、自动配置
熟悉springboot自动配置原理的都知道,spring-boot-autoconfigure里面官方定义了超级多的场景。我们去找找看。
一下子就找到了 task目录下面的TaskSchedulingAutoConfiguration
. 【默认是单线程的】
@ConditionalOnClass(ThreadPoolTaskScheduler.class)
@AutoConfiguration(after = TaskExecutionAutoConfiguration.class)
@EnableConfigurationProperties(TaskSchedulingProperties.class)
public class TaskSchedulingAutoConfiguration {
@Bean
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
// 如果容器中有我们自定义的这种bean,这个就失效了,@ConditionalOnMissingBean注解的作用
@ConditionalOnMissingBean({ SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class })
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) { // 下面构建的bean,拿来这里用了
return builder.build();
}
@Bean
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() {
return new ScheduledBeanLazyInitializationExcludeFilter();
}
@Bean
// 如果容器中有我们自定义的这种bean,这个就失效了,@ConditionalOnMissingBean注解的作用
@ConditionalOnMissingBean
public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, // 从properties读取的
ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
builder = builder.poolSize(properties.getPool().getSize()); // 发现这里是 size = 1, 默认是单线程的!!!!!
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(taskSchedulerCustomizers);
return builder;
}
}
@ConfigurationProperties("spring.task.scheduling") // 说明我们可以根据这个配置来设置线程数那些参数
public class TaskSchedulingProperties {
}
三、任务调度
public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler {
@Nullable
private ScheduledExecutorService scheduledExecutor; // 内部持有一个线程池!!!! initializeExecutor()方法会初始化它
.........
// 主要看这个方法
@Override
@Nullable
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
ScheduledExecutorService executor = getScheduledExecutor(); // 先得到executor
try {
ErrorHandler errorHandler = this.errorHandler;
if (errorHandler == null) {
errorHandler = TaskUtils.getDefaultErrorHandler(true);
}
// 执行这一个
return new ReschedulingRunnable(task, trigger, this.clock, executor, errorHandler).schedule();
}
catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}
..........
}
3.案例分析
①: 什么都不配置,单线程
@Slf4j
@Component
public class TestJob {
@Scheduled(cron = "1-59 * * * * ?")
public void hello() {
try {
log.info("hello---任务开始");
TimeUnit.SECONDS.sleep(5);
log.info("hello---任务------------结束hello");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Scheduled(cron = "1-59 * * * * ?")
public void world() {
try {
log.info("world---任务开始");
TimeUnit.SECONDS.sleep(2);
log.info("world---任务--------------结束world");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
运行结果
world任务,两个之间间隔了7秒钟。!!! 同理,两个hello()任务也间隔了七秒钟。也就是说,任务之间互相影响了。【因为是单线程的嘛】
②:配置调度线程是多线程的
@Configuration
public class SchedulerConfig {
// 配置多线程的调度器, 默认是单线程
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10); // 设置调度线程池大小
scheduler.setThreadNamePrefix("ScheduledTask-");
return scheduler;
}
}
然后任务如下
@Scheduled(cron = "20/40 * * * * ? ") // 从每分钟的20秒开始,每40秒执行一次,---- xx:xx:20 xx:xx+1:20...
public void hello() {
try {
log.info("hello---任务开始");
TimeUnit.SECONDS.sleep(20);
log.info("hello---任务结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Scheduled(cron = "30/20 * * * * ? ") // 从每分钟的30开始,每20秒执行一次,---- xx:xx:30 xx:xx:50 xx:xx+1:30 xx:xx+1:50...
public void world() {
try {
log.info("world---任务开始");
TimeUnit.SECONDS.sleep(10);
log.info("world---任务结束");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
每一分钟内来看,应该是hello()在第20秒先执行,耗时20秒,在40秒结束, world()在第三十秒执行,耗时10秒,在40秒结束。 如果被阻塞的话,world()任务将会在第40秒执行。
但是看执行效果,发现并没有被阻塞,二者是按规定正常执行的,并没有互相影响。故我们的配置生效了,现在定时任务调度是多线程的。【图中也可以看到 执行的线程名字不一样】
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性