杰瑞科技汇

Spring Java定时任务怎么配置与执行?

  1. 核心注解 @Scheduled:最简单、最常用的方式,基于 Spring 的任务调度。
  2. 核心注解 @EnableScheduling:启用定时任务功能。
  3. 配置方式:如何配置线程池,防止任务阻塞。
  4. 各种 cron 表达式:如何定义任务的执行时间。
  5. 实践示例:一个完整的、可运行的代码示例。
  6. 进阶与替代方案:当 @Scheduled 不够用时,有哪些更好的选择。

核心注解:@Scheduled

这是实现定时任务的核心,你只需要在需要定时执行的方法上加上这个注解,Spring 就会负责在指定的时间调用这个方法。

Spring Java定时任务怎么配置与执行?-图1
(图片来源网络,侵删)

重要特性:

  • 该方法不能有返回值(返回值会被忽略),并且可以接受参数。
  • 默认情况下,它是单线程执行的,如果上一个任务的执行时间超过了任务间隔,那么下一个任务会等待上一个任务执行完毕后才开始,这可能会导致任务积压和延迟。
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component // 1. 必须是一个 Spring 管理的 Bean
public class MyScheduledTasks {
    // 2. 在方法上添加 @Scheduled 注解
    @Scheduled(fixedRate = 5000) // 每5秒执行一次
    public void taskWithFixedRate() {
        System.out.println("固定速率任务执行 - " + new Date());
        // 模拟一个耗时3秒的任务
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

启用定时任务:@EnableScheduling

仅仅使用 @Scheduled 是不够的,你还需要告诉 Spring 应用:“我这里需要启用定时任务功能”,这个注解就是用来做这件事的。

我们会在一个配置类上添加这个注解,在 Spring Boot 项目中,最常见的就是在启动类上添加。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 启用定时任务功能
public class MyApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }
}

配置方式:解决单线程问题

如前所述,@Scheduled 默认是单线程的,这在生产环境中是一个巨大的隐患,假设你的一个任务需要10秒执行完,而你设置的 fixedRate 是1秒,那么任务队列会很快被堆积,导致系统响应缓慢。

Spring Java定时任务怎么配置与执行?-图2
(图片来源网络,侵删)

解决方案:配置一个自定义的线程池。

我们需要创建一个 TaskScheduler 的 Bean 来替代默认的单线程执行器。

步骤:

  1. 创建一个配置类
  2. 定义一个线程池 ThreadPoolTaskScheduler
  3. 配置线程池参数(核心线程数、最大线程数、队列大小等)。
  4. 将这个 TaskScheduler 声明为 Bean
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.config.ScheduledTaskRegistrar;
import java.util.concurrent.Executors;
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        // 使用一个固定大小的线程池来执行定时任务
        // 这里我们创建一个拥有10个核心线程的线程池
        taskRegistrar.setScheduler(taskExecutor());
    }
    @Bean
    public TaskScheduler taskExecutor() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10); // 设置线程池大小
        scheduler.setThreadNamePrefix("scheduled-task-"); // 线程名前缀
        scheduler.setWaitForTasksToCompleteOnShutdown(true); // 等待任务在关闭时完成
        scheduler.setAwaitTerminationSeconds(60); // 等待时间
        return scheduler;
    }
}

配置好后,你的所有 @Scheduled 任务都会在这个线程池中并发执行,互不干扰。


cron 表达式详解

@Scheduled 注解提供了多种方式来定义任务执行时间,其中最强大、最灵活的就是 cron 表达式。

cron 表达式格式

一个标准的 cron 表达式由 6-7 个空格分隔的时间字段组成:

秒 分 时 日 月 周 [年]
字段 允许值 允许的特殊字符
0-59
0-59
0-23
1-31 L W
1-12JAN-DEC
1-7SUN-SAT L
1970-2099

特殊字符含义

  • (星号): 代表所有可能的值。 在“分”字段表示“每一分钟”。
  • (逗号): 列出枚举值。MON,WED,FRI 在“周”字段表示“周一、周三、周五”。
  • (连字符): 定义一个范围。10-12 在“时”字段表示“10点到12点”。
  • (斜杠): 定义一个步长。0/15 在“分”字段表示“从0分钟开始,每15分钟一次”。*/5 等同于 0/5
  • (问号): 只用于“日”和“周”字段,表示不指定值,当你需要指定其中一个,而另一个不关心时非常有用,想在每月的20号执行,但不管星期几,就可以写成 20 ? * * *
  • L (Last): 表示“,在“日”字段中,L 表示“一个月的最后一天”,在“周”字段中,L 表示“一周的最后一天(星期六)”,6LSAT 都表示“最后一个星期六”。
  • W (Weekday): 表示“最近的工作日”。15W 表示“离15号最近的工作日(周一到周五)”,如果15号是周六,则会在14号(周五)触发;如果15号是周日,则会在16号(周一)触发。
  • 只用于“周”字段,表示“第几个星期几”。6#3 表示“每个月的第3个星期五”(6=星期五,3=第3个)。

常用 cron 表达式示例

表达式 含义
0 * * * * * 每一秒执行
0 0/5 * * * * 每5分钟执行一次
0 0 0/1 * * * 每小时执行一次
0 0 12 * * ? 每天12:00执行
0 15 10 ? * * 每天10:15执行
0 15 10 * * ? 每天10:15执行 (与上一个等价)
0 15 10 * * ? 2025 在2025年的每天10:15执行
0 0 14 * * MON-FRI 每周一到周五的14:00执行
0 0 0 L * * 每月最后一天的24:00执行
0 0 0 1W * * 每月第一个工作日的24:00执行

完整实践示例 (Spring Boot)

下面是一个完整的 Spring Boot 项目示例,包含了线程池配置和多种定时任务。

pom.xml 确保引入了 spring-boot-starter-web,它会自动引入所需的依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

启动类 SchedulingApplication.java

package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 启用定时任务
public class SchedulingApplication {
    public static void main(String[] args) {
        SpringApplication.run(SchedulingApplication.class, args);
    }
}

线程池配置 SchedulingConfig.java

package com.example.demo.config;
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.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
@Configuration
public class SchedulingConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(taskExecutor());
    }
    @Bean
    public TaskScheduler taskExecutor() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5); // 线程池大小
        scheduler.setThreadNamePrefix("scheduled-task-");
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setAwaitTerminationSeconds(10);
        return scheduler;
    }
}

定时任务类 MyScheduledTasks.java

package com.example.demo.task;
import org.springframework.scheduled.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
@Component
public class MyScheduledTasks {
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    // 1. 固定速率:每次执行开始后,5秒后再次执行,如果任务耗时超过5秒,则会并发执行。
    @Scheduled(fixedRate = 5000)
    public void taskWithFixedRate() {
        System.out.println("固定速率任务执行于: " + dateFormat.format(new Date()));
    }
    // 2. 固定延迟:每次执行完成后,等待5秒,再执行下一次。
    @Scheduled(fixedDelay = 5000)
    public void taskWithFixedDelay() {
        System.out.println("固定延迟任务执行于: " + dateFormat.format(new Date()));
    }
    // 3. 初始延迟+固定速率:首次延迟2秒后开始,然后以5秒的固定速率执行。
    @Scheduled(initialDelay = 2000, fixedRate = 5000)
    public void taskWithInitialDelay() {
        System.out.println("初始延迟任务执行于: " + dateFormat.format(new Date()));
    }
    // 4. 使用 cron 表达式:每天上午10点15分执行
    @Scheduled(cron = "0 15 10 * * ?")
    public void taskWithCron() {
        System.out.println("Cron 任务 - 每天10:15执行于: " + dateFormat.format(new Date()));
    }
}

运行结果

当你启动这个应用时,你会在控制台看到类似下面的输出:

固定延迟任务执行于: 10:00:00
固定速率任务执行于: 10:00:00
初始延迟任务执行于: 10:00:02
固定速率任务执行于: 10:00:05
固定延迟任务执行于: 10:00:05
固定速率任务执行于: 10:00:10
固定延迟任务执行于: 10:00:10
...

你可以观察到,fixedRatefixedDelay 的执行模式是不同的,并且它们都在我们配置的线程池中并发执行。


进阶与替代方案

@Scheduled 非常简单,但它也有一些局限性:

  • 动态修改:修改 cron 表达式需要重启应用。
  • 分布式环境:在多个实例部署时,同一个任务会在所有实例上同时运行,导致任务重复执行。
  • 失败重试:没有内置的失败重试和告警机制。

当遇到这些情况时,可以考虑以下更强大的方案:

Spring Integration + Quartz

Quartz 是一个功能非常强大和成熟的任务调度框架,Spring 对它进行了很好的集成。

  • 优点
    • 功能强大,支持复杂的调度逻辑、集群、持久化。
    • 可以动态修改任务。
  • 缺点
    • 配置相对复杂。
    • 引入了额外的依赖。

分布式任务调度框架 (生产环境首选)

在微服务架构下,最推荐使用专业的分布式任务调度框架。

  • XXL-Job
    • 优点:轻量级、可视化调度中心、支持集群、故障转移、路由策略丰富、支持执行日志、邮件告警,是国内使用最广泛的框架之一。
    • 核心思想:有一个调度中心,负责向各个部署了任务的应用(执行器)下发任务,执行器接收任务并执行,这样就能保证同一个任务在同一时间只有一个实例在执行。
  • Elastic-Job
    • 优点:由当当网开源,基于 Zookeeper 实现分布式协调,支持分片(将一个大任务拆分成多个小任务并行执行)。
    • 核心思想:通过 Zookeeper 的分布式锁和节点选举来协调任务的执行。
方案 适用场景 优点 缺点
@Scheduled 简单的单机应用,任务量小,无分布式需求。 配置极其简单,开箱即用。 单线程,功能有限,无法动态修改,不适合分布式。
Quartz 功能要求较高的单机或小型应用。 功能强大,支持持久化和集群。 配置复杂,API 较重。
XXL-Job / Elastic-Job 微服务架构,分布式系统,需要高可用和高可靠性的场景。 真正的分布式调度,支持集群、分片、故障转移、监控告警。 需要额外部署调度中心,引入了外部依赖。

对于初学者和简单的个人项目,@Scheduled + 自定义线程池 是一个非常好的起点,对于任何生产级的、特别是分布式的系统,强烈推荐使用 XXL-Job 或 Elastic-Job 这样的专业框架。

分享:
扫描分享到社交APP
上一篇
下一篇