A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

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

其中第一种使用 TimerTask 的方法已经不建议使用,原因是在系统时间跳变时TimerTask存在挂死的风险。
第三种使用 Quartz 调度框架可以实现非常强大的定时器功能,包括分布式调度定时器等等。
考虑作为大多数场景使用的方式,下面的篇幅将主要介绍 第二、第四种。
一、应用启动任务
在 SpringBoot 应用程序启动时,可以通过以下两个接口实现初始化任务:
  • CommandLineRunner
  • ApplicationRunner
两者的区别不大,唯一的不同在于:
CommandLineRunner 接收一组字符串形式的进程命令启动参数;
ApplicationRunner 接收一个经过解析封装的参数体对象。
详细的对比看下代码:
[Java] 纯文本查看 复制代码
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
初始化一个线程池的代码如下:
[Java] 纯文本查看 复制代码
 /**
         * 构造调度线程池
         * 
         * @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来实现:
[Java] 纯文本查看 复制代码
@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 注解启用该功能。
代码如下:
[Java] 纯文本查看 复制代码
 /**
     * 利用@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 表达式的样例:
表达式
说明
00
每天的第一个小时
/10
每10秒钟
008-10*
每天的8,9,10点钟整点
06,19
每天的6点和19点每分钟
00/308-10*
每天8:00,8:30,9:00,9:3010:00
009-17MON-FRI
工作日的9点到17点
0002512?
每年的圣诞夜午夜
定制 @Scheduled 线程池
默认情况下,@Scheduled 注解的任务是由一个单线程的线程池进行调度的。
这样会导致应用内的定时任务只能串行执行。
为了实现定时任务并发,或是更细致的定制,
可以使用 SchedulingConfigurer 接口。
代码如下:
[Java] 纯文本查看 复制代码
@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 注解使用。
代码如下:
[Java] 纯文本查看 复制代码
 @Configuration
        @EnableAsync
        public static class ScheduleConfig {

        }
使用 @Async 实现模拟任务
[Java] 纯文本查看 复制代码
@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,将提前返回。
执行结果如下:
[Java] 纯文本查看 复制代码
 - async timer execute in 2 ms
 - async task execute in 3154 ms
这里需要注意一点,异步的实现,其实是通过 Spring 的 AOP 能力实现的。
对于 AsyncTask 内部方法间的调用却无法达到效果。
定制 @Async 线程池
对于 @Async 线程池的定制需使用 AsyncConfigurer接口。
代码如下:
[Java] 纯文本查看 复制代码
 @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 几个常用的注解进行了说明,
并提供定制其线程池的方法,希望能有一定帮助。

0 个回复

您需要登录后才可以回帖 登录 | 加入黑马