欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring 中的定时任务

程序员文章站 2022-06-14 22:02:11
文章目录定时任务原理在 Spring 中使用定时任务非常容易,步骤如下:第一步启动类加上 @EnableScheduling 注解。package com.springboot.chapter13;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.sched...

Spring 中使用定时任务

在 Spring 中使用定时任务非常容易,步骤如下:

  1. 第一步在启动类加上 @EnableScheduling 注解,开启对定时任务的支持。
package com.springboot.chapter13;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 开启对定时任务的支持
public class Chapter13Application {

    public static void main(String[] args) {
        SpringApplication.run(Chapter13Application.class, args);
    }
}
  1. 第二步在方法上加上 @Scheduled 注解,并配置配置项,编写定时逻辑。
package com.springboot.chapter13.task;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public class MyTask {

    int num = 1;

	// 每隔 1s 执行一次
    @Scheduled(fixedRate = 1000)
    public void job1() {
        System.out.println("敲了 " + num + " 个 BUG!");
        num++;
    }
}

别忘了在 MyTask 类标注 @Service@Component 等注解,将我们创建的 MyTask 类注入到 Spring 容器中,这样 Spring 才会去执行定时任务。

  1. 启动项目,控制台打印信息如下:
    Spring 中的定时任务

上述中 @Scheduled 只是按照时间间隔执行,有时候需要指定更为具体的时间,例如,每天晚上 11:00 开始任务,或者一些任务在每周日执行。为了能够更为精确地指定任务执行的时间,所以有必要更为细致地研究 @Scheduled 的配置项。

@Scheduled 注解源码

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
    String CRON_DISABLED = "-";

    String cron() default "";

    String zone() default "";

    long fixedDelay() default -1L;

    String fixedDelayString() default "";

    long fixedRate() default -1L;

    String fixedRateString() default "";

    long initialDelay() default -1L;

    String initialDelayString() default "";
}

@Scheduled 的配置项

配置项 类型 描述
cron String 使用表达式的方式定义任务执行时间
zone String 可以通过它设定区域时间
fixedDelay long 表示从上一个任务完成开始到下一个任务开始的间隔,单位为毫秒
fixedDelayString String 与 fixedDelay 相同,只是使用字符串,这样可以使用 SpEL 来引入配置文件的配置
initialDelay long 在 Spring IoC 容器完成初始化后,首次任务执行延迟时间,单位为毫秒
initialDelayString String 与 initialDelay 相同,只是使用字符串,这样可以使用 SpEL 来引入配置文件的配置
fixedRate long 从上一个任务开始到下一个任务开始的间隔,单位为毫秒
fixedRateString String 与 fixedRate 相同,只是使用字符串,这样可以使用 SpEL 来引入配置文件的配置

上表中的配置项除了 cron 外都比较好理解,只有 cron 是可以通过表达式更为灵活地配置运行的方式。cron 有 6~7 个空格分隔的时间元素,按顺序依次是 “ 秒分时天月星期年 ”,其中年是一个可以不配置的元素,例如下面的配置:

0 0 0 ? * WED

这个配置表示每个星期三中午 0 点整。这个表达式需要注意的是其中的特殊字符,如 ?*,这里因为天和星期会产生定义上的冲突,所以往往会以通配符 ? 表示,它表示不指定值,而 * 则表示任意的月。除此以外还会有下表所示的其他通配符。

通配符含义

通配符 描述
* 表示任意值
? 不指定值,用于处理天和日期配置的冲突
- 指定时间区间
/ 指定时间间隔执行
L 最后的
# 第几个
, 列举多个项

cron 表达式举例

项目类型 描述
0 0 0 * * ? 每天 0 点整触发
0 15 23 ? * * 每天 23:15 触发
0 15 0 * * ? 每天 0:15 触发
0 15 10 * * ? 每天早上 10:15 触发
0 30 10 * * ? 2018 2018 年的每天早上 10:30 触发
0 * 23 * * ? 每天从 23 点开始到 23 点 59 分每分钟一次触发
0 0/3 23 * * ? 每天从 23 点开始到 23:59 分结束每 3 min 一次触发
0 0/3 20,23 * * ? 每天的 20 点至 20:59 和 23 点至 23:59 分两个时间段内每 3 min 一次触发
0 0-5 21 * * ? 每天 21:00 至 21:05 每分钟一次触发
0 10,44 14 ? 3 WED 3 月的每周三的 14:00 和 14:44 触发
0 0 23 ? * MON-FRI 每周一到周五的 23:00 触发
0 30 23 ? * 6L 2017-2020 2017 年至 2020 年的每月最后一个周五的 23:30 触发
0 15 22 ? * 6#3 每月第三周周五的 22:15 触发

注意星期可以使用英文,也可以使用数字 1~7 来代替,1 代表星期天,2 代表星期一,以此类推,所以 7 就代表星期六。

小工具 -> 在线 Cron 表达式生成器:https://cron.qqe2.com

下面再通过两个实例来巩固对定时任务的理解,代码如下:

// Spring IoC 容器初始化后,第一次延迟 3 秒 , 每隔 1 秒执行一次
@Scheduled(initialDelay = 3000,fixedRate = 1000)
public void job2() {
    System.out.println("开始了!");
}

// 11:00 到 11:59 点每分钟执行一次
@Scheduled(cron = "0 * 11 * * ?")
public void job3() {
    System.out.println("睡着了没?");
}

定时任务原理

参考文章链接:https://blog.csdn.net/weixin_34452850/article/details/81127569

如上文所述,我们可以通过 @EnableScheduling 注解开启对定时任务的支持,所以我们先从 @EnableScheduling 注解开始说起。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}

可以看到,@EnableScheduling 注解的主要功能就是通过 @Import 注解导入了 SchedulingConfiguration 配置类,@Import 注解是 Spring 提供的导入注解,可以将一个配置类或者普通的一个类初始化到 Spring 容器中,具体使用可以参考 https://blog.csdn.net/u010502101/article/details/78760032,这里不再赘述。现在我们来看 SchedulingConfiguration 配置类。

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

	// 向 IOC 容器中注册一个 ScheduledAnnotationBeanPostProcessor 实例
	@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
		return new ScheduledAnnotationBeanPostProcessor();
	}
}

SchedulingConfiguration 类就是 Spring 定时任务的配置类,它的主要功能就是向容器中注入了一个ScheduledAnnotationBeanPostProcessor 后处理器实例。ScheduledAnnotationBeanPostProcessor 是 Spring 后处理器的一个典型应用场景(Spring 的 BeanPostProcessor 机制可参考 https://blog.csdn.net/elim168/article/details/76146351),定时任务的解析和注册都由该后处理器完成。下面我们重点分析 ScheduledAnnotationBeanPostProcessor。

ScheduledAnnotationBeanPostProcessor 类中有两个重要的成员变量:

// 定时任务的注册中心,维护所有定时任务实例
private final ScheduledTaskRegistrar registrar;

// ScheduledAnnotationBeanPostProcessor 单独维护的定时任务 Map
private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16);

ScheduledAnnotationBeanPostProcessor 注册定时任务的处理在后处理方法 postProcessAfterInitialization 中:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
	if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
			bean instanceof ScheduledExecutorService) {
		// Ignore AOP infrastructure such as scoped proxies.
		return bean;
	}

	// 扫描 bean 上标记了 @Scheduled 注解的方法
	Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
	if (!this.nonAnnotatedClasses.contains(targetClass) &&
			AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
		Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
				(MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
					Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
							method, Scheduled.class, Schedules.class);
					return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
				});
		if (annotatedMethods.isEmpty()) {
			this.nonAnnotatedClasses.add(targetClass);
			if (logger.isTraceEnabled()) {
				logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
			}
		}
		else {
			// Non-empty set of methods
			annotatedMethods.forEach((method, scheduledMethods) ->
					scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
			if (logger.isTraceEnabled()) {
				logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
						"': " + annotatedMethods);
			}
		}
	}
	return bean;
}

该方法的作用就是扫描 bean 上标记了 @Scheduled 注解的方法,然后执行 processScheduled 方法。

重点来看 processScheduled 方法:

protected void processScheduled(Scheduled scheduled, Method method, Object bean) {
	try {
		Runnable runnable = createRunnable(bean, method);
		boolean processedSchedule = false;
		String errorMessage =
				"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

		Set<ScheduledTask> tasks = new LinkedHashSet<>(4);

		// Determine initial delay
		long initialDelay = scheduled.initialDelay();
		String initialDelayString = scheduled.initialDelayString();
		if (StringUtils.hasText(initialDelayString)) {
			Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both");
			if (this.embeddedValueResolver != null) {
				initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString);
			}
			if (StringUtils.hasLength(initialDelayString)) {
				try {
					initialDelay = parseDelayAsLong(initialDelayString);
				}
				catch (RuntimeException ex) {
					throw new IllegalArgumentException(
							"Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
				}
			}
		}

		// Check cron expression
		String cron = scheduled.cron();
		if (StringUtils.hasText(cron)) {
			String zone = scheduled.zone();
			if (this.embeddedValueResolver != null) {
				cron = this.embeddedValueResolver.resolveStringValue(cron);
				zone = this.embeddedValueResolver.resolveStringValue(zone);
			}
			if (StringUtils.hasLength(cron)) {
				Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
				processedSchedule = true;
				if (!Scheduled.CRON_DISABLED.equals(cron)) {
					TimeZone timeZone;
					if (StringUtils.hasText(zone)) {
						timeZone = StringUtils.parseTimeZoneString(zone);
					}
					else {
						timeZone = TimeZone.getDefault();
					}
					tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
				}
			}
		}

		// At this point we don't need to differentiate between initial delay set or not anymore
		if (initialDelay < 0) {
			initialDelay = 0;
		}

		// Check fixed delay
		long fixedDelay = scheduled.fixedDelay();
		if (fixedDelay >= 0) {
			Assert.isTrue(!processedSchedule, errorMessage);
			processedSchedule = true;
			tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
		}
		String fixedDelayString = scheduled.fixedDelayString();
		if (StringUtils.hasText(fixedDelayString)) {
			if (this.embeddedValueResolver != null) {
				fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
			}
			if (StringUtils.hasLength(fixedDelayString)) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				try {
					fixedDelay = parseDelayAsLong(fixedDelayString);
				}
				catch (RuntimeException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
				}
				tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
			}
		}

		// Check fixed rate
		long fixedRate = scheduled.fixedRate();
		if (fixedRate >= 0) {
			Assert.isTrue(!processedSchedule, errorMessage);
			processedSchedule = true;
			tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
		}
		String fixedRateString = scheduled.fixedRateString();
		if (StringUtils.hasText(fixedRateString)) {
			if (this.embeddedValueResolver != null) {
				fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
			}
			if (StringUtils.hasLength(fixedRateString)) {
				Assert.isTrue(!processedSchedule, errorMessage);
				processedSchedule = true;
				try {
					fixedRate = parseDelayAsLong(fixedRateString);
				}
				catch (RuntimeException ex) {
					throw new IllegalArgumentException(
							"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
				}
				tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
			}
		}

		// Check whether we had any attribute set
		Assert.isTrue(processedSchedule, errorMessage);

		// Finally register the scheduled tasks
		synchronized (this.scheduledTasks) {
			Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
			regTasks.addAll(tasks);
		}
	}
	catch (IllegalArgumentException ex) {
		throw new IllegalStateException(
				"Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage());
	}
}

processScheduled 方法的参数包含了 @Scheduled 注解对象,标记了 @Scheduled 的方法实体和方法所在的 bean,它主要完成了三项工作:

  1. @Scheduled 注解的属性解析
  2. 定时任务的注册
  3. 定时任务的执行

我们来仔细看下具体的处理:

首先是对方法的一些校验,要求方法必须 无参,且 @Scheduled 注解的属性必须配置了 cron、fixedDelay 和 fixedRate 其中的一个。

Runnable runnable = createRunnable(bean, method); // createRunnable 方法内容在下面
boolean processedSchedule = false;
// 这个错误信息变量,往后的源码中会用到,可以从这里错误信息内容看出 @Scheduled 注解必须配置 cron、fixedDelay 和 fixedRate,
// 或者是后两者的 String 写法,fixedDelayString 和 fixedRateString。
String errorMessage =
		"Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required";

// ------------------------------------------------------------------------------------------------------

protected Runnable createRunnable(Object target, Method method) {
	// 断言方法 Assert.IsTrue():测试指定的条件是否为True,如果为True,则测试通过,
	// 若断言不会真时,程序会中止运行,并出现对应的错误信息。
	Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
	Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
	return new ScheduledMethodRunnable(target, invocableMethod);
}

@Scheduled 可配置三种类型的定时任务,分别对应了 cron、fixedDelay 和 fixedRate 属性,下面的代码是对三种类型的属性进行解析并且触发定时任务。

// cron 型定时任务
// Check cron expression
String cron = scheduled.cron();
if (StringUtils.hasText(cron)) {
	String zone = scheduled.zone();
	if (this.embeddedValueResolver != null) {
		cron = this.embeddedValueResolver.resolveStringValue(cron);
		zone = this.embeddedValueResolver.resolveStringValue(zone);
	}
	if (StringUtils.hasLength(cron)) {
		// cron 与 initialDelay 配置项是不能同时使用的
		Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers");
		processedSchedule = true;
		if (!Scheduled.CRON_DISABLED.equals(cron)) {
			TimeZone timeZone;
			if (StringUtils.hasText(zone)) {
				timeZone = StringUtils.parseTimeZoneString(zone);
			}
			else {
				timeZone = TimeZone.getDefault();
			}
			tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone))));
		}
	}
}

// At this point we don't need to differentiate between initial delay set or not anymore
if (initialDelay < 0) {
	initialDelay = 0;
}

// fixed delay 型定时任务
// Check fixed delay
long fixedDelay = scheduled.fixedDelay();
if (fixedDelay >= 0) {
	Assert.isTrue(!processedSchedule, errorMessage);
	processedSchedule = true;
	tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
}
String fixedDelayString = scheduled.fixedDelayString();
if (StringUtils.hasText(fixedDelayString)) {
	if (this.embeddedValueResolver != null) {
		fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString);
	}
	if (StringUtils.hasLength(fixedDelayString)) {
		Assert.isTrue(!processedSchedule, errorMessage);
		processedSchedule = true;
		try {
			fixedDelay = parseDelayAsLong(fixedDelayString);
		}
		catch (RuntimeException ex) {
			throw new IllegalArgumentException(
					"Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
		}
		tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
	}
}

// fixed rate 型定时任务
// Check fixed rate
long fixedRate = scheduled.fixedRate();
if (fixedRate >= 0) {
	Assert.isTrue(!processedSchedule, errorMessage);
	processedSchedule = true;
	tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
}
String fixedRateString = scheduled.fixedRateString();
if (StringUtils.hasText(fixedRateString)) {
	if (this.embeddedValueResolver != null) {
		fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString);
	}
	if (StringUtils.hasLength(fixedRateString)) {
		Assert.isTrue(!processedSchedule, errorMessage);
		processedSchedule = true;
		try {
			fixedRate = parseDelayAsLong(fixedRateString);
		}
		catch (RuntimeException ex) {
			throw new IllegalArgumentException(
					"Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into long");
		}
		tasks.add(this.registrar.scheduleFixedRateTask(new FixedRateTask(runnable, fixedRate, initialDelay)));
	}
}

以常用的 fixed delay 型定时任务为例:首先对 fixedDelay 属性进行解析和校验,然后将其封装成一个 FixedDelayTask 任务交给 registrar 注册中心去注册和执行,下面看下 ScheduledTaskRegistrar 的 scheduleFixedDelayTask 方法:

@Nullable
public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) {
	ScheduledTask scheduledTask = this.unresolvedTasks.remove(task);
	boolean newTask = false;
	// 判断是否是新注册的任务,并将任务保存起来
	if (scheduledTask == null) {
		scheduledTask = new ScheduledTask(task);
		newTask = true;
	}
	// 将任务交给内部代理的线程池去执行
	if (this.taskScheduler != null) {
		if (task.getInitialDelay() > 0) {
			Date startTime = new Date(System.currentTimeMillis() + task.getInitialDelay());
			scheduledTask.future =
					this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), startTime, task.getInterval());
		}
		else {
			scheduledTask.future =
					this.taskScheduler.scheduleWithFixedDelay(task.getRunnable(), task.getInterval());
		}
	}
	else {
		addFixedDelayTask(task);
		this.unresolvedTasks.put(task, scheduledTask);
	}
	return (newTask ? scheduledTask : null);
}

该方法主要就是判断下是否是新注册的任务,并将任务保存起来,然后将定时任务交给内部代理的线程池去执行,最后如果是新注册的任务就将其返回。

ScheduledTaskRegistrar 使用了不同的集合保存不同类型的任务。

@Nullable
private List<CronTask> cronTasks;

@Nullable
private List<IntervalTask> fixedRateTasks;

@Nullable
private List<IntervalTask> fixedDelayTasks;

其他类型的定时任务逻辑类似,这里不再重复叙述。

最后将所有的 ScheduledTask 注册到 scheduledTasks 中。

// Finally register the scheduled tasks
synchronized (this.scheduledTasks) {
	Set<ScheduledTask> regTasks = this.scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>(4));
	regTasks.addAll(tasks);
}

scheduledTasks 是一个 Map,保存了所有的 ScheduledTask 实例,key 是标记有 @Scheduled 注解的 bean,value 是该 bean 下所有的 ScheduledTask 集合。注册时使用了 synchronized 保证了并发安全。

private final Map<Object, Set<ScheduledTask>> scheduledTasks = new IdentityHashMap<>(16);

Spring定时任务框架的核心就是使用后处理器扫描所有@Scheduled注解并注册定时任务,然后通过代理的JDK 线程池执行任务调度,其逻辑并不复杂,但是很好地实现了定时任务的处理,仅通过注解就可以配置各种类型的定时任务,使用起来十分方便

总结
Spring 定时任务框架的核心就是使用后处理器扫描所有 @Scheduled 注解并注册定时任务,然后通过代理的 JDK 线程池执行任务调度,其逻辑并不复杂,但是很好地实现了定时任务的处理,仅通过注解就可以配置各种类型的定时任务,使用起来十分方便。

本文地址:https://blog.csdn.net/qq_46018521/article/details/112200050