Java中定时任务调度的实现

在工作中遇到一个需求,需要定时自动执行某项功能,这就需要用到定时任务了。

定时任务调度:基于给定的时间点,给定的时间间隔或者给定的执行次数自动执行任务。

定时任务调度的几种实现方式:

Timer:
Timer由JDK自带,不需要引入多余的jar。Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。Timer只有一个后台线程执行任务,使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
Quartz:
使用Quartz,这是一个功能比较强大的的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,配置起来稍显复杂,Quartz拥有后台执行线程池能够使用多个线程
Spring Task:
Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。

Timer代码实现

首先写个需要定时实现的逻辑,这里是简单输出格式化后的时间

public class MyTimerTask extends TimerTask{
    @Override
    public void run() {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(df.format(System.currentTimeMillis()));
    }
}

然后写定时任务调度类

public class MyTimer {
    public static void main(String[] args){
        //创建timer实例
        Timer timer=new Timer();
        //创建一个MyTimerTask实例
        MyTimerTask myTimerTask=new MyTimerTask();
        //通过timer定时调用myTimerTask中的run方法,因为 TimerTask 类实现了 Runnable 接口。
        timer.schedule(myTimerTask,10*1000,2*1000);
    }
}
QQ截图20180609173715.png

这里的重点是Timer中schedule()方法的三个参数
第一个参数:是 就是我们刚刚写的MyTimerTask类,这里需要继承TimerTask,并实现 run() 方法,因为 TimerTask 类实现了 Runnable 接口。
第二个参数:是程序启动后,timer定时器第一次调用run方法的时间,0表示不指时间,立刻调用。(10*1000表示10秒,因为参数单位是毫秒)
第三个参数:是指第一次调用之后,从第二次开始每隔多长的时间调用一次 run() 方法。单位同样是毫秒。

拓展:

QQ图片20180609191051.png

其实Timer中schedule()方法一共有四种用法,刚刚只是其中一种
schedule(task, time):time为Date类型,在指定时间执行一次。
schedule(task, firstTime, period):firstTime为Date类型,从firstTime时刻开始,每隔period毫秒执行一次。
schedule(task, delay):从现在起过delay毫秒执行一次

QQ截图20180609194229.png

scheduleAtFixedRate(task,delay,period)schedule(task,delay,period)基本一样
scheduleAtFixedRate(task,firstTime,period)schedule(task,firstTime,period)基本一样

区别可以参考:https://blog.csdn.net/gtuu0123/article/details/6040159

测试结果:

QQ截图20180609174352.png

每隔两秒输出一次时间,第二个参数在截图上体现不了,需要自己敲出来感受。
注:时效性要求较高的多任务并发作业,复杂的任务的调度不推荐使用

Spring Task

@Configuration
@EnableScheduling
public class SchedulingConfig {
    @Scheduled(cron = "0/5 * * * * ?") // 每5秒执行一次
    public void Scheduled(){
        System.out.println("每5秒在控制台打印一次"+new Date());
    }
}

结果如下

Paste_Image.png

spring task 在计算时间的时候,是根据当前服务器的系统时间进行计算.
如果是每10秒执行一次的话,那么它是从系统时间的0,10,20秒进行计算的.
如果是每1分钟执行一次的话,那么它是从系统时间的1分钟,2分钟进行计算的

如何动态修改cron参数
代码解释 一开始设定的是每20秒执行一次,只要访问下
http://localhost:8080/changeExpression
就可以改成每10秒执行一次了

@RestController
@EnableScheduling
public class ConfigTask implements SchedulingConfigurer{
    private String expression="0/20 * * * * *";
    @RequestMapping("/changeExpression")
    public String changeExpression(){
        expression="0/10 * * * * *";
        return "changeExpression";
    }
@Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        Runnable task=new Runnable() {
            @Override
            public void run() {
                System.out.println("TaskCronChange task is running ... "+ new Date());
            }
        };
        Trigger trigger=new Trigger() {
            @Override
            public Date nextExecutionTime(TriggerContext triggerContext) {
                CronTrigger cronTrigger=new CronTrigger(expression);
                return cronTrigger.nextExecutionTime(triggerContext);
            }
        };
        scheduledTaskRegistrar.addTriggerTask(task,trigger);
    }
}

.nextExecutionTim()方法是为了返回一个date类型的数据
看起来很多代码,其实就是先实现SchedulingConfigurer接口。
实现里面的方法,缩小成就这样

public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        Runnable task=new Runnable() {...};
        Trigger trigger=new Trigger() {...};
        scheduledTaskRegistrar.addTriggerTask(task,trigger);
    }

项目需求:
如果有个这样的场景,在某一段时间内要执行特定的操作

@RestController
@EnableScheduling
public class ConfigTask implements SchedulingConfigurer{
    private String expression="0/20 * * * * *";
    @RequestMapping("/changeExpression")
    public String changeExpression(){
        expression="0/10 * * * * *";
        return "changeExpression";
    }
    public void println(long start,long end){
        Date date=new Date();
        long time=date.getTime()/1000;
        if(start<time&&time<end){
            System.out.println("TaskCronChange task is running ... "+ new Date());
        }else {
            System.out.println("TaskCronChange task is stopping ... "+ new Date());
        }
    }
    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        Runnable task=new Runnable() {
            @Override
            public void run() {
                println(1494306514,1494306634);
            }
        };
        Trigger trigger=new Trigger() {
            @Override
            public Date nextExecutionTime(TriggerContext triggerContext) {
                CronTrigger cronTrigger=new CronTrigger(expression);
                return cronTrigger.nextExecutionTime(triggerContext);
            }
        };
        scheduledTaskRegistrar.addTriggerTask(task,trigger);
    }
}

把时间转化成时间戳,再去对比时间
结果如下


Paste_Image.png

动态添加修改删除定时任务
1.首先需要重新认识一个类ThreadPoolTaskScheduler:线程池任务调度类,能够开启线程池进行任务调度。
2.ThreadPoolTaskScheduler.schedule()方法会创建一个定时计划ScheduledFuture,在这个方法需要添加两个参数,Runnable(线程接口类) 和CronTrigger(定时任务触发器)
3.在ScheduledFuture中有一个cancel可以停止定时任务。

@RestController
@EnableScheduling
public class DynamicTask {
    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    private ScheduledFuture<?> future;
    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        return new ThreadPoolTaskScheduler();
    }

    @RequestMapping("/startCron")
    public String startCron() {
        future = threadPoolTaskScheduler.schedule(new MyRunnable(), new CronTrigger("0/5 * * * * *"));
        System.out.println("DynamicTask.startCron()");
        return "startCron";
    }

    @RequestMapping("/stopCron")
    public String stopCron() {
        if (future != null) {
            future.cancel(true);
        }
        System.out.println("DynamicTask.stopCron()");
        return "stopCron";
    }

    @RequestMapping("/changeCron10")
    public String startCron10() {
        stopCron();// 先停止,在开启.
        future = threadPoolTaskScheduler.schedule(new MyRunnable(), new CronTrigger("*//**//*10 * * * * *"));
        System.out.println("DynamicTask.startCron10()");
        return "changeCron10";
    }

    private class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("DynamicTask.MyRunnable.run()," + new Date());
        }
    }
}

在私有类MyRunnable中的run方法中可以写入具体的定时任务逻辑
(a)我们首先了一个类DynamicTask;
(b)定义了两个变量,threadPoolTaskSchedulerfuture 其中futuretreadPoolTaskScheduler执行方法schedule的返回值,主要用于定时任务的停止。
(c)编写启动定时器的方法startCron()
(d)编写停止方法stopCron(),这里编码的时候,需要注意下需要判断下futurenull的时候,不然就很容易抛出NullPointerException
(e)编写修改定时任务执行周期方法changeCron10(),这里的原理就是关闭之前的定时器,创新在创建一个新的定时器。

Quartz

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,是一个完全由Java编写的开源作业调度框架。
官网地址:http://www.quartz-scheduler.org/
为在Java应用程序中进行作业调度提供了简单却强大的机制。Quartz允许开发人员根据时间间隔来调度作业。它实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。

Quartz的特点

① 强大的调度功能,spring默认的调度框架, Quartz 很容易与 Spring 集成实现灵活可配置的调度功能,及时系统因故障关闭,任务调度现场的数据并不会丢失
② 灵活的应用方式,运行定义触发器的调度时间表,并可以对触发器和任务进行关联映射,Quartz提供了组件式的监听器,支持任务和调度的多种组合方式,支持调度数据的多种存储方式
③ 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。

Quartz核心概念

scheduler:任务调度器
trigger:触发器,用于定义任务调度时间规则
job:任务,即被调度的任务

Quartz任务调度基本实现原理

Quartz 任务调度的核心元素是 scheduler, triggerjob,其中 triggerjob 是任务调度的元数据, scheduler 是实际执行调度的控制器。

trigger 是用于定义调度时间的元素,即按照什么时间规则去执行任务。
Quartz 中主要提供了四种类型的 trigger:SimpleTriggerCronTirggerDateIntervalTrigger,和 NthIncludedDayTrigger这四种 trigger 可以满足企业应用中的绝大部分需求。

job用于表示被调度的任务。
主要有两种类型的 job:无状态的(stateless)和有状态的(stateful)
对于同一个trigger来说,有状态的 job 不能被并行执行,只有上一次触发的任务被执行完之后,才能触发下一次执行。
Job 主要有两种属性:volatilitydurability,其中 volatility 表示任务是否被持久化到数据库存储,而 durability 表示在没有trigger关联的时候任务是否被保留。两者都是在值为true的时候任务被持久化或保留。
一个job可以被多个trigger关联,但是一个trigger只能关联一个job

scheduler
scheduler 工厂创建:DirectSchedulerFactory 或者 StdSchedulerFactory
StdSchedulerFactory 使用较多,因为 DirectSchedulerFactory 使用起来不够方便,需要作许多详细的手工编码设置。
Scheduler 主要有三种:RemoteMBeanSchedulerRemoteSchedulerStdScheduler

在maven项目中使用Quartz

pom.xml文件中添加quartz的依赖:
版本(Apr 19, 2017)

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>

首先我们需要定义一个任务类,该类需要继承Job类,
添加execute(JobExecutionContext context)方法,在这个方法中就是我们具体的任务执行的地方。

public class HelloJob implements Job{
    @Override
    public void execute(JobExecutionContext jobExecutionContext) 
                    throws JobExecutionException {
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(df.format(System.currentTimeMillis()));
    }
}

任务调度

public class APP {
    public static void main(String[] args) throws 
                                    SchedulerException, InterruptedException {
        // 获取Scheduler实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        scheduler.start();
        //具体任务.
        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
                                          .withIdentity("job1","group1").build();

        //触发时间点. (每5秒执行1次.)
        SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder
        .simpleSchedule().withIntervalInSeconds(5).repeatForever();
        Trigger trigger = TriggerBuilder.newTrigger()
                           .withIdentity("trigger1","group1") .startNow()
                           .withSchedule(simpleScheduleBuilder).build();
        // 交由Scheduler安排触发
        scheduler.scheduleJob(jobDetail,trigger);
    }
}

测试结果:

QQ截图20180609210103.png

结果就是每隔5秒输出一次时间此外还会输出关于触发器的一些信息

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

推荐阅读更多精彩内容