SpringBoot:Spring Task定时任务完整使用教学

Spring Task 是 Spring 框架提供的轻量级定时任务工具,它无需依赖额外的第三方库(如 Quartz),直接集成在 Spring 核心包中。在 SpringBoot 中,Spring Task 提供了自动配置支持,只需少量注解即可快速实现定时任务功能。

一、Spring Task 概述

1.1 核心优势

  • 轻量级:无需额外依赖,Spring 核心包自带
  • 简单易用:基于注解配置,上手快
  • 集成度高:与 Spring 生态无缝集成
  • 功能完善:支持多种触发方式、异步执行、异常处理等

1.2 适用场景

  • 数据定时备份与清理
  • 定时发送邮件/短信通知
  • 定时统计报表生成
  • 系统状态定时监控
  • 定时同步第三方数据

二、案例:5分钟实现第一个定时任务

2.1 环境准备

创建一个标准的 SpringBoot 项目,无需添加额外依赖(SpringBoot 2.x 及以上版本已默认包含 spring-context 依赖)。

2.2 开启定时任务支持

在 SpringBoot 启动类上添加 @EnableScheduling 注解,开启定时任务自动配置:

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 开启定时任务支持
public class TaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(TaskApplication.class, args);
    }
}

2.3 创建定时任务类

创建一个普通的 Spring Bean,在需要定时执行的方法上添加 @Scheduled 注解:

java 复制代码
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
public class SimpleTask {
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    // 每隔5秒执行一次
    @Scheduled(fixedRate = 5000)
    public void printCurrentTime() {
        System.out.println("当前时间:" + LocalDateTime.now().format(formatter));
    }
}

2.4 运行测试

启动 SpringBoot 应用,控制台会每隔5秒输出一次当前时间,说明定时任务已成功运行。

三、核心注解详解

3.1 @EnableScheduling

  • 作用:开启 Spring 定时任务的自动配置
  • 位置:通常添加在启动类或配置类上
  • 原理 :该注解会导入 SchedulingConfiguration 配置类,自动注册 ScheduledAnnotationBeanPostProcessor,用于扫描带有 @Scheduled 注解的方法并创建定时任务

3.2 @Scheduled

@Scheduled 是最核心的注解,用于标记一个方法为定时任务方法。它提供了多种属性来配置任务的执行规则:

属性 类型 说明
cron(重点) String 使用 cron 表达式定义执行规则
fixedRate long 从上一次执行开始时间算起,间隔指定毫秒数执行
fixedDelay long 从上一次执行结束时间算起,间隔指定毫秒数执行
initialDelay long 首次执行延迟的毫秒数
zone String cron 表达式使用的时区,默认使用服务器本地时区

四、定时任务触发方式详解

4.1 fixedRate 固定频率执行

特点 :从上一次任务开始执行的时间点开始计算,间隔指定时间后执行下一次任务。

java 复制代码
// 每隔5秒执行一次,无论上一次任务执行了多久
@Scheduled(fixedRate = 5000)
public void fixedRateTask() {
    System.out.println("fixedRate任务执行:" + LocalDateTime.now());
}

注意 :如果任务执行时间超过了 fixedRate 设置的间隔时间,下一次任务会立即执行,不会等待。例如:任务执行需要8秒,fixedRate=5秒,那么任务会连续执行,没有间隔。

4.2 fixedDelay 固定延迟执行

特点 :从上一次任务执行结束的时间点开始计算,间隔指定时间后执行下一次任务。

java 复制代码
// 上一次任务执行结束后,延迟3秒执行下一次
@Scheduled(fixedDelay = 3000)
public void fixedDelayTask() {
    System.out.println("fixedDelay任务执行:" + LocalDateTime.now());
}

适用场景:任务执行时间不固定,需要确保两次任务之间有固定的间隔时间。

4.3 initialDelay 首次执行延迟

initialDelay 可以与 fixedRatefixedDelay 配合使用,指定应用启动后,首次执行任务的延迟时间。

java 复制代码
// 应用启动后延迟10秒,然后每隔5秒执行一次
@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void initialDelayTask() {
    System.out.println("initialDelay任务执行:" + LocalDateTime.now());
}

4.4 cron 表达式(最灵活)

cron 表达式是一种强大的时间表达式,可以精确到秒,定义复杂的执行规则。

cron 表达式结构

cron 表达式由6或7个字段组成,空格分隔:

复制代码
秒 分 时 日 月 周 [年]
  • 年字段是可选的,通常省略
  • 每个字段可以使用特殊字符表示不同的含义
常用特殊字符
  • *:表示该字段的所有可能值
  • ?:表示不指定值,用于日和周字段互斥
  • -:表示范围
  • ,:表示枚举多个值
  • /:表示步长
常用 cron 表达式示例
java 复制代码
// 每天凌晨0点执行
@Scheduled(cron = "0 0 0 * * ?")
public void dailyTask() {}

// 每天上午9点到下午6点,每隔半小时执行一次
@Scheduled(cron = "0 0/30 9-18 * * ?")
public void workHourTask() {}

// 每周一至周五的上午10点15分执行
@Scheduled(cron = "0 15 10 ? * MON-FRI")
public void weekdayTask() {}

// 每月1号凌晨1点执行
@Scheduled(cron = "0 0 1 1 * ?")
public void monthlyTask() {}

// 每10秒执行一次
@Scheduled(cron = "*/10 * * * * ?")
public void every10SecondsTask() {}
在线 cron 表达式生成工具

五、任务执行器(TaskExecutor)配置

5.1 默认执行器的问题

Spring Task 默认使用单线程的任务执行器。如果有多个定时任务,它们会排队执行,一个任务阻塞会导致其他任务延迟执行。

5.2 自定义线程池

创建一个配置类,实现 SchedulingConfigurer 接口,自定义任务执行器:

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        // 核心线程数
        scheduler.setPoolSize(10);
        // 线程名前缀
        scheduler.setThreadNamePrefix("task-scheduler-");
        // 等待任务完成后关闭
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        // 等待时间
        scheduler.setAwaitTerminationSeconds(60);
        // 初始化
        scheduler.initialize();
        
        taskRegistrar.setTaskScheduler(scheduler);
    }
}

5.3 配置参数说明

  • setPoolSize(int poolSize):设置线程池核心线程数,建议根据任务数量和执行时间调整
  • setThreadNamePrefix(String prefix):设置线程名前缀,方便日志排查
  • setWaitForTasksToCompleteOnShutdown(boolean):设置是否等待任务完成后关闭线程池
  • setAwaitTerminationSeconds(int seconds):设置等待任务完成的最长时间

六、异步定时任务

6.1 需求情况

即使配置了多线程的任务执行器,同一个任务的多次执行仍然是串行的。如果任务执行时间较长,会导致任务堆积。

使用异步定时任务可以让同一个任务的多次执行并行处理。

6.2 开启异步支持

在启动类或配置类上添加 @EnableAsync 注解:

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableAsync // 开启异步支持
public class TaskApplication {
    public static void main(String[] args) {
        SpringApplication.run(TaskApplication.class, args);
    }
}

6.3 创建异步定时任务

在定时任务方法上添加 @Async 注解:

java 复制代码
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class AsyncTask {

    @Async
    @Scheduled(fixedRate = 1000)
    public void asyncTask() throws InterruptedException {
        System.out.println("异步任务开始执行:" + Thread.currentThread().getName());
        // 模拟任务执行3秒
        Thread.sleep(3000);
        System.out.println("异步任务执行结束:" + Thread.currentThread().getName());
    }
}

运行后会发现,即使任务执行需要3秒,仍然会每隔1秒启动一个新的任务,每个任务在不同的线程中执行。

6.4 自定义异步线程池

默认的异步线程池可能无法满足生产环境需求,建议自定义异步线程池:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class AsyncConfig {

    @Bean("taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(20);
        // 队列容量
        executor.setQueueCapacity(200);
        // 线程存活时间
        executor.setKeepAliveSeconds(60);
        // 线程名前缀
        executor.setThreadNamePrefix("async-task-");
        // 拒绝策略:由调用线程执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

使用时指定线程池名称:

java 复制代码
@Async("taskExecutor")
@Scheduled(fixedRate = 1000)
public void asyncTask() {}

七、动态定时任务

7.1 为什么需要动态定时任务

使用 @Scheduled 注解配置的定时任务是静态的,一旦应用启动,执行规则就无法修改。

动态定时任务允许在应用运行时修改任务的执行时间、暂停、恢复和删除任务。

7.2 实现方式

通过 ScheduledTaskRegistrar 注册动态任务:

java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;

import java.util.concurrent.ScheduledFuture;

@Configuration
public class DynamicTaskConfig implements SchedulingConfigurer {

    private ScheduledTaskRegistrar taskRegistrar;
    private ScheduledFuture<?> future;

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        this.taskRegistrar = taskRegistrar;
    }

    // 添加任务
    public void addTask(Runnable task, String cron) {
        if (future != null) {
            future.cancel(true);
        }
        future = taskRegistrar.getScheduler().schedule(task, new CronTrigger(cron));
    }

    // 暂停任务
    public void stopTask() {
        if (future != null) {
            future.cancel(true);
        }
    }
}

7.3 动态修改任务执行时间

创建一个控制器,提供接口来动态修改任务执行时间:

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDateTime;

@RestController
public class DynamicTaskController {

    @Autowired
    private DynamicTaskConfig dynamicTaskConfig;

    @GetMapping("/updateTask")
    public String updateTask(@RequestParam String cron) {
        Runnable task = () -> System.out.println("动态任务执行:" + LocalDateTime.now());
        dynamicTaskConfig.addTask(task, cron);
        return "任务已更新,新的cron表达式:" + cron;
    }

    @GetMapping("/stopTask")
    public String stopTask() {
        dynamicTaskConfig.stopTask();
        return "任务已暂停";
    }
}

八、任务异常处理

8.1 默认异常处理

默认情况下,如果定时任务方法抛出异常,该任务会终止执行,不会影响其他任务。

8.2 全局异常处理

实现 ErrorHandler 接口,创建全局异常处理器:

java 复制代码
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ErrorHandler;

import java.lang.reflect.Method;

@Configuration
public class TaskExceptionConfig implements SchedulingConfigurer, AsyncConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10);
        scheduler.setThreadNamePrefix("task-scheduler-");
        // 设置异常处理器
        scheduler.setErrorHandler(new TaskErrorHandler());
        scheduler.initialize();
        taskRegistrar.setTaskScheduler(scheduler);
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncTaskErrorHandler();
    }

    // 同步任务异常处理器
    public static class TaskErrorHandler implements ErrorHandler {
        @Override
        public void handleError(Throwable t) {
            System.err.println("同步定时任务执行异常:" + t.getMessage());
            t.printStackTrace();
        }
    }

    // 异步任务异常处理器
    public static class AsyncTaskErrorHandler implements AsyncUncaughtExceptionHandler {
        @Override
        public void handleUncaughtException(Throwable ex, Method method, Object... params) {
            System.err.println("异步定时任务执行异常:" + method.getName());
            ex.printStackTrace();
        }
    }
}

8.3 方法级异常处理

在定时任务方法内部使用 try-catch 块处理异常:

java 复制代码
@Scheduled(fixedRate = 5000)
public void taskWithExceptionHandling() {
    try {
        // 业务逻辑
        int result = 1 / 0;
    } catch (Exception e) {
        System.err.println("任务执行异常:" + e.getMessage());
        // 记录日志、发送告警等
    }
}

九、任务并发与幂等性

9.1 并发问题

在以下场景中,定时任务可能会出现并发执行的问题:

  1. 使用异步定时任务
  2. 应用集群部署
  3. 任务执行时间超过执行间隔

9.2 幂等性设计

为了避免并发执行导致的数据问题,定时任务必须保证幂等性。常用的实现方式:

  1. 数据库唯一约束:在关键表上添加唯一索引
  2. 分布式锁:使用 Redis、Zookeeper 等实现分布式锁
  3. 状态机:记录任务执行状态,避免重复执行

9.3 Redis 分布式锁实现

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class IdempotentTask {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String LOCK_KEY = "task:lock:idempotent";
    private static final long LOCK_EXPIRE = 30; // 锁过期时间,秒

    @Scheduled(fixedRate = 5000)
    public void idempotentTask() {
        // 尝试获取锁
        Boolean locked = redisTemplate.opsForValue()
                .setIfAbsent(LOCK_KEY, "locked", LOCK_EXPIRE, TimeUnit.SECONDS);
        
        if (Boolean.TRUE.equals(locked)) {
            try {
                // 执行业务逻辑
                System.out.println("获取到锁,执行任务");
            } finally {
                // 释放锁
                redisTemplate.delete(LOCK_KEY);
            }
        } else {
            System.out.println("未获取到锁,跳过本次执行");
        }
    }
}

十、集群环境下的定时任务

10.1 集群环境的问题

在集群环境下,每个应用实例都会执行定时任务,导致任务重复执行。

10.2 解决方案

  1. 分布式锁:如上面的 Redis 分布式锁实现
  2. 任务调度中心:使用 XXL-Job、Elastic-Job 等专业的分布式任务调度框架
  3. 单独部署:将定时任务单独部署在一个实例上

10.3 推荐方案

对于简单的定时任务,使用 Redis 分布式锁即可满足需求。对于复杂的任务调度需求,建议使用专业的分布式任务调度框架,如 XXL-Job。

十一、常见问题

11.1 定时任务不执行

可能原因

  1. 忘记添加 @EnableScheduling 注解
  2. 定时任务类没有被 Spring 扫描到(没有添加 @Component 等注解)
  3. 方法不是 public 的
  4. 方法有参数
  5. 方法返回值不是 void

解决方案:检查以上几点,确保配置正确。

11.2 任务执行时间不准确

可能原因

  1. 使用了默认的单线程执行器,任务被阻塞
  2. 服务器时间不准确
  3. cron 表达式时区设置不正确

解决方案

  1. 配置多线程任务执行器
  2. 校准服务器时间
  3. @Scheduled 注解中指定时区:@Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Shanghai")

11.3 任务重复执行

可能原因

  1. 应用集群部署
  2. 任务执行时间超过执行间隔
  3. 异步任务没有控制并发

解决方案

  1. 使用分布式锁
  2. 调整任务执行间隔
  3. 控制异步任务的并发数

十二、总结

  1. 合理配置线程池:根据任务数量和执行时间调整线程池大小,避免任务阻塞
  2. 使用异步任务:对于执行时间较长的任务,使用异步执行
  3. 保证幂等性:所有定时任务都应该设计为幂等的
  4. 异常处理:添加全局异常处理和方法级异常处理,避免任务终止
  5. 日志记录:在任务执行前后记录详细日志,方便问题排查
  6. 监控告警:对任务执行情况进行监控,异常时及时告警
  7. 避免长任务:尽量将长任务拆分为多个短任务
  8. 集群环境使用分布式锁:避免任务重复执行
相关推荐
jiayong231 小时前
Tool Permission 与 Sandbox 的安全流水线:Agent 工具系统的工程边界
java·数据库·安全·agent
rururunu1 小时前
Windows 下切换 Java 环境太复杂了,我做了个 cli 工具,可以快速安装,切换 Java 版本
java
qq_452396231 小时前
第十一篇:《性能压测基础:JMeter线程模型与压测策略设计》
java·开发语言·jmeter
澈2071 小时前
二叉搜索树:高效增删查的秘诀
java·开发语言·算法
青云计划1 小时前
Spring
java·后端·spring
yychen_java2 小时前
深度解析电力交易系统的“硬核”战场
java·能源
无尽冬.2 小时前
个人八股之string字符串
java·开发语言·经验分享·后端·异世界
伯远医学2 小时前
Nat. Methods | 邻近标记技术:活细胞中捕捉分子互作的新利器
java·开发语言·前端·javascript·人工智能·算法·eclipse