补习系列(9)-springboot 定时器,你用对了吗

目录

  • 简介
  • 一、应用启动任务
  • 二、JDK 自带调度线程池
  • 三、@Scheduled
    • 定制 @Scheduled 线程池
  • 四、@Async
    • 定制 @Async 线程池
  • 小结

简介

大多数的应用程序都离不开定时器,通常在程序启动时、运行期间会需要执行一些特殊的处理任务。
比如资源初始化、数据统计等等,SpringBoot 作为一个灵活的框架,有许多方式可以实现定时器或异步任务。
我总结了下,大致有以下几种:

    1. 使用 JDK 的 TimerTask
    1. 使用 JDK 自带调度线程池
    1. 使用 Quartz 调度框架
    1. 使用 @Scheduled 、@Async 注解

其中第一种使用 TimerTask 的方法已经不建议使用,原因是在系统时间跳变时TimerTask存在挂死的风险
第三种使用 Quartz 调度框架可以实现非常强大的定时器功能,包括分布式调度定时器等等。
考虑作为大多数场景使用的方式,下面的篇幅将主要介绍 第二、第四种。

一、应用启动任务

在 SpringBoot 应用程序启动时,可以通过以下两个接口实现初始化任务:

  1. CommandLineRunner
  2. ApplicationRunner

两者的区别不大,唯一的不同在于:
CommandLineRunner 接收一组字符串形式的进程命令启动参数;
ApplicationRunner 接收一个经过解析封装的参数体对象。

详细的对比看下代码:

public class CommandLines {

    private static final Logger logger = LoggerFactory.getLogger(CommandLines.class);

    @Component
    @Order(1)
    public static class CommandLineAppStartupRunner implements CommandLineRunner {

        @Override
        public void run(String... args) throws Exception {
            logger.info(
                    "[CommandLineRunner]Application started with command-line arguments: {} .To kill this application, press Ctrl + C.",
                    Arrays.toString(args));
        }
    }

    @Component
    @Order(2)
    public static class AppStartupRunner implements ApplicationRunner {

        @Override
        public void run(ApplicationArguments args) throws Exception {
            logger.info("[ApplicationRunner]Your application started with option names : {}", args.getOptionNames());
        }
    }
}

二、JDK 自带调度线程池

为了实现定时调度,需要用到 ScheduledThreadpoolExecutor
初始化一个线程池的代码如下:

    /**
     * 构造调度线程池
     *
     * @param corePoolSize
     * @param poolName
     * @return
     */
    public static ScheduledThreadPoolExecutor newSchedulingPool(int corePoolSize, String poolName) {

        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize);

        // 设置变量
        if (!StringUtils.isEmpty(poolName)) {
            threadPoolExecutor.setThreadFactory(new ThreadFactory() {

                @Override
                public Thread newThread(Runnable r) {
                    Thread tr = new Thread(r, poolName + r.hashCode());
                    return tr;
                }
            });
        }
        return threadPoolExecutor;
    }

可以将 corePoolSize 指定为大于1,以实现定时任务的并发执行。

为了在 SpringBoot 项目中使用,我们利用一个CommandLineRunner来实现:

@Component
@Order(1)
public class ExecutorTimer implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(ExecutorTimer.class);

    private ScheduledExecutorService schedulePool;

    @Override
    public void run(String... args) throws Exception {
        logger.info("start executor tasks");

        schedulePool = ThreadPools.newSchedulingPool(2);

        schedulePool.scheduleWithFixedDelay(new Runnable() {

            @Override
            public void run() {
                logger.info("run on every minute");

            }
        }, 5, 60, TimeUnit.SECONDS);
    }
}

schedulePool.scheduleWithFixedDelay 指定了调度任务以固定的频率执行。

三、@Scheduled

@Scheduled 是 Spring3.0 提供的一种基于注解实现调度任务的方式。
在使用之前,需要通过 @EnableScheduling 注解启用该功能。

代码如下:

/**
 * 利用@Scheduled注解实现定时器
 *
 * @author atp
 *
 */
@Component
public class ScheduleTimer {

    private static final Logger logger = LoggerFactory.getLogger(ScheduleTimer.class);

    /**
     * 每10s
     */
    @Scheduled(initialDelay = 5000, fixedDelay = 10000)
    public void onFixDelay() {
        logger.info("schedule job on every 10 seconds");
    }

    /**
     * 每分钟的0秒执行
     */
    @Scheduled(cron = "0 * * * * *")
    public void onCron() {
        logger.info("schedule job on every minute(0 second)");
    }

    /**
     * 启用定时器配置
     *
     * @author atp
     *
     */
    @Configuration
    @EnableScheduling
    public static class ScheduleConfig {
    }
}

说明
上述代码中展示了两种定时器的使用方式:

第一种方式
指定初始延迟(initialDelay)、固定延迟(fixedDelay);

第二种方式
通过 cron 表达式定义
这与 unix/linux 系统 crontab 的定义类似,可以实现非常灵活的定制。

一些 cron 表达式的样例:

表达式 说明
0 0 * * * * 每天的第一个小时
/10 * * * * 每10秒钟
0 0 8-10 * * * 每天的8,9,10点钟整点
0 * 6,19 * * * 每天的6点和19点每分钟
0 0/30 8-10 * * * 每天8:00, 8:30, 9:00, 9:30 10:00
0 0 9-17 * * MON-FRI 工作日的9点到17点
0 0 0 25 12 ? 每年的圣诞夜午夜

定制 @Scheduled 线程池

默认情况下,@Scheduled 注解的任务是由一个单线程的线程池进行调度的。
这样会导致应用内的定时任务只能串行执行。

为了实现定时任务并发,或是更细致的定制,
可以使用 SchedulingConfigurer 接口。

代码如下:

    @Configuration
    @EnableScheduling
    public class ScheduleConfig implements SchedulingConfigurer {

        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.setScheduler(taskExecutor());
        }

        @Bean(destroyMethod="shutdown")
        public Executor taskExecutor() {
            //线程池大小
            return Executors.newScheduledThreadPool(50);
        }
    }

四、@Async

@Async 注解的意义在于将 Bean方法的执行方式改为异步方式。
比如 在前端请求处理时,能通过异步执行提前返回结果。

类似的,该注解需要配合 @EnableAsync 注解使用。

代码如下:

    @Configuration
    @EnableAsync
    public static class ScheduleConfig {

    }

使用 @Async 实现模拟任务

@Component
public class AsyncTimer implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(AsyncTimer.class);

    @Autowired
    private AsyncTask task;

    @Override
    public void run(String... args) throws Exception {
        long t1 = System.currentTimeMillis();
        task.doAsyncWork();

        long t2 = System.currentTimeMillis();
        logger.info("async timer execute in {} ms", t2 - t1);
    }

    @Component
    public static class AsyncTask {

        private static final Logger logger = LoggerFactory.getLogger(AsyncTask.class);

        @Async
        public void doAsyncWork() {
            long t1 = System.currentTimeMillis();

            try {
                Thread.sleep((long) (Math.random() * 5000));
            } catch (InterruptedException e) {
            }

            long t2 = System.currentTimeMillis();
            logger.info("async task execute in {} ms", t2 - t1);
        }
    }

示例代码中,AsyncTask 等待一段随机时间后结束。
而 AsyncTimer 执行了 task.doAsyncWork,将提前返回。

执行结果如下:

- async timer execute in 2 ms
- async task execute in 3154 ms

这里需要注意一点,异步的实现,其实是通过 Spring 的 AOP 能力实现的。
对于 AsyncTask 内部方法间的调用却无法达到效果。

定制 @Async 线程池

对于 @Async 线程池的定制需使用 AsyncConfigurer接口。

代码如下:

    @Configuration
    @EnableAsync
    public static class ScheduleConfig implements AsyncConfigurer {

        @Bean
        public ThreadPoolTaskScheduler taskScheduler() {
            ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
            //线程池大小
            scheduler.setPoolSize(60);
            scheduler.setThreadNamePrefix("AsyncTask-");
            scheduler.setAwaitTerminationSeconds(60);
            scheduler.setWaitForTasksToCompleteOnShutdown(true);
            return scheduler;
        }

        @Override
        public Executor getAsyncExecutor() {
            return taskScheduler();
        }

        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return null;
        }

    }

小结

定时异步任务是应用程序通用的诉求,本文收集了几种常见的实现方法。
作为 SpringBoot 应用来说,使用注解是最为便捷的。
在这里我们对 @Scheduled、@Async 几个常用的注解进行了说明,
并提供定制其线程池的方法,希望对读者能有一定帮助。

欢迎继续关注"美码师的补习系列-springboot篇" ,如果觉得老司机的文章还不赖,请多多分享转发^-^

原文地址:https://www.cnblogs.com/littleatp/p/9615014.html

时间: 2024-10-08 18:28:16

补习系列(9)-springboot 定时器,你用对了吗的相关文章

补习系列(15)-springboot 分布式会话原理

目录 一.背景 二.SpringBoot 分布式会话 三.样例程序 四.原理进阶 A. 序列化 B. 会话代理 C. 数据老化 小结 一.背景 在 补习系列(3)-springboot 几种scope 一文中,笔者介绍过 Session的部分,如下: 对于服务器而言,Session 通常是存储在本地的,比如Tomcat 默认将Session 存储在内存(ConcurrentHashMap)中. 但随着网站的用户越来越多,Session所需的空间会越来越大,同时单机部署的 Web应用会出现性能瓶颈

补习系列(10)-springboot 之配置读取

目录 简介 一.配置样例 二.如何注入配置 1. 缺省配置文件 2. 使用注解 3. 启动参数 还有.. 三.如何读取配置 @Value 注解 Environment 接口 @ConfigurationProperties 注解 四.不同环境中的配置 1. 区别开发.测试.发布环境 2. 声明多配置文件 参考文档 简介 在早前的博客中曾经写过 Spring 程序通过 Bean 映射实现配置信息的读取. 在SpringBoot 框架中读取配置的方式变得非常多样,这导致读者在搜寻资料时反而容易迷糊.

补习系列(21)-SpringBoot初始化之7招式

目录 背景 1. @PostConstruct 注解 2. InitializingBean 接口 3. @Bean initMethod方法 4. 构造器注入 5. ApplicationListener 6. CommandLineRunner 7. ApplicationRunner 测试代码 参考文档 背景 在日常开发时,我们常常需要 在SpringBoot 应用启动时执行某一段逻辑,如下面的场景: 获取一些当前环境的配置或变量 向数据库写入一些初始数据 连接某些第三方系统,确认对方可以

补习系列(1)-springboot项目基础搭建课

[TOC] 前言 springboot 最近火的不行,目前几乎已经是 spring 家族最耀眼的项目了.抛开微服务.技术社区这些推广因素不说,框架本身的确有非常多的优点.比如 更简化的配置,摒除了许多繁杂的xml配置(事实证明,越简单的东西越容易让人记住): 内置Servlet容器,不再依赖外部环境 大量的starter模块,随手拈来 支持热部署 作为一名老程序员来说,仍然需要保持一个积极学习的态度.哎,简单点说就是少点伤感,认清现实.你曾经引以为傲的某某EE 技术已经被颠覆了,赶紧换车道 ..

补习系列-springboot 实现拦截的五种姿势

目录 简介 姿势一.使用 Filter 接口 1. 注册 FilterRegistrationBean 2. @WebFilter 注解 姿势二.HanlderInterceptor 姿势三.@ExceptionHandler 注解 姿势四.RequestBodyAdvice/ResponseBodyAdvice RequestBodyAdvice 的用法 ResponseBodyAdvice 用法 姿势五.@Aspect 注解 思考 小结 简介 AOP(面向切面编程)常用于解决系统中的一些耦合

补习系列-springboot 单元测试之道

目录 目标 一.About 单元测试 二.About Junit 三.SpringBoot-单元测试 项目依赖 测试样例 四.Mock测试 五.最后 目标 了解 单元测试的背景 了解如何 利用 springboot 实现接口的测试 了解如何 利用 mokito 做代码的 mock 一.About 单元测试 单元测试其实是一种廉价的技术,是由开发者创建运行测试代码,用于对程序模块(软件设计的最小单位)进行正确性检验的一种做法. 而所谓的最小单元,就是指应用的最小可测试部件. 在面向对象领域,最小单

补习系列-springboot-restful应用

一.目标 了解 Restful 是什么,基本概念及风格: 能使用SpringBoot 实现一套基础的 Restful 风格接口: 利用Swagger 生成清晰的接口文档. 二.Restful 入门 什么是REST 摘自百科的定义:REST即表述性状态转移(英文:Representational State Transfer,简称REST) 是Roy Fielding博士(HTTP规范主要贡献者)在2000年的论文中提出来的一种软件架构风格. 是一种针对网络应用的设计和开发方式,可以降低开发的复杂

补习系列(20)-大话 WebSocket 与 "尬聊"的实现

目录 一.聊聊 WebSocket 二.Stomp 是个什么鬼 三.SpringBoot 整合 WebSocket A. 引入依赖 B. WebSocket 配置 C. 控制器 D. 前端实现 四.参考文档 一.聊聊 WebSocket 从HTML5技术流行至今,WebSocket已经有非常广泛的应用: 在线游戏,提供实时的操作交互体验 社交平台,与好友实时的私信对话 新闻动态,获得感兴趣的主题信息推送 ... 这些场景,都需要服务器能主动实时的给浏览器或客户端推送消息,注意关键词是主动,还有实

不过如此系列之SpringBoot入门

最早接触SpringBoot大概是在2016年了,最初是用其构建了一个手机web端的网站,之后自己深入接触之后用Springboot构建了一个自己的博客网站,但是一直没有梳理其中的知识点,太过于混乱而意识到必须要整理一番了.本系列博客适用于对spring及springmvc等框架有使用的同学,个人认为Springboot差不多是对spring生态圈的整合,简化了web开发,极大的降低了web开发的门槛节约了开发者的时间,同时由于大部分配置封装在其内部也能够考验开发者对程序出现异常的把控能力. 接