Springboot之一文带你搞懂Scheduler定时器

在开发程序中总是避免不了一些周期性定时任务,比如定时同步数据库、定时发送邮件、定时初始化数据等等。

那么在springboot中如何使用定时任务呢? 本文将由浅入深逐渐揭开定时任务的“神秘面纱”。

本文知识点:

springboot中如何使用Scheduler

springboot中如何动态修改Scheduler的执行时间(cron)

springboot中如何实现多线程并行任务

想了解springboot中的Scheduler?看懂本文妥妥的了~!

如何使用Scheduler?

使用@EnableScheduling启用定时任务

使用@Scheduled编写相关定时任务

开启定时任务

在程序中添加@EnableScheduling注解即可启用Spring的定时任务功能,这类似于Spring的XML中的功能。

@SpringBootApplication

@EnableScheduling

public class ScheduleApplaction {

    public static void main(String[] args) throws Exception {

        SpringApplication.run(ScheduleApplaction.class, args);

    }

}

关于@Scheduled

通过查看Scheduled注解类的源码可知该注解支持如下几种方式使用定时任务

cron()

fixedDelay()

fixedDelayString()

fixedRate()

fixedRateString()

initialDelay()

initialDelayString()

注:由于xx和xxString方法只是参数类型不同用法一致,所以本文对xxString表达式方法不做解释。感兴趣的同学可以去查阅下ScheduledAnnotationBeanPostProcessor类的processScheduled方法。

cron

这个也是定时任务中最常用的一种调度方式,主要在于它的功能强大,使用方便。

支持cron语法,与linux中的crontab表达式类似,不过Java中的cron支持到了秒。

表达式规则如下:

{秒} {分} {时} {日} {月} {周} {年(可选)}

这儿有很多朋友记不住,或者和crontab的表达式记混。

Tips:linux的crontab表达式为:{分} {时} {日} {月} {周}

其实这儿也没有很好的记忆方式,采用最简单的方式:死记硬背即可(( ╯╰ ))。

cron各选项的取值范围及解释:

项取值范围null备注

秒0-59否参考①

分0-59否参考①

时0-23否参考①

日1-31否参考②

月1-12或JAN-DEC否参考①

周1-7或SUN-SAT否参考③

年1970-2099是参考①

参考①

“*” 每隔1单位时间触发;

"," 在指定单位时间触发,比如"10,20"代表10单位时间、20单位时间时触发任务

"-" 在指定的范围内触发,比如"5-30"代表从5单位时间到30单位时间时每隔1单位时间触发一次

"/" 触发步进(step),"/“前面的值代表初始值(”“等同"0”),后面的值代表偏移量,比如"0/25"或者"/25"代表从0单位时间开始,每隔25单位时间触发1次;"10-45/20"代表在[10-45]单位时间内每隔20单位时间触发一次

参考②

“* , - /” 这四个用法同①

"?" 与{周}互斥,即意味着若明确指定{周}触发,则表示{日}无意义,以免引起 冲突和混乱;

“L” 如果{日}占位符如果是"L",即意味着当月的最后一天触发

"W "意味着在本月内离当天最近的工作日触发,所谓最近工作日,即当天到工作日的前后最短距离,如果当天即为工作日,则距离为0;所谓本月内的说法,就是不能跨月取到最近工作日,即使前/后月份的最后一天/第一天确实满足最近工作日;因此,"LW"则意味着本月的最后一个工作日触发,"W"强烈依赖{月}

“C” 根据日历触发,由于使用较少,暂时不做解释

参考③

“* , - /” 这四个用法同①

"?" 与{日}互斥,即意味着若明确指定{日}触发,则表示{周}无意义,以免引起冲突和混乱

"L" 如果{周}占位符如果是"L",即意味着星期的的最后一天触发,即星期六触发,L= 7或者 L = SAT,因此,“5L"意味着一个月的最后一个星期四触发

”#" 用来指定具体的周数,"#“前面代表星期,”#"后面代表本月第几周,比如"2#2"表示本月第二周的星期一,"5#3"表示本月第三周的星期四,因此,“5L"这种形式只不过是”#“的特殊形式而已

"C” 根据日历触发,由于使用较少,暂时不做解释

其他关于cron的详细介绍请参考Spring Task 中cron表达式整理记录

/**

* 在11月7号晚上22点的7分到8分之间每隔半分钟(30秒)执行一次任务

*

* @author zhangyd

*/

@Scheduled(cron = "0/30 7-8 22 7 11 ? ")

public void doJobByCron() throws InterruptedException {

int index = integer.incrementAndGet();

System.out.println(String.format("[%s] %s doJobByCron start @ %s", index, Thread.currentThread(), LocalTime.now()));

// 这儿随机睡几秒,方便查看执行效果

TimeUnit.SECONDS.sleep(new Random().nextInt(5));

System.out.println(String.format("[%s] %s doJobByCron end  @ %s", index, Thread.currentThread(), LocalTime.now()));

}

查看打印的结果

[1] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:00.004

[1] Thread[pool-1-thread-1,5,main] doJobByCron end  @ 22:07:01.004

[2] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:07:30.001

[2] Thread[pool-1-thread-1,5,main] doJobByCron end  @ 22:07:30.001

[3] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:00.001

[3] Thread[pool-1-thread-1,5,main] doJobByCron end  @ 22:08:01.002

[4] Thread[pool-1-thread-1,5,main] doJobByCron start @ 22:08:30.002

[4] Thread[pool-1-thread-1,5,main] doJobByCron end  @ 22:08:33.002

给大家安利一款在线生成Cron表达式的神器:http://cron.qqe2.com。使用简单,一看就会。

fixedDelay

上一次任务执行完成后,延时固定长度时间执行下一次任务。关键词:上一次任务执行完成后

就如官方文档中所说:在最后一次调用结束到下一次调用开始之间以毫秒为单位进行等待,等待完成后执行下一次任务

Execute the annotated method with a fixed period in milliseconds

between the end of the last invocation and the start of the next.

通过代码可以更好的理解

private AtomicInteger integer = new AtomicInteger(0);

/**

* 上次任务执行完的3秒后再次执行

*

* @author zhangyd

*/

@Scheduled(fixedDelay = 3000)

public void doJobByFixedDelay() throws InterruptedException {

int index = integer.incrementAndGet();

System.out.println(String.format("[%s] %s doJobByFixedDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));

// 这儿随机睡几秒,方便查看执行效果

TimeUnit.SECONDS.sleep(new Random().nextInt(10));

System.out.println(String.format("[%s] %s doJobByFixedDelay end  @ %s", index, Thread.currentThread(), LocalTime.now()));

}

查看控制台输出

[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:03.235

[1] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end  @ 18:28:06.236

[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:09.238

[2] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end  @ 18:28:10.238

[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay start @ 18:28:13.240

[3] Thread[pool-1-thread-1,5,main] doJobByFixedDelay end  @ 18:28:21.240

可以看到不管每个任务实际需要执行多长时间,该任务再次执行的时机永远都是在上一次任务执行完后,延时固定长度时间后执行下一次任务。

fixedRate

官方文档中已经很明白的说明了此方法的用法。

Execute the annotated method with a fixed period in milliseconds between invocations.

按照固定的速率执行任务,无论之前的任务是否执行完毕。关键词:不管前面的任务是否执行完毕

private AtomicInteger integer = new AtomicInteger(0);

/**

* 固定每3秒执行一次

*

* @author zhangyd

*/

@Scheduled(fixedRate = 3000)

public void doJobByFixedRate() throws InterruptedException {

int index = integer.incrementAndGet();

System.out.println(String.format("[%s] %s doJobByFixedRate start @ %s", index, Thread.currentThread(), LocalTime.now()));

// 这儿随机睡几秒,方便查看执行效果

TimeUnit.SECONDS.sleep(new Random().nextInt(10));

System.out.println(String.format("[%s] %s doJobByFixedRate end  @ %s", index, Thread.currentThread(), LocalTime.now()));

}

查看控制台打印的结果

[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:23.520

[1] Thread[pool-1-thread-1,5,main] doJobByFixedRate end  @ 18:57:24.521

[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:26.515

[2] Thread[pool-1-thread-1,5,main] doJobByFixedRate end  @ 18:57:31.516

[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:31.516

[3] Thread[pool-1-thread-1,5,main] doJobByFixedRate end  @ 18:57:31.516

[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:32.515

[4] Thread[pool-1-thread-1,5,main] doJobByFixedRate end  @ 18:57:39.516

[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:39.516

[5] Thread[pool-1-thread-1,5,main] doJobByFixedRate end  @ 18:57:40.517

[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:40.517

[6] Thread[pool-1-thread-1,5,main] doJobByFixedRate end  @ 18:57:41.517

[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate start @ 18:57:41.517

[7] Thread[pool-1-thread-1,5,main] doJobByFixedRate end  @ 18:57:47.519

这儿需要重点关注每个任务的起始时间下一次任务的起始时间。如果说我们不看这个结果,直接就代码来说,我们会认为,程序每隔3秒执行一次,第二次任务和第一次任务的间隔时间是3秒,没毛病。但是我们通过上面打印的日志发现,程序并不是按照我们预想的流程执行的。

注:为方便阅读,任务命名为T+任务的编号,比如T1表示第一个任务,以此类推。

从上面结果看,T1起始时间到T2起始时间之间间隔了3秒,T2起始时间到T3起始时间间隔了5秒,T3起始时间到T4起始时间间隔了1秒。

为什么会有这种现象呢?这正如上面所说:“按照固定的延迟时间执行任务,无论之前的任务是否执行完毕

那么按照这种规则,咱们预先估计下每次任务的执行时间就能解释上面的问题。T1起始时间是23秒,T2起始时间是26,以此类推,T3理论上是29,T4是32,T5是35,T6是38,T7是41…

这种机制也可以称为“任务编排”,也就是说从fixedRate任务开始的那一刻,它后续的任务执行的时间已经被预先编排好了。如果定时任务执行的时间大于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后立即执行;如果定时任务执行的时间小于fixedRate指定的延迟时间,则定时任务会在上一个任务结束后等到预编排的时间时执行。

initialDelay

该条指令主要是用于配合fixedRate和fixedDelay使用的,作用是在fixedRate或fixedDelay任务第一次执行之前要延迟的毫秒数,说白了:它的作用就是在程序启动后,并不会立即执行该定时任务,它将在延迟一段时间后才会执行该定时任务

Number of milliseconds to delay before the first execution of a fixedRate() or fixedDelay() task.

本例为了方便测试,需要首先知道这个定时任务的bean在什么时候装载完成,或者说这个定时任务的bean什么时候初始化完成,所以,需要处理一下代码。

@Component

public class AppSchedulingConfigurer implements ApplicationListener {

    private AtomicInteger integer = new AtomicInteger(0);

    /**

    * 第一次延迟5秒后执行,之后按fixedRate的规则每3秒执行一次

    *

    * @author zhangyd

    */

    @Scheduled(initialDelay = 5000, fixedRate = 3000)

    public void doJobByInitialDelay() throws InterruptedException {

        int index = integer.incrementAndGet();

        System.out.println(String.format("[%s] %s doJobByInitialDelay start @ %s", index, Thread.currentThread(), LocalTime.now()));

        // 这儿随机睡几秒,方便查看执行效果

        TimeUnit.SECONDS.sleep(new Random().nextInt(5));

        System.out.println(String.format("[%s] %s doJobByInitialDelay end  @ %s", index, Thread.currentThread(), LocalTime.now()));

    }

    /**

    * Spring容器初始化完成

    */

    @Override

    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {

        System.out.println(String.format("springboot上下文context准备完毕时. %s start @ %s", Thread.currentThread(), LocalTime.now()));

    }

}

如上,使用Spring的事件监听应用启动的时间,以此打印出initialDelay应该从什么时候开始等待。

(关于Spring和Springboot的事件,本文不做深入介绍,后期会专文讲解这一块内容)

查看打印结果

springboot上下文context准备完毕时. Thread[main,5,main] start @ 21:18:42.092

[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:47.023

[1] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end  @ 21:18:51.024

[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:51.024

[2] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end  @ 21:18:53.025

[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:53.025

[3] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end  @ 21:18:57.025

[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:57.025

[4] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end  @ 21:18:57.025

[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay start @ 21:18:59.025

[5] Thread[pool-1-thread-1,5,main] doJobByInitialDelay end  @ 21:19:00.025

由此可以明显看出,initialDelay确实是在bean上下文准备完毕(容器已初始化完成)时延迟了5秒钟后执行的fixedRate任务。

如何动态修改执行时间(cron)?

实现动态配置cron的好处就是可以随时修改任务的执行表达式而不用重启服务。

想实现这种功能,我们只需要实现SchedulingConfigurer接口重写configureTasks接口即可。

@Component

public class DynamicScheduledConfigurer implements SchedulingConfigurer {

    // 默认每秒执行一次定时任务

    private String cron = "0/1 * * * * ?";

    private AtomicInteger integer = new AtomicInteger(0);

    @Override

    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

        taskRegistrar.addTriggerTask(() -> {

            int index = integer.incrementAndGet();

            System.out.println(String.format("[%s] %s 动态定时任务 start @ %s", index, Thread.currentThread(), LocalTime.now()));

        }, triggerContext -> {

            CronTrigger trigger = new CronTrigger(this.getCron());

            return trigger.nextExecutionTime(triggerContext);

        });

    }

    public String getCron() {

        return cron;

    }

    public void setCron(String cron) {

        System.out.println(String.format("%s Cron已修改!修改前:%s,修改后:%s @ %s", Thread.currentThread(), this.cron, cron, LocalTime.now()));

        this.cron = cron;

    }

}

例子中默认的cron表示每秒执行一次定时任务,我们要做的就是动态修改这个执行时间。

接下来编写一个controller负责处理cron以及预编排5组该cron将要执行的时间节点

@SpringBootApplication

@Controller

@EnableScheduling

public class ScheduleApplaction {

    @Autowired

    DynamicScheduledConfigurer dynamicScheduledConfigurer;

    public static void main(String[] args) {

        SpringApplication.run(ScheduleApplaction.class, args);

    }

    @RequestMapping("/")

    public String index() {

        return "index";

    }

    /**

    * 修改动态定时任务的cron值

    */

    @RequestMapping("/updateTask")

    @ResponseBody

    public void updateTask(String cron) {

        dynamicScheduledConfigurer.setCron(cron);

    }

    /**

    * 预解析5次该cron将要执行的时间节点

    *

    * @param cron 带解析的cron

    * @return

    * @throws IOException

    */

    @RequestMapping("/parseCron")

    @ResponseBody

    public List parseCron(String cron) throws IOException {

        String urlNameString = "http://cron.qqe2.com/CalcRunTime.ashx?CronExpression=" + URLEncoder.encode(cron, "UTF-8");

        URL realUrl = new URL(urlNameString);

        URLConnection connection = realUrl.openConnection();

        connection.setRequestProperty("accept", "*/*");

        connection.setRequestProperty("connection", "Keep-Alive");

        connection.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36");

        connection.connect();

        StringBuilder result = new StringBuilder();

        try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {

            String line;

            while ((line = in.readLine()) != null) {

                result.append(line);

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

        return JSONArray.parseArray(result.toString(), String.class);

    }

}

html页面

   

    动态修改cron

   

   

   

       

最近5次运行的时间

   

启动项目后先查看控制台输出,默认应该是每秒执行一次定时任务。

[1] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:23.010

[2] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:24.001

[3] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:25.001

[4] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:00:26.002

接下来修改一下默认cron,如下图,将定时任务修改为每5秒执行一次

[56] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:18.002

[57] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:19.001

Thread[http-nio-8080-exec-7,5,main] Cron已修改!修改前:0/1 * * * * ?,修改后:0/5 * * * * ? @ 14:01:19.963

[58] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:20.001

[59] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:25.002

[60] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:30.002

[61] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:35.002

[62] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:40.001

[63] Thread[pool-1-thread-1,5,main] 动态定时任务 start @ 14:01:45.002

ok,到此为止就实现了动态修改定时任务的功能。

如何实现多线程并行任务

通过上面几个例子,可能细心的朋友已经发现了一个规律:上面所有的定时任务都是单线程的。

其实也正式如此,如官方文档中解释的:

By default, will be searching for an associated scheduler definition:

either a unique TaskScheduler bean in the context, or a TaskScheduler

bean named “taskScheduler” otherwise; the same lookup will also be

performed for a ScheduledExecutorService bean. If neither of the two is

resolvable, a local single-threaded default scheduler will be created

and used within the registrar.

大概意思就是:默认情况下会检索是否指定了一个自定义的TaskScheduler,如果没有的情况下,会创建并使用一个本地单线程的任务调度器(线程池)。这一点,可以在ScheduledTaskRegistrar类(定时任务注册类)中加以佐证:

public void afterPropertiesSet() {

this.scheduleTasks();

}

protected void scheduleTasks() {

if (this.taskScheduler == null) {

this.localExecutor = Executors.newSingleThreadScheduledExecutor();

this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);

}

// ...

}

那么我们如何将调度器改造成多线程形式的呢?继续向下看文档

When more control is desired, a @Configuration class may implement

SchedulingConfigurer. This allows access to the underlying

ScheduledTaskRegistrar instance.

意思很明了,我们可以通过实现SchedulingConfigurer接口,然后通过ScheduledTaskRegistrar类去注册自定义的线程池。在SchedulingConfigurer类中已经提供了一个setScheduler方法用来注册自定义的Scheduler Bean

public void setScheduler(Object scheduler) {

Assert.notNull(scheduler, "Scheduler object must not be null");

if (scheduler instanceof TaskScheduler) {

this.taskScheduler = (TaskScheduler)scheduler;

} else {

if (!(scheduler instanceof ScheduledExecutorService)) {

throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());

}

this.taskScheduler = new ConcurrentTaskScheduler((ScheduledExecutorService)scheduler);

}

}

接下来我们就按照这种方式扩展一个SchedulingConfigurer,代码如下:

@Component

public class MultiThreadSchedulingConfigurer implements SchedulingConfigurer {

    private AtomicInteger integer = new AtomicInteger(0);

    @Scheduled(cron = "0/1 * * * * ?")

    public void multiThread() {

        System.out.println(String.format("[1] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));

    }

    @Scheduled(cron = "0/1 * * * * ?")

    public void multiThread2() {

        System.out.println(String.format("[2] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));

    }

    @Scheduled(cron = "0/1 * * * * ?")

    public void multiThread3() {

        System.out.println(String.format("[3] %s exec @ %s", Thread.currentThread().getName(), LocalTime.now()));

    }

    @Override

    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {

        scheduledTaskRegistrar.setScheduler(newExecutors());

    }

    @Bean(destroyMethod = "shutdown")

    private Executor newExecutors() {

        return Executors.newScheduledThreadPool(10, r -> new Thread(r, String.format("ZHYD-Task-%s", integer.incrementAndGet())));

    }

}

如代码所示,我们自定义一个容量为10得线程池,并且自定义一下线程池中线程得命名格式,同时为了方便查看,我们同时开启三个定时任务,在同样的时间同时执行,以此来观察定时器在并行执行时的具体线程分配情况

注意在上面的例子中自定义线程池时,使用了@Bean注解的(destroyMethod=“shutdown”)属性,这个属性可确保在Spring应用程序上下文本身关闭时正确关闭定时任务。

Note in the example above the use of @Bean(destroyMethod=“shutdown”).

This ensures that the task executor is properly shut down when the

Spring application context itself is closed.

[2] ZHYD-Task-2 exec @ 15:44:14.008

[1] ZHYD-Task-3 exec @ 15:44:14.008

[3] ZHYD-Task-1 exec @ 15:44:14.008

[2] ZHYD-Task-1 exec @ 15:44:15

[1] ZHYD-Task-3 exec @ 15:44:15

[3] ZHYD-Task-2 exec @ 15:44:15

[1] ZHYD-Task-4 exec @ 15:44:16.001

[3] ZHYD-Task-6 exec @ 15:44:16.001

[2] ZHYD-Task-5 exec @ 15:44:16.001

[1] ZHYD-Task-1 exec @ 15:44:17.002

[2] ZHYD-Task-7 exec @ 15:44:17.002

[3] ZHYD-Task-2 exec @ 15:44:17.002

[1] ZHYD-Task-1 exec @ 15:44:18.001

通过上面的结果可以看出,每个定时任务的执行线程都不在是固定的了。

总结

springboot中通过@EnableScheduling注解开启定时任务,@Scheduled支持cron、fixedDelay、fixedRate、initialDelay四种定时任务的表达式

springboot中通过实现SchedulingConfigurer接口并且重写configureTasks方法实现动态配置cron和多线程并行任务

作者:慕冬雪

链接:http://www.imooc.com/article/259921

来源:慕课网

本文首次发布于慕课网 ,转载请注明出处,谢谢合作

你可能感兴趣的:(Springboot之一文带你搞懂Scheduler定时器)