定时任务,是指定一个未来的时间范围执行一定任务的功能。windows操作系统把它叫做任务计划,linux中cron服务都提供了这个功能。在我们开发业务系统中也会涉及到这个功能,java的quartz组件库就提供了这个功能。下面我以quartz为例来介绍一下定时任务的使用。
quartz组件的使用方法很多,网上可以找到很多资料,这里我们对quartz组件进行了一些包装、简化,形成自己的定时任务组件库。
我们先来看一个实际应用场景,我们需要在2020-01-01 10:00(这个未来时间)发送一个短信,使用我们的定时任务组件基础设施,通过程序调用该组件,定时任务组件会将这个定时任务保存到数据库表中,执行的最终结果是:在那个时间点会发出这个短信,发出短信后,这个定时任务就会删除。这就是一个定时任务从定制到最终执行的整个过程。下面我们谈谈如何来建立我们的组件基础设施,并正确地使用它。
使用经验
环境准备:引入quartz-1.8.6.jar,相关数据库表(QRTZ_BLOB_TRIGGERS,QRTZ_CALENDARS,QRTZ_CRON_TRIGGERS,QRTZ_FIRED_TRIGGERS,QRTZ_JOB_DETAILS,QRTZ_JOB_LISTENERS,
QRTZ_LOCKS,QRTZ_PAUSED_TRIGGER_GRPS,QRTZ_SCHEDULER_STATE,QRTZ_SIMPLE_TRIGGERS,QRTZ_TRIGGER_LISTENERS),配置文件quartz.properties
需要注意,在web应用的classpath路径下默认配置一个quartz.properties,这是quartz组件读取相关参数进行环境设置的文件,可参考(http://vowtree.iteye.com/blog/850913)如下:
|
定时任务的启动一般在系统启动时加载,web应用中可以通过在web.xml中配置一个监听器,如下:
|
监听器类cn.zwork.eap.time.TimeListener的主要作用是初始化定时任务,其中参数timedjobConfigLocation的值/timedjob.xml包含了需要定时的任务job的一些参数信息。timedjob.xml文件配置如下:
|
监听器类cn.zwork.eap.time.TimeListener实现代码如下:
public class TimeListener implements ServletContextListener {
private static final Log log=Log.getLog(TimeListener.class);
private static final String TIMEDJOB_CONFIG = "timedjobConfigLocation";
@Override
public void contextDestroyed(ServletContextEvent arg0) {
}
@Override
public void contextInitialized(ServletContextEvent event) {
TimeBiz timebiz = (TimeBiz) ApplicationContextAccessor
.getBean("timeBiz");
ServletContext context = event.getServletContext();
String xmlfile = context.getInitParameter(TIMEDJOB_CONFIG);
if (xmlfile != null && xmlfile.length() > 0) {
// 读取配置文件
try {
readJobList(timebiz,xmlfile);
} catch (Exception e) {
log.error("read timedjob config file error!!!");
e.printStackTrace();
}
}
timebiz.startScheduler();
}
private void readJobList(TimeBiz timebiz, String path)
throws FileNotFoundException, JSONException {
InputStream input = this.getClass().getResourceAsStream(path);
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(input);
Element timedjobs = document.getRootElement();
// 读取系统定时任务
Element system = timedjobs.element("system");
if (system != null) {
List<Element> systemNodes = system.elements("job");
if(systemNodes!=null){
for (Element element : systemNodes) {
addJob(timebiz, element, false);
}
}
}
// 读取简单定时任务
Element simple = timedjobs.element("simple");
if (simple != null) {
List<Element> simpleNodes = simple.elements("job");
if(simpleNodes!=null){
for (Element element : simpleNodes) {
addJob(timebiz, element, true);
}
}
}
} catch (DocumentException e) {
log.error("read timedjob config file error!!!"+e.getMessage());
e.printStackTrace();
}
}
private void addJob(TimeBiz timebiz, Element job, boolean isSimple)
throws JSONException {
String key = job.attribute("key").getValue();
String beanname = job.attribute("bean").getValue();
String cronExpress = job.attribute("cron").getValue();
Element paramter = job.element("paramters");
JSONObject json = null;
if (paramter != null) {
json = new JSONObject(paramter.getText());
}
if (isSimple)
timebiz.scheduleAsSimple(key.trim(), beanname.trim(),
cronExpress.trim(), json);
else
timebiz.scheduleAsSystem(key.trim(), beanname.trim(),
cronExpress.trim(), json);
}
}
其中涉及一个定时任务处理接口TimeBiz,通过spring配置bean容器加载实现类,实现其中的方法。
public interface TimeBiz {
/**
* 启动Quartz调度器
*/
public void startScheduler();
/**
* 工作任务计划设置
* 一次性执行的工作任务
* @param jobkey 工作任务key
* @param beanname 工作任务的spring容器bean名
* @param date 工作任务触发时间
* @param jsonstr 工作任务参数,可以json格式
* @throws JSONException
*/
void schedule(String jobkey,String beanname,Date date,String jsonstr) throws JSONException;
/**
* 工作任务计划设置
* 指定cron表达式时间规则执行的工作任务
* @param jobkey 工作任务key
* @param job 工作任务的类名
* @param cronExpression 工作任务计划的cron表达式
* @param jsonstr 工作任务参数,可以json格式
* @throws JSONException
*/
void schedule(String jobkey,Class<TimedJob> job,String cronExpression,String jsonstr) throws JSONException;
}
说明:其中startScheduler();方法是在启动quartz组件时执行的调度器初始化,后面2个方法是需要执行定时任务调用的方法,也可以根据实际需要进行扩展,可以用作相关业务定时任务的工具方法。
上面说了许多定时任务组件基础设施,下面看看具体如何调用,看一个调用定时发送短信的例子。
public class SmsEntryBizImpl implements SmsEntryBiz {
private TimeBiz timeBiz;
...
public void addSms(Sms sms, List<SmsRelation> smsRelationList)
throws BizException {
String id = sms.getId();//短信id
Date sendTime = sms.getTimeSendDate();//短信发送时间
String jsonParams="{id:'"+id+"'}";
//开始设置定时任务
timeBiz.schedule("timed-sms"+id, "smsJob", sendTime,jsonParams);//这里就调用了timeBiz的定时任务工具方法
}...
}
说明:主要调用的方法schedule,有几个参数:任务id、任务job的bean名、发送时间、任务参数。任务bean我们提供了一个接口,如下
|
可以通过spring容器,将任务bean的实现类注入,实现类可以包含具体的任务执行的业务逻辑。实现类代码如下
|
当程序调用了该发送短信的方法后,比如设置了在2020-01-01 10:00(这个未来时间)发送一个短信,quartz组件会将这个定时任务持久化到数据库表中,具体表名为QRTZ_TRIGGERS,先看看存储的这条记录数据。如下:
TRIGGER_timed-sms14 zcrm_develop JOB_timed-sms14 zcrm_develop 0 1340356380000 -1 5 WAITING SIMPLE 1340356380000 0 0
建表语句如下:
CREATE TABLE `QRTZ_TRIGGERS` (
`TRIGGER_NAME` varchar(200) NOT NULL,
`TRIGGER_GROUP` varchar(200) NOT NULL,
`JOB_NAME` varchar(200) NOT NULL,
`JOB_GROUP` varchar(200) NOT NULL,
`IS_VOLATILE` varchar(1) NOT NULL,
`DESCRIPTION` varchar(250) DEFAULT NULL,
`NEXT_FIRE_TIME` bigint(13) DEFAULT NULL,
`PREV_FIRE_TIME` bigint(13) DEFAULT NULL,
`PRIORITY` int(11) DEFAULT NULL,
`TRIGGER_STATE` varchar(16) NOT NULL,
`TRIGGER_TYPE` varchar(8) NOT NULL,
`START_TIME` bigint(13) NOT NULL,
`END_TIME` bigint(13) DEFAULT NULL,
`CALENDAR_NAME` varchar(200) DEFAULT NULL,
`MISFIRE_INSTR` smallint(2) DEFAULT NULL,
`JOB_DATA` blob,
PRIMARY KEY (`TRIGGER_NAME`,`TRIGGER_GROUP`),
KEY `JOB_NAME` (`JOB_NAME`,`JOB_GROUP`),
CONSTRAINT `QRTZ_TRIGGERS_ibfk_1` FOREIGN KEY (`JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`JOB_NAME`, `JOB_GROUP`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这个定时任务短信发送预期最终结果是,在上述那个时间会正确的发送这个短信,发送完后,这个数据记录也会自动从这个表记录中删除。这就是一个定时任务从定制到最终执行的整个过程。
问题集
问题:修改了任务逻辑,测试却发现没有按修改后的逻辑执行。
答:使用的定时任务数据库为公用的,是多租户应用端公用的,如果只有1个应用端修改了任务逻辑,其他应用端可能会取得定时任务执行权,使用定时任务公用数据库,这样就不能保证是那个已修改任务逻辑的应用端执行定时任务,所以不能得到预期结果。我们可以用这种方法来解决:1)我们可以先建立一个调试环境,即本地建立一个定时任务数据库,本地代码修改连接到这个数据库,然后修改任务逻辑,进行测试,若得到正确的结果后,提交最新程序,更新各个应用端代码;2)最后就可以恢复原先的定时任务数据库的连接,保证各个应用端程序是同一个程序,这样就可以让任务逻辑按照预期结果正确执行了。
参考资料
用quartz实现多任务动态加载 - 随影求是的个人空间