# SpringBoot 定时任务

## 基于注解，静态执行周期 <a href="#slide-1" id="slide-1"></a>

Spring Task 是 Spring 提供的轻量级定时任务工具，相比于其他第三方类库更加方便易用。

1. 新建配置类，添加`@EnableScheduling`注解开启SpringTask。

```java
@Configuration
@EnableScheduling
public class SpringTaskConfiguration {
}
```

2. 创建定时任务类，使用`@Scheduled`注解注册`Cron`表达式执行定时任务。

```java
@Slf4j
@Component
public class CronTask {
    @Scheduled(cron = "0/1 * * ? * ?")
    public void cron() {
        log.info("cron:{}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}
```

<figure><img src="https://591911669-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FipKeL5OCbHsvZx1DHH4B%2Fuploads%2FvXDyVCOVZvc99omeunlb%2Fimage.png?alt=media&#x26;token=24b4af60-5b3b-406c-83d6-a368534c8906" alt=""><figcaption></figcaption></figure>

默认情况下，`@Scheduled`创建的线程池大小为1，如果想增加线程池大小的话，可以让`SpringTaskConfiguration`类实现`SchedulingConfigurer`接口，通过`setPoolSize`增加线程池大小。

```java
@Configuration
@EnableScheduling
public class SpringTaskConfiguration implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        var threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(10);  // 设置线程池线程数量
        threadPoolTaskScheduler.setThreadNamePrefix("task-");  // 设置线程名称前缀
        threadPoolTaskScheduler.initialize();  // 初始化线程池
        taskRegistrar.setScheduler(threadPoolTaskScheduler);
    }
}
```

<figure><img src="https://591911669-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FipKeL5OCbHsvZx1DHH4B%2Fuploads%2FWTgcWsUlxUDfqBmGrHwk%2Fimage.png?alt=media&#x26;token=d524a6b7-706c-452c-804b-094ecdc917e3" alt=""><figcaption></figcaption></figure>

Spring Task 除了支持Cron表达式，还有fixedRate（固定速率执行）、fixedDelay（固定延迟执行）、initialDelay（初始延迟）三种用法。

{% tabs %}
{% tab title="fixedRate" %}

```java
// 固定速率执行。每5秒执行一次。
@Scheduled(fixedRate = 5000)
public void reportCurrentTimeWithFixedRate() {
    log.info("Current Thread : {}", Thread.currentThread().getName());
    log.info("Fixed Rate Task : The time is now {}", DateUtil.now());
}

```

{% endtab %}

{% tab title="fixedDelay" %}

```java
// 固定延迟执行。距离上一次调用成功后2秒才执。
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
    try {
        TimeUnit.SECONDS.sleep(3);
        log.info("Fixed Delay Task : The time is now {}",DateUtil.now());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
```

{% endtab %}

{% tab title="initialDelay" %}

```java
// 初始延迟。任务的第一次执行将延迟5秒，然后将以5秒的固定间隔执行。
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public void reportCurrentTimeWithInitialDelay() {
    log.info("Fixed Rate Task with Initial Delay : The time is now {}", DateUtil.now());
}
```

{% endtab %}
{% endtabs %}

## 动态设置执行周期（基于接口）

使用`@Scheduled`注解很方便，但缺点是当调整了执行周期后，需要重新打包部署才能生效，为了省去这一步，我们可以将执行周期存在数据库或者相关的配置文件中，服务可以从数据库或者配置中读取。

```java
@Configuration
@EnableScheduling
public class SpringTaskConfiguration implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.addTriggerTask(() -> {
            // 任务逻辑
            System.out.println("执行定时任务");
        }, triggerContext -> {
            // 此处的cron表达式可以从数据库或配置文件中读取，方便实现定时任务的动态配置
            String cron = "0/5 * * * * ?";
            CronTrigger cronTrigger = new CronTrigger(cron);
            return cronTrigger.nextExecution(triggerContext);
        });
    }
}
```

## 多线程定时任务

```java
@Component
@EnableAsync
public class CornTask {
    @Async
    @Scheduled(fixedDelay = 1000)  //间隔1秒
    public void first() throws InterruptedException {
        System.out.println("第一个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
        System.out.println();
        Thread.sleep(1000 * 10);
    }

    @Async
    @Scheduled(fixedDelay = 2000)
    public void second() {
        System.out.println("第二个定时任务开始 : " + LocalDateTime.now().toLocalTime() + "\r\n线程 : " + Thread.currentThread().getName());
        System.out.println();
    }
}
```

## Corn 表达式

Cron表达式是一个含有时间意义的字符串，以5个空格隔开，分成6个时间元素。例如：

| 示例                   | 说明                     |
| -------------------- | ---------------------- |
| `0 15 10 ？ * *`      | 每天上午10:15执行任务          |
| `0 0 10,14,16 * * ?` | 每天10 点、14 点、16 点执行任务   |
| `0 0 12 ? * 3`       | 每个星期三中午 12 点执行任务       |
| `0 15 10 15 * ?`     | 每月 15 日上午 10 点 15 执行任务 |

每个时间元素的取值范围，以及可出现的特殊字符如下所示。

| 时间元素 | 取值范围     | 可出现的特殊字符  |
| ---- | -------- | --------- |
| 秒    | `[0,59]` | `*,-/`    |
| 分钟   | `[0,59]` | `*,-/`    |
| 小时   | `[0,59]` | `*,-/`    |
| 日期   | `[0,31]` | `*,-/?LW` |
| 月份   | `[1,12]` | `*,-/`    |
| 星期   | `[1,7]`  | `*,-/?L#` |

特殊字符的含义和示例如下所示。

<table><thead><tr><th width="107">特殊字符</th><th width="203">含义</th><th>示例</th></tr></thead><tbody><tr><td><code>*</code></td><td>所有可能的值</td><td>很好理解，月域中为每个月，星期域中每个星期几</td></tr><tr><td><code>,</code></td><td>枚举的值</td><td>很好理解，小时域中 <code>10,14,16</code>，就表示这几个小时可选</td></tr><tr><td><code>-</code></td><td>范围</td><td>很好理解，分钟域中 <code>10-19</code>，就表示 10-19 分钟每隔一分钟执行一次</td></tr><tr><td><code>/</code></td><td>指定数值的增量</td><td>很好理解，分钟域中 <code>0/15</code>，就表示每隔 15 分钟执行一次</td></tr><tr><td><code>?</code></td><td>不指定值</td><td>很好理解，日期域指定了星期域就不能指定值，反之亦然，因为日期域和星期域属于冲突关系</td></tr><tr><td><code>L</code></td><td>单词 Last 的首字母</td><td>很好理解，日期域和星期域支持，表示月的最后一天或者星期的最后一天</td></tr><tr><td><code>W</code></td><td>除周末以外的工作日</td><td>很好理解，仅日期域支持</td></tr><tr><td><code>#</code></td><td>每个月的第几个星期几</td><td>很好理解，仅星期域支持，<code>4#2</code>表示某月的第二个星期四</td></tr></tbody></table>
