定时任务,是指定一个未来的时间范围执行一定任务的功能。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)如下: 

 

#============================================================================
 # Configure Main Scheduler Properties  
 #============================================================================
 #用在 JDBC JobStore 中来唯一标识实例,但是所有集群节点中必须相同。
 org.quartz.scheduler.instanceName = ZWorkCRMScheduler
 #基于主机名和时间戳来产生实例 ID       
 org.quartz.scheduler.instanceId = AUTO   #============================================================================
 # Configure ThreadPool  
 #============================================================================
 org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
 org.quartz.threadPool.threadCount = 10
 org.quartz.threadPool.threadPriority = 5#============================================================================
 # Configure JobStore  
 #============================================================================
 #最大触发超时时间(毫秒),如果超过则认为“失误”
 org.quartz.jobStore.misfireThreshold = 60000
 #将schedule相关信息保存在RDB中
 org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
 #
 #类似于Hibernate的dialect,用于处理DB之间的差异
 org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
 #存储相关信息表的前缀
 org.quartz.jobStore.tablePrefix = QRTZ_  
 #jobStore处理未按时触发的Job的数量
 org.quartz.jobStore.maxMisfiresToHandleAtATime=10
 #Scheduler实例参与到集群中
 org.quartz.jobStore.isClustered = true 
 #scheduler的checkin时间,时间长短影响failure scheduler的发现速度
 org.quartz.jobStore.clusterCheckinInterval = 15000

定时任务的启动一般在系统启动时加载,web应用中可以通过在web.xml中配置一个监听器,如下:

...
 <listener>
   <listener-class>cn.zwork.eap.time.TimeListener
   </listener-class>                                                                                                                                                
  </listener>
   <context-param>
   <param-name>timedjobConfigLocation</param-name>
   <param-value>/timedjob.xml</param-value>
  </context-param>...

 监听器类cn.zwork.eap.time.TimeListener的主要作用是初始化定时任务,其中参数timedjobConfigLocation的值/timedjob.xml包含了需要定时的任务job的一些参数信息。timedjob.xml文件配置如下:

<?xml version="1.0" encoding="UTF-8"?>                                                                                                                                                      
 <timedjobs>
 <system>
 <job key="CUSTOMER" bean="customerJob" cron="0 0 3 * * ?"/>
 <job key="FILECLEAN" bean="fileCleanJob" cron="0 0 2 * * ?"/>
 </system> <simple>
</simple>
</timedjobs>

监听器类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我们提供了一个接口,如下

public interface TimedJob {
 public void execute(JSONObject paramters);
 }

可以通过spring容器,将任务bean的实现类注入,实现类可以包含具体的任务执行的业务逻辑。实现类代码如下

public class SmsJob implements TimedJob {
     private static Log log = Log.getLog(SmsJob.class.getName());
     private SmsEntryBiz smsEntryBiz;
     ...
  public SmsJob() {
  }    @Override
     public void execute(JSONObject paramters) {
         // 定时发送短信
         log.debug("[TIMING_SMS_SEND BEGIN]:" + new Date());
         try{
          smsEntryBiz.modifySmsStatus(paramters);//修改短信状态为"已发送"
         }catch(Exception e){
          log.debug("[SmsJob.class Error]:" + new Date());
         }
         log.debug("[TIMING_SMS_SEND END]:" + new Date());
     }
 }

当程序调用了该发送短信的方法后,比如设置了在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实现多任务动态加载 - 随影求是的个人空间