quartz是一个可以动态添加、管理定时任务的框架,因为业务需要,增加一种支持周XX执行的定时任务WeekdaysTrigger。
实现思路如下:首先定义WeekdaysTrigger接口:
public interface WeekdaysTrigger extends Trigger {
// 仅支持两种misfire补偿策略:忽略与立即执行
int MISFIRE_INSTRUCTION_FIRE_NOW = 1;
int getTimesTriggered();
void setTimesTriggered(int timesTriggered);
// 需要执行的周XX
Integer[] getWeekdays();
// 触发的时间点
String getTriggerTime();
// 定时器开始时间
Date getInitTime();
String getOtherParams();
}
接着定义WeekdaysTrigger的实现WeekdaysTriggerImpl
@Slf4j
public class WeekdaysTriggerImpl extends AbstractTrigger<WeekdaysTrigger> implements WeekdaysTrigger, CoreTrigger {
private static final int YEAR_TO_GIVEUP_SCHEDULING_AT = java.util.Calendar.getInstance().get(java.util.Calendar.YEAR) + 100;
// public static final int REPEAT_INDEFINITELY = -1;
protected Integer[] weekdays;
protected Date initTime;
// 定时器加载时的首次触发时间
protected Date startTime;
protected String triggerTime;
private int timesTriggered;
// 上一次触发时间、下一次触发时间
private Date previousFireTime, nextFireTime;
protected WeekdaysTriggerImpl(){}
/**
*
* @param weekdays 星期 x
* @param triggerTime 每日的触发时间点 HH:mm
* @param startTime 触发器开始时间
*/
public WeekdaysTriggerImpl(Integer weekdays[],
String triggerTime,
Date startTime) {
this.weekdays = weekdays;
this.triggerTime = triggerTime;
this.initTime = startTime;
try {
this.startTime = this.getNextFireTime(startTime);
} catch (ParseException e) {
e.printStackTrace();
log.error("WeekdaysTrigger 创建失败", e);
}
}
// 计算下一次触发时间
protected Date getNextFireTime(Date startTime) throws ParseException {
Date now = new Date();
Date sameDayTime = DateUtil.appendTime2Date(startTime, triggerTime);
if(now.compareTo(sameDayTime) > 0 || !judgeWeekdayLegal(startTime)) { // 如果当天时间已过 或者 非有效的星期x
Date nextLegalDate = null;
if(now.compareTo(startTime) > 0) { // 跳过无效的天
nextLegalDate = getNextLegalDate(now);
} else {
nextLegalDate = getNextLegalDate(startTime);
}
return DateUtil.appendTime2Date(nextLegalDate, triggerTime);
}
return sameDayTime;
}
/**
* 星期 X 的条件是否满足
* @param date
* @return
*/
protected Boolean judgeWeekdayLegal(Date date) {
int weekday = DateUtil.getWeekday(date);
return isWeekdayLegal(weekday);
}
/**
* 获取下一个满足星期条件的日期
* @param date
* @return
*/
protected Date getNextLegalDate(Date date) {
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.setTime(date);
calendar.add(java.util.Calendar.DATE, 1);
int weekday = calendar.get(java.util.Calendar.DAY_OF_WEEK);
while(!isWeekdayLegal(weekday)) {
calendar.add(java.util.Calendar.DATE, 1);
weekday = calendar.get(java.util.Calendar.DAY_OF_WEEK);
}
return calendar.getTime();
}
protected Boolean isWeekdayLegal(int weekday) {
for(Integer day: weekdays) {
if(day.intValue() == weekday) {
return true;
}
}
return false;
}
// 触发之后,马上计算下一次的触发时间
@Override
public void triggered(Calendar calendar) {
timesTriggered++;
previousFireTime = nextFireTime;
nextFireTime = getFireTimeAfter(nextFireTime);
while (nextFireTime != null && calendar != null
&& !calendar.isTimeIncluded(nextFireTime.getTime())) {
nextFireTime = getFireTimeAfter(nextFireTime);
if(nextFireTime == null)
break;
//avoid infinite loop
java.util.Calendar c = java.util.Calendar.getInstance();
c.setTime(nextFireTime);
if (c.get(java.util.Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
nextFireTime = null;
}
}
}
@Override
public Date computeFirstFireTime(Calendar calendar) {
nextFireTime = getStartTime();
while (nextFireTime != null && calendar != null
&& !calendar.isTimeIncluded(nextFireTime.getTime())) {
nextFireTime = getFireTimeAfter(nextFireTime);
if(nextFireTime == null)
break;
//avoid infinite loop
java.util.Calendar c = java.util.Calendar.getInstance();
c.setTime(nextFireTime);
if (c.get(java.util.Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
return null;
}
}
return nextFireTime;
}
@Override
public boolean mayFireAgain() {
return (this.getNextFireTime() != null);
}
@Override
public Date getStartTime() {
return this.startTime;
}
@Override
public void setStartTime(Date startTime) {
}
@Override
public void setEndTime(Date endTime) {
}
@Override
public Date getEndTime() {
return null;
}
@Override
public Date getNextFireTime() {
return nextFireTime;
}
@Override
public Date getPreviousFireTime() {
return previousFireTime;
}
@Override
public Date getFireTimeAfter(Date afterTime) {
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.setTime(afterTime);
calendar.add(java.util.Calendar.DATE, 1);
try {
return this.getNextFireTime(calendar.getTime());
} catch (ParseException e) {
log.error(e.getMessage() , e);
return null;
}
}
@Override
public Date getFinalFireTime() {
return null;
// return (getEndTime() == null) ? null : getFireTimeBefore(getEndTime());
}
@Override
protected boolean validateMisfireInstruction(int misfireInstruction) {
if (misfireInstruction < MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY) {
return false;
}
if (misfireInstruction > MISFIRE_INSTRUCTION_FIRE_NOW) {
return false;
}
return true;
}
// 目前支持 ignore 和 now 两种misfire处理方式
@Override
public void updateAfterMisfire(Calendar cal) {
int instr = getMisfireInstruction();
if(instr == Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY)
return;
setNextFireTime(new Date());
}
@Override
public void updateWithNewCalendar(Calendar calendar, long misfireThreshold) {
nextFireTime = getFireTimeAfter(previousFireTime);
if (nextFireTime == null || calendar == null) {
return;
}
Date now = new Date();
while (nextFireTime != null && !calendar.isTimeIncluded(nextFireTime.getTime())) {
nextFireTime = getFireTimeAfter(nextFireTime);
if(nextFireTime == null)
break;
//avoid infinite loop
java.util.Calendar c = java.util.Calendar.getInstance();
c.setTime(nextFireTime);
if (c.get(java.util.Calendar.YEAR) > YEAR_TO_GIVEUP_SCHEDULING_AT) {
nextFireTime = null;
}
if(nextFireTime != null && nextFireTime.before(now)) {
long diff = now.getTime() - nextFireTime.getTime();
if(diff >= misfireThreshold) {
nextFireTime = getFireTimeAfter(nextFireTime);
}
}
}
}
@Override
public void setNextFireTime(Date nextFireTime) {
this.nextFireTime = nextFireTime;
}
@Override
public void setPreviousFireTime(Date previousFireTime) {
this.previousFireTime = previousFireTime;
}
@Override
public ScheduleBuilder<WeekdaysTrigger> getScheduleBuilder() {
WeekdaysScheduleBuilder sb = new WeekdaysScheduleBuilder(weekdays, triggerTime, initTime);
switch(getMisfireInstruction()) {
case MISFIRE_INSTRUCTION_FIRE_NOW : sb.withMisfireHandlingInstructionFireNow();
break;
case MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY: sb.withMisfireHandlingInstructionIgnoreMisfires();
break;
}
return sb;
}
@Override
public int getTimesTriggered() {
return timesTriggered;
}
@Override
public void setTimesTriggered(int timesTriggered) {
this.timesTriggered = timesTriggered;
}
@Override
public boolean hasAdditionalProperties() {
return false;
}
@Override
public Integer[] getWeekdays() {
return weekdays;
}
@Override
public String getTriggerTime() {
return triggerTime;
}
@Override
public Date getInitTime() {
return this.startTime;
}
@Override
public String getOtherParams() {
return null;
}
}
接着增加WeekdaysScheduleBuilder类:
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.quartz.ScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.spi.MutableTrigger;
import java.util.Date;
/**
* @Author zhangyugu
* @Date 2021/7/24 12:10 下午
* @Version 1.0
*/
@Slf4j
public class WeekdaysScheduleBuilder extends ScheduleBuilder<WeekdaysTrigger> {
private Integer[] weekdays;
private String triggerTime;
private Date startTime;
private String otherParams;
private int misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_SMART_POLICY;
public WeekdaysScheduleBuilder(Integer[] weekdays, String triggerTime, Date startTime) {
this.weekdays = weekdays;
this.triggerTime = triggerTime;
this.startTime = startTime;
}
public WeekdaysScheduleBuilder(Integer[] weekdays, String triggerTime, String otherParams, Date startTime) {
this.weekdays = weekdays;
this.triggerTime = triggerTime;
this.startTime = startTime;
this.otherParams = otherParams;
}
@Override
public MutableTrigger build() {
if(StringUtils.isBlank(otherParams)) {
WeekdaysTriggerImpl weekdaysTrigger = new WeekdaysTriggerImpl(weekdays, triggerTime, startTime);
weekdaysTrigger.setMisfireInstruction(misfireInstruction);
return weekdaysTrigger;
} else {
SunSetRiseTriggerImpl sunSetRiseTrigger = new SunSetRiseTriggerImpl(weekdays, triggerTime, otherParams, startTime);
sunSetRiseTrigger.setMisfireInstruction(misfireInstruction);
return sunSetRiseTrigger;
}
}
/**
* If the Trigger misfires, use the
* {@link Trigger#MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY} instruction.
*
* @return the updated CronScheduleBuilder
* @see Trigger#MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
*/
public WeekdaysScheduleBuilder withMisfireHandlingInstructionIgnoreMisfires() {
misfireInstruction = Trigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY;
return this;
}
/**
* If the Trigger misfires, use the
* {@link SimpleTrigger#MISFIRE_INSTRUCTION_FIRE_NOW} instruction.
*
* @return the updated SimpleScheduleBuilder
* @see SimpleTrigger#MISFIRE_INSTRUCTION_FIRE_NOW
*/
public WeekdaysScheduleBuilder withMisfireHandlingInstructionFireNow() {
misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW;
return this;
}
}
最后是WeekdaysTrigger数据的实例化类WeekdaysTriggerPersistenceDelegate:
public class WeekdaysTriggerPersistenceDelegate implements TriggerPersistenceDelegate, StdJDBCConstants {
protected String tablePrefix;
protected String schedNameLiteral;
public void initialize(String theTablePrefix, String schedName) {
this.tablePrefix = theTablePrefix;
this.schedNameLiteral = "'" + schedName + "'";
}
@Override
public boolean canHandleTriggerType(OperableTrigger trigger) {
return trigger instanceof WeekdaysTriggerImpl;
}
public String getHandledTriggerTypeDiscriminator() {
return "WEEKDAYS";
}
@Override
public int insertExtendedTriggerProperties(Connection conn, OperableTrigger trigger, String state, JobDetail jobDetail) throws SQLException, IOException {
WeekdaysTrigger weekdaysTrigger = (WeekdaysTrigger) trigger;
PreparedStatement ps = null;
try {
ps = conn.prepareStatement(Util.rtp(
"INSERT INTO "
+ TABLE_PREFIX_SUBST + "WEEKDAYS_TRIGGERS" + " ("
+ COL_SCHEDULER_NAME + ", "
+ COL_TRIGGER_NAME + ", " + COL_TRIGGER_GROUP + ", "
+ "REPEAT_DAYS" + ", "
+ "TRIGGER_TIME, "
+ "OTHER_PROPS, "
+ "START_TIME, "
+ COL_TIMES_TRIGGERED + ") " + " VALUES(" + SCHED_NAME_SUBST + ", ?, ?, ?, ?, ?, ?, ?)"
, tablePrefix, schedNameLiteral));
ps.setString(1, trigger.getKey().getName());
ps.setString(2, trigger.getKey().getGroup());
ps.setString(3, IdSpliter.join(weekdaysTrigger.getWeekdays(), ","));
ps.setString(4, weekdaysTrigger.getTriggerTime());
ps.setString(5, weekdaysTrigger.getOtherParams());
ps.setTimestamp(6, new Timestamp(weekdaysTrigger.getInitTime().getTime()));
ps.setInt(7, weekdaysTrigger.getTimesTriggered());
return ps.executeUpdate();
} finally {
Util.closeStatement(ps);
}
}
@Override
public int updateExtendedTriggerProperties(Connection conn, OperableTrigger trigger, String state, JobDetail jobDetail) throws SQLException, IOException {
WeekdaysTrigger weekdaysTrigger = (WeekdaysTrigger) trigger;
PreparedStatement ps = null;
try {
ps = conn.prepareStatement(Util.rtp("UPDATE "
+ TABLE_PREFIX_SUBST + "WEEKDAYS_TRIGGERS" + " SET "
+ "REPEAT_DAYS" + " = ?, "
+ "TRIGGER_TIME" + " = ?, "
+ "OTHER_PROPS" + " = ?, "
+ "START_TIME" + " = ?, "
+ COL_TIMES_TRIGGERED + " = ? WHERE "
+ COL_SCHEDULER_NAME + " = " + SCHED_NAME_SUBST
+ " AND " + COL_TRIGGER_NAME
+ " = ? AND " + COL_TRIGGER_GROUP + " = ?",
tablePrefix, schedNameLiteral));
ps.setString(1, IdSpliter.join(weekdaysTrigger.getWeekdays(), ","));
ps.setString(2, weekdaysTrigger.getTriggerTime());
ps.setString(3, weekdaysTrigger.getOtherParams());
ps.setTimestamp(4, new Timestamp(weekdaysTrigger.getInitTime().getTime()));
ps.setInt(5, weekdaysTrigger.getTimesTriggered());
ps.setString(6, weekdaysTrigger.getKey().getName());
ps.setString(7, weekdaysTrigger.getKey().getGroup());
return ps.executeUpdate();
} finally {
Util.closeStatement(ps);
}
}
@Override
public int deleteExtendedTriggerProperties(Connection conn, TriggerKey triggerKey) throws SQLException {
PreparedStatement ps = null;
try {
ps = conn.prepareStatement(Util.rtp(
"DELETE FROM "
+ TABLE_PREFIX_SUBST + "WEEKDAYS_TRIGGERS" + " WHERE "
+ COL_SCHEDULER_NAME + " = " + SCHED_NAME_SUBST
+ " AND " + COL_TRIGGER_NAME + " = ? AND " + COL_TRIGGER_GROUP + " = ?"
, tablePrefix, schedNameLiteral));
ps.setString(1, triggerKey.getName());
ps.setString(2, triggerKey.getGroup());
return ps.executeUpdate();
} finally {
Util.closeStatement(ps);
}
}
@Override
public TriggerPropertyBundle loadExtendedTriggerProperties(Connection conn, TriggerKey triggerKey) throws SQLException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
ps = conn.prepareStatement(Util.rtp("SELECT *" + " FROM "
+ TABLE_PREFIX_SUBST + "WEEKDAYS_TRIGGERS" + " WHERE "
+ COL_SCHEDULER_NAME + " = " + SCHED_NAME_SUBST
+ " AND " + COL_TRIGGER_NAME + " = ? AND " + COL_TRIGGER_GROUP + " = ?",
tablePrefix, schedNameLiteral));
ps.setString(1, triggerKey.getName());
ps.setString(2, triggerKey.getGroup());
rs = ps.executeQuery();
if (rs.next()) {
String weekdays = rs.getString("REPEAT_DAYS");
String triggerTime = rs.getString("TRIGGER_TIME");
Timestamp startTime = rs.getTimestamp("START_TIME");
String otherParams = rs.getString("OTHER_PROPS");
int timesTriggered = rs.getInt(COL_TIMES_TRIGGERED);
String[] statePropertyNames = { "timesTriggered" };
Object[] statePropertyValues = { timesTriggered };
return new TriggerPropertyBundle(new WeekdaysScheduleBuilder(IdSpliter.splitStr2IntArr(weekdays, ","), triggerTime, otherParams, startTime), statePropertyNames, statePropertyValues);
}
throw new IllegalStateException("No record found for selection of Trigger with key: '" + triggerKey + "' and statement: " + Util.rtp(SELECT_SIMPLE_TRIGGER, tablePrefix, schedNameLiteral));
} finally {
Util.closeResultSet(rs);
Util.closeStatement(ps);
}
}
}