Spring 4.x Task 和 Schedule 概述

原创-转载请注明 http://tramp.cincout.cn/2016/09/30/spring-task-and-schedule-deep-research/

摘要

在很多业务场景中,系统都需要用到任务调度系统。例如定期地清理Redis 缓存,周期性地检索某一条件并更新系统的资源等。在现代的应用系统中,快速地响应用户的请求,是用户体验最主要的因素之一。因此在Web 系统中异步地执行任务,也会在很多场景中经常涉及到。本文对任务调度和异步执行的Java 实现进行了总结,主要讲述一下内容:

  • Java 对异步执行和任务调度的支持
  • Spring 4.X 的异步执行和任务调度实现

Java 对异步执行和任务调度的支持

异步执行和任务调度底层的语言支撑都是Java 的多线程技术。线程是系统进行独立运行和调度的基本单位。拥有了多线程,系统就拥有了同时处理多项任务的能力。

Java 实现异步调用

在Java 中要实现多线程有实现Runnable 接口和扩展Thread 类两种方式。只要将需要异步执行的任务放在run() 方法中,在主线程中启动要执行任务的子线程就可以实现任务的异步执行。如果需要实现基于时间点触发的任务调度,就需要在子线程中循环的检查系统当前的时间跟触发条件是否一致,然后触发任务的执行。该内容属于Java 多线程的基础知识,此处略过不讲。

Java Timer 和 TimeTask 实现任务调度

为了便于开发者快速地实现任务调度,Java JDK 对任务调度的功能进行了封装,实现了Timer 和TimerTask 两个工具类。

TimerTask 类
TimerTask 类

由上图,我们可以看出TimeTask 抽象类在实现Runnable 接口的基础上增加了任务cancel() 和任务scheduledExecuttionTime() 两个方法。

Timer 类
Timer 类

上图为调度类Timer 的实现。从Timer类的源码,可以看到其采用TaskQueue 来实现对多个TimeTask 的管理。TimerThread 集成自Thread 类,其mainLoop() 用来对任务进行调度。而Timer 类提供了四种重载的schedule() 方法和重载了两种sheduleAtFixedRate() 方法来实现几种基本的任务调度类型。下面的代码是采用Timer 实现的定时系统时间打印程序。

public class PrintTimeTask extends TimerTask {
    @Override
        public void run() {
            System.out.println(new Date().toString());
        }
                    
    public static void main(String[] args) {
        Timer timer = new Timer("hello");
        timer.schedule(new PrintTimeTask(), 1000L, 2000L);
    }
}

Spring 4.x 中的异步执行和任务调度

Spring 4.x 中的异步执行

Spring 作为一站式框架,为开发者提供了异步执行和任务调度的抽象接口TaskExecutorTaskScheduler。Spring 对这些接口的实现类支持线程池(Thread Pool) 和代理。
Spring 提供了对JDK 中Timer和开源的流行任务调度框架Quartz的支持。Spring 通过将关联的Schedule 转化为FactoryBean 来实现。通过Spring 调度框架,开发者可以快速地通过MethodInvokingFactoryBean 来实现将POJO 类的方法转化为任务。

Spring TaskExecutor

TaskExecutor 接口扩展自java.util.concurrent.Executor 接口。TaskExecutor 被创建来为其他组件提供线程池调用的抽象。

ThreadPoolTaskExecutor 是TaskExecutor 的最主要实现类之一。该类的核心继承关系如下图所示。

ThreadPooltaskexecutor 类
ThreadPooltaskexecutor 类

ThreadPoolTaskExecutor 接口扩展了重多的接口,让其具备了更多的能力。要实现异步需要标注@Async 注解:

  • AsyncTaskExecutor 增加了返回结果为Future 的submit() 方法,该方法的参数为Callable 接口。相比Runnable 接口,多了将执行结果返回的功能。
  • AsyncListenableTaskExecutor 接口允许返回拥有回调功能的ListenableFuture 接口,这样在结果执行完毕是,能够直接回调处理。
public class ListenableTask {
    @Async
    public ListenableFuture<Integer> compute(int n) {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return new AsyncResult<>(sum);
    }

    static class CallBackImpl implements 
        ListenableFutureCallback<Integer> {
        @Override
        public void onFailure(Throwable ex) {
            System.out.println(ex.getMessage());
        }

        @Override
        public void onSuccess(Integer result) {
            System.out.println(result);
        }
    }

    public static void main(String[] args) {
        ListenableTask listenableTask = new ListenableTask();
        ListenableFuture<Integer> listenableFuture = 
            listenableTask.compute(10);
        listenableFuture.addCallback(new CallBackImpl());
    }
}
  • ThreadFactory 定义了创建线程的工厂方法,可以扩展该方法实现对Thread 的改造。

基于Java Config

  • 基于注解 当采用基于Java Config 注解配置时,只需要在主配置添加@EnableAsync 注解,Spring 会自动的创建基于ThreadPoolTaskExecutor 实例注入到上下文中。
@Configuration
@EnableAsync
public class AppConfig {
}
  • 基于AsyncConfigurer接口自定义 开发者可以自定义Executor 的类型,并且注册异常处理器。
@Configuration
public class TaskConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(100);
        executor.setCorePoolSize(10);
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncUncaughtExceptionHandler() {
            @Override
            public void handleUncaughtException(Throwable ex, 
                                                Method method, Object... params) {
                System.out.println(ex.getMessage());
            }
        };
    }
}

基于XML Config

  • 基于传统XML的配置 基于XML 的形式,采用传统的Java Bean的形式配置ThreadPoolTaskExecutor。然后采用自动注入(autowire, resource,name)的可以直接在Spring Component 中注入Executor。以编程的形式实现异步任务。
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.
    ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="5" />
    <property name="maxPoolSize" value="10" />
    <property name="queueCapacity" value="25" />
</bean>
  • 基于task 命名空间的配置 Spring 为任务的执行提供了便利的task 命名空间。当采用基于XML 配置时Spring 会自动地为开发者创建Executor。同时可以在annotation-driven 标签上注册实现了AsyncUncaughtExceptionHandler 接口的异常处理器。
<!-- config exception handler  -->
<bean id="taskAsyncExceptionHandler" class="org.zzy.spring4.application.schedulie.TaskAsyncExceptionHandler"/>
<task:annotation-driven exception-handler="taskAsyncExceptionHandler" scheduler="scheduler" executor="executor"/>

异步执行的异常处理

除了上文提到的两种异常处理方式,Spring 还提供了基于SimpleApplicationEventMulticaster 类的异常处理方式。

@Bean
public SimpleApplicationEventMulticaster eventMulticaster(TaskExecutor taskExecutor) {
    SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
    eventMulticaster.setTaskExecutor(taskExecutor);
    eventMulticaster.setErrorHandler(new ErrorHandler() {
        @Override
        public void handleError(Throwable t) {
            System.out.println(t.getMessage());
        }
    });
    return eventMulticaster;
}

Spring 4.x 中任务调度实现

Spring 的任务调度主要基于TaskScheduler 接口。ThreadPoolTaskScheduler 是Spring 任务调度的核心实现类。该类提供了大量的重载方法进行任务调度。Trigger 定义了任务被执行的触发条件。Spring 提供了基于Corn 表达式的CornTrigger实现。TaskScheduler 如下图所示。

ThreadPoolTaskExecutor 类
ThreadPoolTaskExecutor 类
实现TaskScheduler 接口的ThreadPoolTaskExecutor 继承关系。

ThreadPoolTaskExecutor 类
ThreadPoolTaskExecutor 类

基于Java Config

  • 基于注解的配置 当采用基于Java Config 注解配置时,只需要在主配置添加@EnableScheduling 注解,Spring 会自动的创建基于ThreadPoolTaskExecutor 实例注入到上下文中。
@Configuration
@EnableScheduling
public class AppConfig {
}
  • 基于SchedulingConfigurer接口自定义
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setTaskScheduler(new ThreadPoolTaskScheduler());
        taskRegistrar.getScheduler().schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        }, new CronTrigger("0 15 9-17 * * MON-FRI"));
    }
}

基于XML Config

<task:annotation-driven scheduler="myScheduler"/>
<task:scheduler id="myScheduler" pool-size="10"/>

@Scheduled 注解的使用

当某个Bean 由Spring 管理生命周期时,就可以方便的使用@Shcheduled 注解将该Bean 的方法准换为基于任务调度的策略。

@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
    // something that should execute periodically
}

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
    // something that should execute on weekdays only
}

task 命名空间中的task:scheduled-tasks

该元素能够实现快速地将一个普通Bean 的方法转换为Scheduled 任务的途径。具体如下:

<task:scheduled-tasks scheduler="myScheduler">
    <task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
    <task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
    <task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>

总结

本文着重介绍了JDK 为任务调度提供的基础类Timer。并在此基础上详细介绍了Spring 4.x 的异步执行和任务调度的底层接口设计。并针对常用的模式进行了讲解,并附带了源代码。第三方开源的Quartz 实现了更为强大的任务调度系统,Spring 也对集成Quartz 提供了转换。之后会择机再详细的介绍Quartz 的应用和设计原理。同时,Servlet 3.x 为Web 的异步调用提供了AsyncContext,对基于Web 的异步调用提供了原生的支持,后续的文章也会对此有相应的介绍。

参考引用

  1. Spring Doc Task and Schedule
  2. Quartz
  3. Corn Wiki
  4. Servlet AsyncContext
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • 博客原文 徒手翻译spring framework 4.2.3官方文档的第33章,若有翻译不当之处请指正。 定时任...
    rabbitGYK阅读 5,614评论 4 24
  • 今天晨读分享的书籍是——《只需倾听》,分享的内容有:沟通需要让对方觉得被尊重、被理解、被关注。 被尊重 以前,我以...
    5070黑土阅读 294评论 6 7
  • 注:由此文章构思初稿改写而成,初稿是友人赠与处在百鬼夜行下的友人自己而著,由本人改写而成,以满足自己的倾诉欲。 “...
    鲟觅阅读 493评论 0 4