目标

定时任务也叫任务调度,目前常见的任务框架有Quarts、Elastic-Job、xxl-job、PowerJob等。

本文主要介绍的是SpringBoot框架自带的任务调度功能(@EnableScheduling)的使用步骤。以及使用它时的注意事项。在多任务并行执行时有个配置必须要改,否则默认是单线程的。

适用场景

这个功能最大的优势就是SpringBoot自带的,比Quarts更轻量。不需要引入额外的依赖。

只需要几个注解就可以使用。适用于业务不是特别复杂的场景或者不需要从数据库来配置定时任务的场景。

环境说明

JDK1.8+

SpringBoot2.3+

功能启用

pom文件

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.15</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

	<dependencies>
        <!-- SpringBoot 拦截器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
		
    </dependencies>

 启动类

  1. 在SpringBoot启动类上加上@EnableScheduling注解。代码参考:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableAsync(proxyTargetClass = true)
@EnableScheduling
@ComponentScan(value = {"com.purvar"})
public class PlmToolsApplication {
    public static void main(String[] args) {
        SpringApplication.run(PlmToolsApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  plm-tools-启动成功!   ლ(´ڡ`ლ)゙  \n");
    }
}

使用说明

在定时任务方法上加上@Scheduled注解并指定cron表达式。代码参考:

@Service
public class ScheduleService{
    
    @Scheduled(cron = "* * * * * ?")
    public void task1() throws Exception {
        System.out.println("task1 start:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
        TimeUnit.SECONDS.sleep(5);
        System.out.println("task1 end:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
    }

    @Scheduled(cron = "* * * * * ?")
    public void task2() throws Exception {
        System.out.println("task2 start:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
    }
}

 到这里,定时任务已经配置好了。上面案例代码中的任务方法task1和task2任务方法会按配置定时执行了。是不是比quarts简单多了?但下面还有一些注意事项分享给大家。

多任务并行

由于在没有特殊配置的情况下,Spring默认定时任务的线程池是单线程的。我用上面的案例来说明单线程意味着什么。

案例中,task1和task2都配置成了每1秒执行1次,但是每次task1都会执行5秒。

我们一般期望的结果是:无论task1执行了多长时间都不应该影响task2的执行。task2应该是始终是每1秒执行1次的。

但由于Spring默认的定时任务线程池里只有1个线程,所以当task1执行的时候,task2也无法执行,要等task1执行成功了,task2才能执行。

执行效果见日志。

task1 start:2021-10-10 10:26:42
task1 end:2021-10-10 10:26:45
task2 start:2021-10-10 10:26:45(ps:3秒后才执行)
task1 start:2021-10-10 10:26:46
task1 end:2021-10-10 10:26:49
task2 start:2021-10-10 10:26:49
task1 start:2021-10-10 10:26:50
task1 end:2021-10-10 10:26:53
task2 start:2021-10-10 10:26:53

配置多任务并行执行有如下几种方式。

  1. 新建一个定时任务配置类,实现SchedulingConfigurer接口,加上@Configuration注解
  2. 在yml种配置线程池。

 任务配置类

实现SchedulingConfigurer接口,同时还可以在这个类里面统一管理系统中所有的定时任务。当定时任务有新增时,可以动态调整线程的数量。

定时任务的执行频率也可以配置在yml中,参考task3.

/**
 * 所有的定时任务都放这里,方便管理<br>
 * 如果想把所有定时任务全部关闭总开关 把@Configuration注释<br>
 * 如果想把某个定时任务全关闭 把对应方法上的@Scheduled注释<br>
 * @author namelessmyth
 * @since 2021-10-10
 */
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    /** 调用service的方法,不要在这里写业务逻辑*/
    @Autowired
    private FixService fixService;

    /**
     * 配置线程池,确保不同的任务方法使用不同的线程执行.<br>
     * Spring默认是单线程执行所有定时任务方法<br>
     * <font color="red">如果定时任务的总数量超过了线程池的大小,请记得及时调整</font><br>
     * <font color="blue">@EnableAsync和@Async用于使同一个任务方法变成多线程处理,使用前需要控制好并发问题</font><br>
     * @param taskRegistrar
     */
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors() * 2));
    }

    @Scheduled(cron = "* * * * * ?")
    public void task1() throws Exception {
        System.out.println("task1 start:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
        TimeUnit.SECONDS.sleep(3);
        System.out.println("task1 end:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
    }

    @Scheduled(cron = "* * * * * ?")
    public void task2() throws Exception {
        System.out.println("task2 start:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()));
    }

    @Scheduled(cron = "${aaa.bbb.key}")
    public void task3() throws Exception {
        fixService.fixImesFlag(null);
    }

}

修改之后的执行日志,可以看出task2已经不在等task1执行完了再执行了:

task1 start:2021-10-10 10:23:47
task2 start:2021-10-10 10:23:47
task2 start:2021-10-10 10:23:48
task2 start:2021-10-10 10:23:49
task2 start:2021-10-10 10:23:50
task1 end:2021-10-10 10:23:50
task1 start:2021-10-10 10:23:51
task2 start:2021-10-10 10:23:51
task2 start:2021-10-10 10:23:52
task2 start:2021-10-10 10:23:53
task1 end:2021-10-10 10:23:54
task2 start:2021-10-10 10:23:54
task2 start:2021-10-10 10:23:55
task1 start:2021-10-10 10:23:55
task2 start:2021-10-10 10:23:56

yml配置

# Spring配置
spring:
  # 定时任务
  task:
    scheduling:
      # 定时任务线程名称前缀
      thread-name-prefix: plm-task-
      # 线程池数量配置,默认为:1
      pool:
        size: 20

异步配置

配置步骤:

  1. 在@EnableScheduling注解下方加上@EnableAsync注解
  2. 在要异步执行的定时任务方法上加上@Async注解。

配置了异步之后,如果task1配置的是每隔1秒执行1次。那不管task1上一次执行有没有结束,Spring都会每隔1秒启动一个新线程来再次执行task1(直到超出线程池的最大配置数量)。这种暂时还没有想到使用场景,建议大家谨慎使用。