SpringBoot中定時(shí)任務(wù)@Scheduled注解的使用解讀
項(xiàng)目開發(fā)中,經(jīng)常會(huì)遇到定時(shí)任務(wù)的場(chǎng)景,Spring提供了@Scheduled注解,方便進(jìn)行定時(shí)任務(wù)的開發(fā)
概述
要使用@Scheduled注解,首先需要在啟動(dòng)類添加@EnableScheduling,啟用Spring的計(jì)劃任務(wù)執(zhí)行功能,這樣可以在容器中的任何Spring管理的bean上檢測(cè)@Scheduled注解,執(zhí)行計(jì)劃任務(wù)
注解定義
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {
String cron() default "";
String zone() default "";
long fixedDelay() default -1;
String fixedDelayString() default "";
long fixedRate() default -1;
String fixedRateString() default "";
long initialDelay() default -1;
String initialDelayString() default "";
}參數(shù)說明
| 參數(shù) | 參數(shù)說明 | 示例 |
|---|---|---|
| cron | 任務(wù)執(zhí)行的cron表達(dá)式 | 0/1 * * * * ? |
| zone | cron表達(dá)時(shí)解析使用的時(shí)區(qū),默認(rèn)為服務(wù)器的本地時(shí)區(qū),使用java.util.TimeZone#getTimeZone(String)方法解析 | GMT-8:00 |
| fixedDelay | 上一次任務(wù)執(zhí)行結(jié)束到下一次執(zhí)行開始的間隔時(shí)間,單位為ms | 1000 |
| fixedDelayString | 上一次任務(wù)執(zhí)行結(jié)束到下一次執(zhí)行開始的間隔時(shí)間,使用java.time.Duration#parse解析 | PT15M |
| fixedRate | 以固定間隔執(zhí)行任務(wù),即上一次任務(wù)執(zhí)行開始到下一次執(zhí)行開始的間隔時(shí)間,單位為ms,若在調(diào)度任務(wù)執(zhí)行時(shí),上一次任務(wù)還未執(zhí)行完畢,會(huì)加入worker隊(duì)列,等待上一次執(zhí)行完成后立即執(zhí)行下一次任務(wù) | 2000 |
| fixedRateString | 與fixedRate邏輯一致,只是使用java.time.Duration#parse解析 | PT15M |
| initialDelay | 首次任務(wù)執(zhí)行的延遲時(shí)間 | 1000 |
| initialDelayString | 首次任務(wù)執(zhí)行的延遲時(shí)間,使用java.time.Duration#parse解析 | PT15M |
源碼解析
配置了@Scheduled注解的方法,Spring的處理是通過注冊(cè)ScheduledAnnotationBeanPostProcessor來執(zhí)行,將不同配置參數(shù)的任務(wù)分配給不同的handler處理,核心代碼如下
org.springframework.scheduling.annotation.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;
}
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;
}
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#processScheduled
/**
* Process the given {@code @Scheduled} method declaration on the given bean.
* @param scheduled the @Scheduled annotation
* @param method the method that the annotation has been declared on
* @param bean the target bean instance
* @see #createRunnable(Object, Method)
*/
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());
}
}
org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
/**
* Schedule all registered tasks against the underlying
* {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}.
*/
proected void scheduleTasks() {
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
if (this.triggerTasks != null) {
for (TriggerTask task : this.triggerTasks) {
addScheduledTask(scheduleTriggerTask(task));
}
}
if (this.cronTasks != null) {
for (CronTask task : this.cronTasks) {
addScheduledTask(scheduleCronTask(task));
}
}
if (this.fixedRateTasks != null) {
for (IntervalTask task : this.fixedRateTasks) {
addScheduledTask(scheduleFixedRateTask(task));
}
}
if (this.fixedDelayTasks != null) {
for (IntervalTask task : this.fixedDelayTasks) {
addScheduledTask(scheduleFixedDelayTask(task));
}
}
}
使用詳解
定時(shí)任務(wù)同步/異步執(zhí)行
定時(shí)任務(wù)執(zhí)行默認(rèn)是單線程模式,會(huì)創(chuàng)建一個(gè)本地線程池,線程池大小為1。當(dāng)項(xiàng)目中有多個(gè)定時(shí)任務(wù)時(shí),任務(wù)之間會(huì)相互等待,同步執(zhí)行
源碼:
// org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
// java.util.concurrent.Executors#newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}代碼示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(cron = "0/1 * * * * ?")
public void singleThreadTest1() {
log.info("singleThreadTest1");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
}
@Scheduled(cron = "0/1 * * * * ?")
public void singleThreadTest2() {
log.info("singleThreadTest2");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
}
@Scheduled(cron = "0/1 * * * * ?")
public void singleThreadTest3() {
log.info("singleThreadTest3");
LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(1));
}
}執(zhí)行結(jié)果:

可以看到,默認(rèn)情況下,三個(gè)任務(wù)串行執(zhí)行,都使用pool-1-thread-1同一個(gè)線程池,并且線程只有一個(gè)
可以通過實(shí)現(xiàn)SchedulingConfigurer接口,手動(dòng)創(chuàng)建線程池,配置期望的線程數(shù)量
示例代碼:
@Configuration
public class ScheduledConfig implements SchedulingConfigurer {
/**
* 任務(wù)執(zhí)行線程池大小
*/
private static final int TASK_POOL_SIZE = 50;
/**
* 線程名
*/
private static final String TASK_THREAD_PREFIX = "test-task-";
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
ThreadPoolTaskScheduler taskPool = new ThreadPoolTaskScheduler();
taskPool.setPoolSize(TASK_POOL_SIZE);
taskPool.setThreadNamePrefix(TASK_THREAD_PREFIX);
taskPool.initialize();
scheduledTaskRegistrar.setTaskScheduler(taskPool);
}
}任務(wù)執(zhí)行結(jié)果:

此時(shí)任務(wù)的執(zhí)行已經(jīng)異步化,從自定義線程池中分配線程執(zhí)行任務(wù),在實(shí)際應(yīng)用中需要考慮實(shí)際任務(wù)數(shù)量,創(chuàng)建相應(yīng)大小的線程池
fixedRate/fixedDelay區(qū)別
fixedRate是配置上一次任務(wù)執(zhí)行開始到下一次執(zhí)行開始的間隔時(shí)間,不會(huì)等待上一次任務(wù)執(zhí)行完成就會(huì)調(diào)度下一次任務(wù),將其放入等待隊(duì)列中
代碼示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(initialDelay = 1000, fixedRate = 1000)
public void fixedRate() throws Exception {
log.info("fixedRate run");
TimeUnit.SECONDS.sleep(3);
}
}執(zhí)行結(jié)果:

任務(wù)配置的fixedRate為1s,執(zhí)行日志打印的時(shí)間間隔都是3s左右,也就是上一次執(zhí)行完成后,緊接著就執(zhí)行下一次任務(wù)
fixedDelay是配置的上一次任務(wù)執(zhí)行結(jié)束到下一次執(zhí)行開始的間隔時(shí)間,也就是說會(huì)等待上一次任務(wù)執(zhí)行結(jié)束后,延遲間隔時(shí)間,再執(zhí)行下一次任務(wù)
代碼示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(initialDelay = 1000, fixedDelay = 1000)
public void fixedDelay() throws Exception {
log.info("fixedDelay run");
TimeUnit.SECONDS.sleep(3);
}
}執(zhí)行結(jié)果:

任務(wù)配置的fixedDelay為1s,執(zhí)行日志打印的時(shí)間間隔都是4s左右,也就是上一次執(zhí)行完成后,延遲1s后執(zhí)行下一次任務(wù)
cron表達(dá)式如果配置為類似每秒執(zhí)行、每分鐘執(zhí)行(例:0/1 * * * * ?, 每秒執(zhí)行),調(diào)度跟fixedDelay是一致的,也是在上一次任務(wù)執(zhí)行結(jié)束后,等待間隔時(shí)間
代碼示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(cron = "0/1 * * * * ?")
public void cronRun() throws Exception{
log.info("cron run");
TimeUnit.SECONDS.sleep(3);
}
}執(zhí)行結(jié)果:

執(zhí)行日志打印的時(shí)間間隔都是4s左右,也就是上一次執(zhí)行完成后,延遲1s后執(zhí)行下一次任務(wù)
cron表達(dá)式如果配置為固定時(shí)間執(zhí)行(例:1 * * * * ?, 秒數(shù)為1時(shí)執(zhí)行),若上一次任務(wù)沒有執(zhí)行完,則不會(huì)調(diào)度本次任務(wù),跳過本次執(zhí)行,等待下一次執(zhí)行周期
代碼示例:
@Slf4j
@Component
public class RunIntervalTestScheduler {
@Scheduled(cron = "1 * * * * ?")
public void cronRun() throws Exception{
log.info("cron run");
TimeUnit.SECONDS.sleep(70);
}
}執(zhí)行結(jié)果:

上一次任務(wù)未執(zhí)行完畢,則跳過了本次執(zhí)行
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
- Spring自帶定時(shí)任務(wù)@Scheduled注解實(shí)例講解
- Spring定時(shí)任務(wù)@scheduled多線程使用@Async注解示例
- Spring定時(shí)任務(wù)@Scheduled注解(cron表達(dá)式fixedRate?fixedDelay)
- Spring中的@Scheduled定時(shí)任務(wù)注解詳解
- SpringBoot中@Scheduled()注解以及cron表達(dá)式詳解
- Spring 定時(shí)任務(wù)@Scheduled 注解中的 Cron 表達(dá)式詳解
- spring-boot通過@Scheduled配置定時(shí)任務(wù)及定時(shí)任務(wù)@Scheduled注解的方法
- 詳解在Spring3中使用注解(@Scheduled)創(chuàng)建計(jì)劃任務(wù)
- spring @Scheduled定時(shí)任務(wù)注解使用方法及注意事項(xiàng)小結(jié)
相關(guān)文章
java實(shí)現(xiàn)刪除某條信息并刷新當(dāng)前頁操作
這篇文章主要介紹了java實(shí)現(xiàn)刪除某條信息并刷新當(dāng)前頁操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-11-11
解決get請(qǐng)求入?yún)NotNull驗(yàn)證不生效問題
這篇文章主要介紹了解決get請(qǐng)求入?yún)NotNull驗(yàn)證不生效問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
詳解Spring AOP 實(shí)現(xiàn)“切面式”valid校驗(yàn)
本篇文章主要介紹了詳解Spring AOP 實(shí)現(xiàn)“切面式”valid校驗(yàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01
Spring?IOC容器基于XML外部屬性文件的Bean管理
這篇文章主要為大家介紹了Spring?IOC容器Bean管理XML外部屬性文件,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
簡(jiǎn)單的理解java集合中的HashSet和HashTree幾個(gè)重寫方法
這篇文章主要介紹了簡(jiǎn)單的理解java集合中的HashSet和HashTree幾個(gè)重寫方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10

