SSM框架下定时任务调度的实现(留备)

这是SSM框架整合的第4篇文章了,这两天基本都是每天整理2篇的节奏,真的是闲得蛋疼了我。不过这几篇文章都是自己动手实践过程中记录的,期间也遇到一些问题,参考了一些前辈的实现方式。在这里感谢前辈们的无私奉献,才换来现在我们的学习成本才能这么低,谢谢大家~
回归话题,今天想跟大家分享的是在ssm框架下实现任务调度的实现方式。因为在我们实际的项目中,很多时候会使用到一些定时器,比如邮件汇总、报表汇总这样的业务场景。那在ssm框架下有什么方式实现呢?本人常用的有2种,这里当做记录,和大家一起分享下。

spring task

spring task是spring 3.0版本开始,框架自带的任务调度开发工具。可以通过注解或配置的方式简单快速地将一个普通类转为定时任务,只要在spring-context.xml中启用spring task,简单的配置即可,如:

<task:annotation-driven />

1
2

普通类:
public class DemoTask {
public void doSomething() {
//想要做的事情
}
}

1
2
3
4
5
6
7

@Component
public class DemoTask {
@Scheduled(cron=“0 0/1 * * * ?”) //cron表达式
public void doSomething() {
//想要做的事情
}
}

1
2
3
4
5
6
7

Quartz

Quartz是一个开源的框架,实现的功能也是任务调度。它采用工厂模式进行任务调度,主要有3个概念:JobDetail、Tigger、SchedulerFactory,个人简单的理解就是:什么事情(JobDetail)在什么时候(Tigger)有哪位大爷(SchedulerFactory)调配着做。当然,Qaurtz的功能十分强大,还能够配合mysql数据库实现集群化。这里我们就基于一个需求来实现SSM跟Quartz的整合。

需求

在我们的实际项目中经常会遇到这样的一种情况,我在业务代码里面设定了一个定时任务,单机跑起来没问题吧,都可以执行。但是蛋疼的是,我们很多时候项目是采用集群方式部署的,假设部署了A、B、C三台服务器,那业务代码肯定是一样的,这样定时任务就会各自独立跑一遍吧,假如这个定时任务操作了数据库,写数据进去,是不是有可能因为同样的流程并发着跑了导致有脏数据了。很简单的想法就是我就留一台机子跑定时任务嘛,其他两台去掉相同的业务代码不就可以了?当然可以,可是部署是不是就很麻烦了,还有一个就是哪天机子宕掉了,定时任务不就没有了。所以,基于上述的需要,我们需要的是,集群内服务器的代码肯定要一样的,然后我们的定时任务每次只能有一台机子跑,谁跑我们不关心嘛。这时我们就基于Quartz实现下,把它整合到SSM框架去。

ssm整合Quartz

首先,还是在pom.xml添加相关的依赖。
org.quartz-scheduler quartz 2.2.1 org.quartz-scheduler quartz-jobs 2.2.1
接着我们要基于QuartzJobBean这个抽象接口实现我们的具体要调度的任务,但是我们又不想我们的任务继承任何类或接口实现,Quartz提供了MethodInvokingJobDetailFactoryBean帮我们实现了这一点。

DemoTask.java
public class DemoTask {
public void execute() {
System.out.println(Util.dateFormat(“集群化定时任务…”);
}
}

spring-quartz.xml

<?xml version="1.0" encoding="UTF-8"?>






























貌似大功告成了吧,启动项目看看,结果发现报错了。刚开始接触的时候也觉得一头雾水,不知道怎么办,后面看了几位前辈的文章介绍才知道Spring从2.0.2开始便不再支持Quartz集群。具体表现 在Quartz 的 Task 实例化被写入进入数据库后,会产生 Serializable 的错误。
这里写图片描述
那如何处理这个问题呢?参考了网络上大家的一些做法,就是直接实现JobDetail,自己实现MethodInvokingJobDetailFactoryBean的逻辑,MethodInvokingJobDetailFactoryBean 的思想:是在executeInternal时将通过反射获取bean的方法,调用invoke执行该方法,配合jobClass和jobDataAsMap实现注入。那我们修改下代码按这思路处理下:

JobDetailBean.java
public class JobDetailBean extends QuartzJobBean {
private String targetObject;
private String targetMethod;
private ApplicationContext ctx;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {

    try {
        Object bean = ctx.getBean(targetObject);
        Method m = bean.getClass().getDeclaredMethod(targetMethod);
        m.invoke(bean);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
public void setApplicationContext(ApplicationContext applicationContext) {
    this.ctx = applicationContext;
}
public void setTargetObject(String targetObject) {
    this.targetObject = targetObject;
}
public void setTargetMethod(String targetMethod) {
    this.targetMethod = targetMethod;
}

}

spring-quartz.xml

<?xml version="1.0" encoding="UTF-8"?>

<!-- 实例 -->
<bean id="demo"
    class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
    <property name="jobClass" value="com.maven4web.quartz.JobDetailBean" />
    <property name="durability" value="true" />
    <property name="description" value="demo实例..." />
    <property name="jobDataAsMap">
        <map>
            <entry key="targetObject" value="demoTask" />
            <entry key="targetMethod" value="execute" />
        </map>
    </property>
</bean>

<bean id="demoTrigger"
    class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
    <property name="jobDetail" ref="demo" />
    <property name="cronExpression" value="0 0/1 * * * ?" />
</bean>

<!-- 以下是Quartz定时调度配制 -->
<bean id="schedulerFactory"
    class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="configLocation" value="classpath:quartz.properties" />
    <property name="dataSource" ref="dataSource" />
    <property name="applicationContextSchedulerContextKey" value="applicationContext" />
    <!--可选,QuartzScheduler 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 -->
    <property name="overwriteExistingJobs" value="true" />
    <!-- 设置自动启动 -->
    <property name="autoStartup" value="true" />
    <property name="startupDelay" value="5" />
    <property name="triggers">
        <list>
            <ref bean="demoTrigger" />
        </list>
    </property>
</bean>
ok,我们启动服务器再看看效果。服务器正常启动了,1分钟之后控制台也打印了我们写在定时器里面的内容。 这里写图片描述 那至于在集群环境下是否能够达到我们想要的效果,大家可以把项目部署到2、3台服务器上试试。本人之前在实际的项目中已经使用了这种方式实现了,就不多此一举了。Over~

== 另外一个==

一、什么是Quartz

什么是Quartz?

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:

    持久性作业 - 就是保持调度定时的状态;
    作业管理 - 对调度作业进行有效的管理;

大部分公司都会用到定时任务这个功能。
拿火车票购票来说,当你下单后,后台就会插入一条待支付的task(job),一般是30分钟,超过30min后就会执行这个job,去判断你是否支付,未支付就会取消此次订单;当你支付完成之后,后台拿到支付回调后就会再插入一条待消费的task(job),Job触发日期为火车票上的出发日期,超过这个时间就会执行这个job,判断是否使用等。

在我们实际的项目中,当Job过多的时候,肯定不能人工去操作,这时候就需要一个任务调度框架,帮我们自动去执行这些程序。那么该如何实现这个功能呢?

(1)首先我们需要定义实现一个定时功能的接口,我们可以称之为Task(或Job),如定时发送邮件的task(Job),重启机器的task(Job),优惠券到期发送短信提醒的task(Job),实现接口如下:
这里写图片描述

(2)有了任务之后,还需要一个能够实现触发任务去执行的触发器,触发器Trigger最基本的功能是指定Job的执行时间,执行间隔,运行次数等。
这里写图片描述

(3)有了Job和Trigger后,怎么样将两者结合起来呢?即怎样指定Trigger去执行指定的Job呢?这时需要一个Schedule,来负责这个功能的实现。
这里写图片描述

上面三个部分就是Quartz的基本组成部分:

调度器:Scheduler
任务:JobDetail
触发器:Trigger,包括SimpleTrigger和CronTrigger

二、Quartz Demo搭建

下面来利用Quartz搭建一个最基本的Demo。
1、导入依赖的jar包:

org.quartz-scheduler quartz 2.3.0
1
2
3
4
5
6

2、新建一个能够打印任意内容的Job:

/**

  • Created by wanggenshen

  • Date: on 2018/7/7 16:28.

  • Description: 打印任意内容
    */
    public class PrintWordsJob implements Job{

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
    String printTime = new SimpleDateFormat(“yy-MM-dd HH-mm-ss”).format(new Date());
    System.out.println(“PrintWordsJob start at:” + printTime + “, prints: Hello Job-” + new Random().nextInt(100));

    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

3、创建Schedule,执行任务:

/**

  • Created by wanggenshen

  • Date: on 2018/7/7 16:31.

  • Description: XXX
    */
    public class MyScheduler {

    public static void main(String[] args) throws SchedulerException, InterruptedException {
    // 1、创建调度器Scheduler
    SchedulerFactory schedulerFactory = new StdSchedulerFactory();
    Scheduler scheduler = schedulerFactory.getScheduler();
    // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容)
    JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class)
    .withIdentity(“job1”, “group1”).build();
    // 3、构建Trigger实例,每隔1s执行一次
    Trigger trigger = TriggerBuilder.newTrigger().withIdentity(“trigger1”, “triggerGroup1”)
    .startNow()//立即生效
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
    .withIntervalInSeconds(1)//每隔1s执行一次
    .repeatForever()).build();//一直执行

     //4、执行
     scheduler.scheduleJob(jobDetail, trigger);
     System.out.println("--------scheduler start ! ------------");
     scheduler.start();
    
     //睡眠
     TimeUnit.MINUTES.sleep(1);
     scheduler.shutdown();
     System.out.println("--------scheduler shutdown ! ------------");
    

    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36

运行程序,可以看到程序每隔1s会打印出内容,且在一分钟后结束:
这里写图片描述
三、Quartz核心详解

下面就程序中出现的几个参数,看一下Quartz框架中的几个重要参数:

Job和JobDetail
JobExecutionContext
JobDataMap
Trigger、SimpleTrigger、CronTrigger

(1)Job和JobDetail
Job是Quartz中的一个接口,接口下只有execute方法,在这个方法中编写业务逻辑。
接口中的源码:
这里写图片描述

JobDetail用来绑定Job,为Job实例提供许多属性:

name
group
jobClass
jobDataMap

JobDetail绑定指定的Job,每次Scheduler调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。

为什么设计成JobDetail + Job,不直接使用Job

JobDetail定义的是任务数据,而真正的执行逻辑是在Job中。
这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,Sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。

(2)JobExecutionContext
JobExecutionContext中包含了Quartz运行时的环境以及Job本身的详细数据信息。
当Schedule调度执行一个Job的时候,就会将JobExecutionContext传递给该Job的execute()中,Job就可以通过JobExecutionContext对象获取信息。
主要信息有:
这里写图片描述

(3)JobExecutionContext
JobDataMap实现了JDK的Map接口,可以以Key-Value的形式存储数据。
JobDetail、Trigger都可以使用JobDataMap来设置一些参数或信息,
Job执行execute()方法的时候,JobExecutionContext可以获取到JobExecutionContext中的信息:
如:

JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class) .usingJobData(“jobDetail1”, “这个Job用来测试的”)
.withIdentity(“job1”, “group1”).build();

Trigger trigger = TriggerBuilder.newTrigger().withIdentity(“trigger1”, “triggerGroup1”)
.usingJobData(“trigger1”, “这是jobDetail1的trigger”)
.startNow()//立即生效
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(1)//每隔1s执行一次
.repeatForever()).build();//一直执行

1
2
3
4
5
6
7
8
9
10

Job执行的时候,可以获取到这些参数信息:

@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {

    System.out.println(jobExecutionContext.getJobDetail().getJobDataMap().get("jobDetail1"));
    System.out.println(jobExecutionContext.getTrigger().getJobDataMap().get("trigger1"));
    String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date());
    System.out.println("PrintWordsJob start at:" + printTime + ", prints: Hello Job-" + new Random().nextInt(100));


}

1
2
3
4
5
6
7
8
9
10

(4)Trigger、SimpleTrigger、CronTrigger

Trigger

Trigger是Quartz的触发器,会去通知Scheduler何时去执行对应Job。

new Trigger().startAt():表示触发器首次被触发的时间;
new Trigger().endAt():表示触发器结束触发的时间;

1
2

SimpleTrigger
SimpleTrigger可以实现在一个指定时间段内执行一次作业任务或一个时间段内多次执行作业任务。
下面的程序就实现了程序运行5s后开始执行Job,执行Job 5s后结束执行:

Date startDate = new Date();
startDate.setTime(startDate.getTime() + 5000);

Date endDate = new Date();
endDate.setTime(startDate.getTime() + 5000);

    Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
            .usingJobData("trigger1", "这是jobDetail1的trigger")
            .startNow()//立即生效
            .startAt(startDate)
            .endAt(endDate)
            .withSchedule(SimpleScheduleBuilder.simpleSchedule()
            .withIntervalInSeconds(1)//每隔1s执行一次
            .repeatForever()).build();//一直执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

CronTrigger

CronTrigger功能非常强大,是基于日历的作业调度,而SimpleTrigger是精准指定间隔,所以相比SimpleTrigger,CroTrigger更加常用。CroTrigger是基于Cron表达式的,先了解下Cron表达式:
由7个子表达式组成字符串的,格式如下:

[秒] [分] [小时] [日] [月] [周] [年]

Cron表达式的语法比较复杂,
如:* 30 10 ? * 1/5 *
表示(从后往前看)
[指定年份] 的[ 周一到周五][指定月][不指定日][上午10时][30分][指定秒]

又如:00 00 00 ? * 10,11,12 1#5 2018
表示2018年10、11、12月的第一周的星期五这一天的0时0分0秒去执行任务。

下面是给的一个例子:
这里写图片描述

可通过在线生成Cron表达式的工具:http://cron.qqe2.com/ 来生成自己想要的表达式。
这里写图片描述

下面的代码就实现了每周一到周五上午10:30执行定时任务

/**

  • Created by wanggenshen

  • Date: on 2018/7/7 20:06.

  • Description: XXX
    */
    public class MyScheduler2 {
    public static void main(String[] args) throws SchedulerException, InterruptedException {
    // 1、创建调度器Scheduler
    SchedulerFactory schedulerFactory = new StdSchedulerFactory();
    Scheduler scheduler = schedulerFactory.getScheduler();
    // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容)
    JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class)
    .usingJobData(“jobDetail1”, “这个Job用来测试的”)
    .withIdentity(“job1”, “group1”).build();
    // 3、构建Trigger实例,每隔1s执行一次
    Date startDate = new Date();
    startDate.setTime(startDate.getTime() + 5000);

     Date endDate = new Date();
     endDate.setTime(startDate.getTime() + 5000);
    
     CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
             .usingJobData("trigger1", "这是jobDetail1的trigger")
             .startNow()//立即生效
             .startAt(startDate)
             .endAt(endDate)
             .withSchedule(CronScheduleBuilder.cronSchedule("* 30 10 ? * 1/5 2018"))
             .build();
    
     //4、执行
     scheduler.scheduleJob(jobDetail, cronTrigger);
     System.out.println("--------scheduler start ! ------------");
     scheduler.start();
     System.out.println("--------scheduler shutdown ! ------------");
    

    }
    }
    ————————————————
    版权声明:本文为CSDN博主「是Guava不是瓜娃」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/noaman_wgs/article/details/80984873

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值