定時任務的Java實現
就是計劃任務啦,只是在項目中這樣叫也就習慣了,參考項目中大神的實現。
目的:通過MySQL配置,可以從MySQL中讀取參數,按時定時啓動和關閉。數據庫記錄字段包括實現類名(默認爲jobName、jobGroupName、triggerName、triggerGroupName),創建時間,調度規則(cron表達式),啓動標誌,啓動參數。
實現:quartz,與Strus2管理Action的思路相似(ActionMapping->ActionProxy->Action)。對於多節點的考慮,也是通過對數據庫記錄的查詢、添加、刪除實現Lock(數據庫記錄是唯一的)。
相關的類有5個
類 | 備註 |
---|---|
JobTimerController | 適配原有項目結構,啓動調度器執行定時任務 |
JobScheduler | 靜態調度器類,管理Job列表,對Job的CQUD操作 |
JobProxy | IJob的代理類,每個任務對應一個線程實例,執行定時任務 |
JobLock | 靜態鎖,防止多節點異步執行任務,基於數據庫實現 |
IJob | 函數接口,定時任務需要基於此實現 |
類之間的關係如下
下面說說大體思路如何實現
1.定時任務調度器啓動類JobTimerController
注意resultList、jobList只是保存Job的description(Map形式),但resultList是初始啓動配置,jobList是內存中正運行的任務,而JobScheduler纔是實際對IJob子類(相關任務實例)的操作(啓動中斷等)。因此,當數據庫中添加了新的定時任務時,需要啓動該類通知調度器類JobScheduler執行新的任務(本身該類該類也要當作定時任務執行才合理嘛)。
public void work(){
List resultList = /* 從數據庫中獲取任務 */
ArrayList jobList = JobScheduler.getJobList();
// 比較resultList與jobList
//調度器已經運行在內存中且已已有任務在執行
if (!ArrayUtils.isEmpty(jobList.toArray())){
for(int i=0;i<jobList.size();i++){
jobMap = (HashMap) jobList.get(i);
String jobName = jobMap.get("jobName").toString();
//resultList不存在,數據庫中無記錄則移除任務
if(!exist(jobName)){
//從quartz中刪除,調用Scheduler.deleteJob
JobScheduler.removeJob(jobMap);
//從調度器JobScheduler中刪除
jobList.remove(jobMap);
i--;
}
}
}
//配置好在數據庫中,啓動新的任務
if (!ArrayUtils.isEmpty(resultList.toArray())){
for(int i=0;i<resultList.size();i++){
jobMap = (HashMap) resultList.get(i);
String jobName = jobMap.get("jobName").toString();
//不在調度器中,則新建任務實例並啓動執行
if(!exist(jobName)){
//添加到調度器JobScheduler
jobList.add(jobMap);
//調用Scheduler.scheduleJob啓動定時任務
JobScheduler.addJob(jobMap);
}
//已存在則校驗啓動標誌等
else{
if( "Y".equals(jobMap.get("jobStart").toString()) )
JobScheduler.updateJob(jobMap);
}
}
}
}
2.定時任務調度器類JobSchduler
封裝了quartz的操作。
private static SchedulerFactory sf = new StdSchedulerFactory();
public static void addJob(HashMap jobMap) throws SchedulerException, ParseException, ClassNotFoundException, InstantiationException, IllegalAccessException {
String jobName = jobMap.get("jobName").toString();
String jobGroupName = jobMap.get("jobGroupName").toString();
String triggerName = jobMap.get("triggerName").toString();
String triggerGroupName = jobMap.get("triggerGroupName").toString();
String jobClass = jobMap.get("jobBussinessClass").toString();
String time = jobMap.get("cronExpression").toString();
Class tstxJobClass = Class.forName(baseTaskClassName);
Job tstxJob = (Job) tstxJobClass.newInstance();
Scheduler sched = sf.getScheduler();
JobDetail jobDetail = new JobDetail(jobName, jobGroupName,tstxJob.getClass());
jobDetail.getJobDataMap().put("jobMap", jobMap);
CronTrigger trigger = new CronTrigger(triggerName,triggerGroupName);
trigger.setCronExpression(time);
sched.scheduleJob(jobDetail,trigger);
//啓動
if(!sched.isShutdown()){
sched.start();
}
}
public static void updateJob(HashMap jobMap) throws SchedulerException, ParseException {
String triggerName = jobMap.get("triggerName").toString();
String triggerGroupName = jobMap.get("triggerGroupName").toString();
String time = jobMap.get("cronExpression").toString();
Scheduler sched = sf.getScheduler();
Trigger trigger = sched.getTrigger(triggerName, triggerGroupName);
if(trigger!=null){
CronTrigger ct = (CronTrigger) trigger;
// 修改時間
ct.setCronExpression(time);
// 重啓觸發器
sched.rescheduleJob(triggerName, triggerGroupName , ct);
}
}
public static void removeJob(HashMap jobMap) throws SchedulerException{
String jobName = jobMap.get("jobName").toString();
String jobGroupName = jobMap.get("jobGroupName").toString();
String triggerName = jobMap.get("triggerName").toString();
String triggerGroupName = jobMap.get("triggerGroupName").toString();
Scheduler sched = sf.getScheduler();
sched.pauseTrigger(triggerName, triggerGroupName);//停止觸發器
sched.unscheduleJob(triggerName, triggerGroupName);//移除觸發器
sched.deleteJob(jobName, jobGroupName);
}
public static void interruptJob(HashMap jobMap) throws SchedulerException{
String jobName = jobMap.get("jobName").toString();
String jobGroupName = jobMap.get("jobGroupName").toString();
String triggerName = jobMap.get("triggerName").toString();
String triggerGroupName = jobMap.get("triggerGroupName").toString();
Scheduler sched = sf.getScheduler();
sched.interrupt(triggerName, triggerGroupName);//中斷觸發器
}
3.定時任務代理類JobProxy
implements InterruptableJob,新開線程執行具體的定時任務
@Override
public void interrupt() throws UnableToInterruptJobException {
_interrupted = true;
}
@Override
public void execute(final JobExecutionContext context) throws JobExecutionException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
JobDetail jobDetail = context.getJobDetail();
HashMap jobMap = (HashMap)jobDetail.getJobDataMap().get("jobMap");
//鎖定任務
JobLock.Lock(jobMap);
//任務正在執行中, 中斷當前線程任務..
if (_interrupted) {
return;
}
try{
//創建具體的任務實現類並執行任務
String className = (String)jobMap.get("jobClass");
IBaseJob baseJob = Class.forName(className).newInstance();
baseJob.execute(context);
}catch (JobExecutionException e){
/* 顯示異常,打印日誌 */
}
//解鎖
finally {
JobLock.unLock(jobMap);
}
}
});
thread.start();
}
4.併發鎖JobLock
基於數據庫的實現,需要達到鎖的效果時,寫入相關信息到數據庫記錄,當數據庫中存在相應記錄時,則說明該任務已鎖,其中lockJobName爲PK。
public synchronized static void lock(HashMap jobMap){
//解析定時任務參數信息
String jobName = jobMap.get("jobName").toString();//任務名稱
String jobGroupName = jobMap.get("jobGroupName").toString();//任務組名
String lockJobName = "LOCK_" + jobName + "_" + jobGroupName;//redis鎖定job名稱
HashMap lockJobMap = /* 通過lockJobName從數據庫中獲取Lock信息 */
//未鎖
if (null == lockJobMap){
//嘗試將任務鎖定
boolean lock = /* 將lockJobName寫入到數據庫中 */
if (!lock){
//鎖定失敗, 不可執行定時任務
JobSchduler.interruptJob(jobMap);
return;
}
}
//已鎖,需判斷定時任務鎖定(過期)時間
else{
String jobStartTimeStr = lockJobMap.get("jobStartTime").toString();//任務執行時間
String lockTime = StringUtils.isEmpty(jobMap.get("lockTime").toString()) ? "600" : jobMap.get("lockTime").toString();//鎖定秒數
Date jobStartTimeData = null;
try {
jobStartTimeData = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(jobStartTimeStr);//轉換日期類型
} catch (ParseException e) {
e.printStackTrace();
}
Integer lockTimeInt = Integer.parseInt(lockTime);//計算鎖定時長
Date jobExceedDate = DateUtils.addSeconds(jobStartTimeData,lockTimeInt);//計算JOB過期時間
Boolean isJobExceed = jobExceedDate.before(new Date());
//鎖定時間(開始時間+鎖定時長)與當前時間比較
//過期不可執行任務
if(!isJobExceed){
JobSchduler.interruptJob(jobMap);
}
//未過期,重鎖
else{
unlock(jobMap);
//嘗試將任務鎖定
boolean lock = /* 將lockJobName寫入到數據庫中 */
if (!lock){
//鎖定失敗, 不可執行定時任務
JobSchduler.interruptJob(jobMap);
return;
}
}
}
}
public synchronized static void unlock(HashMap jobMap){
String jobName = jobMap.get("jobName").toString();//任務名稱
String jobGroupName = jobMap.get("jobGroupName").toString();//任務組名
String lockJobName = "LOCK_" + jobName + "_" +jobGroupName;//redis鎖定job名稱
/* 通過lockJobName更新數據庫中相應記錄狀態 */
}
5.定時任務接口類IJob
實現定時任務的具體邏輯,就是quartz要求的那個。
public interface IJob {
public void execute(JobExecutionContext context) throws JobExecutionException;
}