提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
之前在翻看项目的时候突然看见定时器,因为一直用的@Scheduled,并没有怎么去接触过quartz,就突发奇想的去查询了一些两者的差别及优缺点。其中大部分博主都在说@Scheduled无法持久化,这点我是不认可的。
或许会相对麻烦一点,但是个人觉得因为麻烦而直接弃用@Scheduled转用其他第三方框架就为了完成定时器的持久化这两者的成本其实都差不多,而@Scheduled相对而言更便于使用,故就去琢磨了一套基于@Scheduled的持久化的方案。
另外在完成该方案的时候,又发现,该方案其实也可以完成@Scheduled的动态配置。
注:该文章不是科普,不是教程,中途遇到不懂的知识点请自行百度。如CronTrigger是什么?这种问题就别问我了。
一、@Scheduled是什么?
@Scheduled注解是Spring Boot提供的用于定时任务控制的注解,主要用于控制任务在某个指定时间执行,或者每隔一段时间执行。
具体的使用方法这里就不缀叙了,其他的文章都写的很详细了,还不会使用的可以自行搜索。
下面直接进入主题,上代码!
二、准备工作
1.重写CronTrigger
重写CronTrigger的目的仅仅只是为了方便使用,意义并不大,若介意的可以直接使用org.springframework.scheduling.support包下的CronTrigger。
代码如下:
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
public class CronTrigger {
private final String expression;
private final TimeZone timeZone;
private final BitSet months = new BitSet(12);
private final BitSet daysOfMonth = new BitSet(31);
private final BitSet daysOfWeek = new BitSet(7);
private final BitSet hours = new BitSet(24);
private final BitSet minutes = new BitSet(60);
private final BitSet seconds = new BitSet(60);
/**
* Construct a {@link CronTrigger} from the pattern provided, using the
* default {@link TimeZone}.
*
* @param expression a space-separated list of time fields
* @throws IllegalArgumentException if the pattern cannot be parsed
* @see TimeZone#getDefault()
*/
public CronTrigger(String expression) {
this(expression, TimeZone.getDefault());
}
/**
* Construct a {@link CronTrigger} from the pattern provided, using the
* specified {@link TimeZone}.
*
* @param expression a space-separated list of time fields
* @param timeZone the TimeZone to use for generated trigger times
* @throws IllegalArgumentException if the pattern cannot be parsed
*/
public CronTrigger(String expression, TimeZone timeZone) {
this.expression = expression;
this.timeZone = timeZone;
parse(expression);
}
/**
* Return the cron pattern that this sequence generator has been built for.
*/
String getExpression() {
return this.expression;
}
/**
* Get the next {@link Date} in the sequence matching the Cron pattern and
* after the value provided. The return value will have a whole number of
* seconds, and will be after the input value.
*
* @param date a seed value
* @return the next value matching the pattern
*/
public Date next(Date date) {
/*
* The plan:
*
* 1 Start with whole second (rounding up if necessary)
*
* 2 If seconds match move on, otherwise find the next match: 2.1 If
* next match is in the next minute then roll forwards
*
* 3 If minute matches move on, otherwise find the next match 3.1 If
* next match is in the next hour then roll forwards 3.2 Reset the
* seconds and go to 2
*
* 4 If hour matches move on, otherwise find the next match 4.1 If next
* match is in the next day then roll forwards, 4.2 Reset the minutes
* and seconds and go to 2
*/
Calendar calendar = new GregorianCalendar();
calendar.setTimeZone(this.timeZone);
calendar.setTime(date);
// First, just reset the milliseconds and try to calculate from there...
calendar.set(Calendar.MILLISECOND, 0);
long originalTimestamp = calendar.getTimeInMillis();
doNext(calendar, calendar.get(Calendar.YEAR));
if (calendar.getTimeInMillis() == originalTimestamp) {
// We arrived at the original timestamp - round up to the next whole
// second and try again...
calendar.add(Calendar.SECOND, 1);
doNext(calendar, calendar.get(Calendar.YEAR));
}
return calendar.getTime();
}
private void doNext(Calendar calendar, int dot) {
List<Integer> resets = new ArrayList<Integer>();
int second = calendar.get(Calendar.SECOND);
List<Integer> emptyList = Collections.emptyList();
int updateSecond = findNext(this.seconds, second, calendar, Calendar.SECOND, Calendar.MINUTE, emptyList);
if (second == updateSecond) {
resets.add(Calendar.SECOND);
}
int minute = calendar.get(Calendar.MINUTE);
int updateMinute = findNext(this.minutes, minute, calendar, Calendar.MINUTE, Calendar.HOUR_OF_DAY, resets);
if (minute == updateMinute) {
resets.add(Calendar.MINUTE);
} else {
doNext(calendar, dot);
}
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int updateHour = findNext(this.hours, hour, calendar, Calendar.HOUR_OF_DAY, Calendar.DAY_OF_WEEK, resets);
if (hour == updateHour) {
resets.add(Calendar.HOUR_OF_DAY);
} else {
doNext(calendar, dot);
}
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
int updateDayOfMonth = findNextDay(calendar, this.daysOfMonth, dayOfMonth, daysOfWeek, dayOfWeek, resets);
if (dayOfMonth == updateDayOfMonth) {
resets.add(Calendar.DAY_OF_MONTH);
} else {
doNext(calendar, dot);
}
int month = calendar.get(Calendar.MONTH);
int updateMonth = findNext(this.months, month, calendar, Calendar.MONTH, Calendar.YEAR, resets);
if (month != updateMonth) {
if (calendar.get(Calendar.YEAR) - dot > 4) {
throw new IllegalArgumentException("Invalid cron expression \"" + this.expression + "\" led to runaway search for next trigger");
}
doNext(calendar, dot);
}
}
private int findNextDay(Calendar calendar, BitSet daysOfMonth, int dayOfMonth, BitSet daysOfWeek, int dayOfWeek, List<Integer> resets) {
int count = 0;
int max = 366;
// the DAY_OF_WEEK values in java.util.Calendar start with 1 (Sunday),
// but in the cron pattern, they start with 0, so we subtract 1 here
while ((!daysOfMonth.get(dayOfMonth) || !daysOfWeek.get(dayOfWeek - 1)) && count++ < max) {
calendar.add(Calendar.DAY_OF_MONTH, 1);
dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
reset(calendar, resets);
}
if (count >= max) {
throw new IllegalArgumentException("Overflow in day for expression \"" + this.expression + "\"");
}
return dayOfMonth;
}
/**
* Search the bits provided for the next set bit after the value provided,
* and reset the calendar.
*
* @param bits a {@link BitSet} representing the allowed values of the field
* @param value the current value of the field
* @param calendar the calendar to increment as we move through the bits
* @param field the field to increment in the calendar (@see {@link Calendar}
* for the static constants defining valid fields)
* @param lowerOrders the Calendar field ids that should be reset (i.e. the ones of
* lower significance than the field of interest)
* @return the value of the calendar field that is next in the sequence
*/
private int findNext(BitSet bits, int value, Calendar calendar, int field, int nextField, List<Integer> lowerOrders) {
int nextValue = bits.nextSetBit(value);
// roll over if needed
if (nextValue == -1) {
calendar.add(nextField, 1);
reset(calendar, Arrays.asList(field));
nextValue = bits.nextSetBit(0);
}
if (nextValue != value) {
calendar.set(field, nextValue);
reset(calendar, lowerOrders);
}
return nextValue;
}
/**
* Reset the calendar setting all the fields provided to zero.
*/
private void reset(Calendar calendar, List<Integer> fields) {
for (int field : fields) {
calendar.set(field, field == Calendar.DAY_OF_MONTH ? 1 : 0);
}
}
// Parsing logic invoked by the constructor
/**
* Parse the given pattern expression.
*/
private void parse(String expression) throws IllegalArgumentException {
String[] fields = expression.split(" ");
if (!areValidCronFields(fields)) {
throw new IllegalArgumentException(String.format("Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
}
setNumberHits(this.seconds, fields[0], 0, 60);
setNumberHits(this.minutes, fields[1], 0, 60);
setNumberHits(this.hours, fields[2], 0, 24);
setDaysOfMonth(this.daysOfMonth, fields[3]);
setMonths(this.months, fields[4]);
setDays(this.daysOfWeek, replaceOrdinals(fields[5], "SUN,MON,TUE,WED,THU,FRI,SAT"), 8);
if (this.daysOfWeek.get(7)) {
// Sunday can be represented as 0 or 7
this.daysOfWeek.set(0);
this.daysOfWeek.clear(7);
}
}
/**
* Replace the values in the comma-separated list (case insensitive) with
* their index in the list.
*
* @return a new String with the values from the list replaced
*/
private String replaceOrdinals(String value, String commaSeparatedList) {
String[] list = commaSeparatedList.split(",");
for (int i = 0; i < list.length; i++) {
String item = list[i].toUpperCase();
value = value.toUpperCase().replace(item, "" + i);
}
return value;
}
private void setDaysOfMonth(BitSet bits, String field) {
int max = 31;
// Days of month start with 1 (in Cron and Calendar) so add one
setDays(bits, field, max + 1);
// ... and remove it from the front
bits.clear(0);
}
private void setDays(BitSet bits, String field, int max) {
if (field.contains("?")) {
field = "*";
}
setNumberHits(bits, field, 0, max);
}
private void setMonths(BitSet bits, String value) {
int max = 12;
value = replaceOrdinals(value, "FOO,JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC");
BitSet months = new BitSet(13);
// Months start with 1 in Cron and 0 in Calendar, so push the values
// first into a longer bit set
setNumberHits(months, value, 1, max + 1);
// ... and then rotate it to the front of the months
for (int i = 1; i <= max; i++) {
if (months.get(i)) {
bits.set(i - 1);
}
}
}
private void setNumberHits(BitSet bits, String value, int min, int max) {
String[] fields = value.split(",");
for (String field : fields) {
if (!field.contains("/")) {
// Not an incrementer so it must be a range (possibly empty)
int[] range = getRange(field, min, max);
bits.set(range[0], range[1] + 1);
} else {
String[] split = field.split("/");
if (split.length > 2) {
throw new IllegalArgumentException("Incrementer has more than two fields: '" + field + "' in expression \"" + this.expression + "\"");
}
int[] range = getRange(split[0], min, max);
if (!split[0].contains("-")) {
range[1] = max - 1;
}
int delta = Integer.valueOf(split[1]);
if (delta <= 0) {
throw new IllegalArgumentException("Incrementer delta must be 1 or higher: '" + field + "' in expression \"" + this.expression + "\"");
}
for (int i = range[0]; i <= range[1]; i += delta) {
bits.set(i);
}
}
}
}
private int[] getRange(String field, int min, int max) {
int[] result = new int[2];
if (field.contains("*")) {
result[0] = min;
result[1] = max - 1;
return result;
}
if (!field.contains("-")) {
result[0] = result[1] = Integer.valueOf(field);
} else {
String[] split = field.split("-");
if (split.length > 2) {
throw new IllegalArgumentException("Range has more than two fields: '" + field + "' in expression \"" + this.expression + "\"");
}
result[0] = Integer.valueOf(split[0]);
result[1] = Integer.valueOf(split[1]);
}
if (result[0] >= max || result[1] >= max) {
throw new IllegalArgumentException("Range exceeds maximum (" + max + "): '" + field + "' in expression \"" + this.expression + "\"");
}
if (result[0] < min || result[1] < min) {
throw new IllegalArgumentException("Range less than minimum (" + min + "): '" + field + "' in expression \"" + this.expression + "\"");
}
if (result[0] > result[1]) {
throw new IllegalArgumentException("Invalid inverted range: '" + field + "' in expression \"" + this.expression + "\"");
}
return result;
}
/**
* Determine whether the specified expression represents a valid cron
* pattern.
* <p>
* Specifically, this method verifies that the expression contains six
* fields separated by single spaces.
*
* @param expression the expression to evaluate
* @return {@code true} if the given expression is a valid cron expression
* @since 4.3
*/
public static boolean isValidExpression(String expression) {
String[] fields = expression.split(" ");
return areValidCronFields(fields);
}
private static boolean areValidCronFields(String[] fields) {
return (fields != null && fields.length == 6);
}
public static String format(LocalDateTime date) {
return date.format(DateTimeFormatter.ofPattern("ss mm HH dd MM *"));
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof CronTrigger)) {
return false;
}
CronTrigger otherCron = (CronTrigger) other;
return (this.months.equals(otherCron.months) && this.daysOfMonth.equals(otherCron.daysOfMonth) && this.daysOfWeek.equals(otherCron.daysOfWeek) && this.hours.equals(otherCron.hours) && this.minutes.equals(otherCron.minutes) && this.seconds.equals(otherCron.seconds));
}
@Override
public int hashCode() {
return (17 * this.months.hashCode() + 29 * this.daysOfMonth.hashCode() + 37 * this.daysOfWeek.hashCode() + 41 * this.hours.hashCode() + 53 * this.minutes.hashCode() + 61 * this.seconds.hashCode());
}
@Override
public String toString() {
return getClass().getSimpleName() + ": " + this.expression;
}
}
2.job实体类
代码如下:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import net.sf.json.JSONObject;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
@Data
@ApiModel(value = "TaskJob对象", description = "TaskJob对象")
public class TaskJob implements Serializable {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
private String jobName; // 任务名称
private String className; // 定时任务类名
private String methodName; // 定时任务方法名
private String cron; // 定时任务cron表达式
private Integer status; // 定时任务状态 这里是简单的demo版本,正式使用建议使用枚举类
private LocalDateTime createTime; //创建时间
private LocalDateTime doTime; //执行时间
@TableField(exist = false)
private Map<String, String> params; //定时任务中,执行方法或dao层操作中可能用到参数
private String paramsJson; //参数json字符串
public Map<String, String> getParams() {
return this.params == null ? (Map<String, String>) JSONObject.fromObject(this.paramsJson) : this.params;
}
public String getParamsJson() {
return this.paramsJson == null ? JSONObject.fromObject(this.params).toString() : this.paramsJson;
}
public String getCron() {
return this.cron == null ? this.doTime.format(DateTimeFormatter.ofPattern("ss mm HH dd MM *")) : this.cron;
}
public LocalDateTime getDoTime() {
return this.doTime == null ? LocalDateTime.parse(this.cron, DateTimeFormatter.ofPattern("ss mm HH dd MM *")) : this.doTime;
}
}
3.@Scheduled的持久化及动态设置
因为在完成持久化的过程中,发现也可以完成动态设置,就一起完成了。这里就不将代码分开了,原理是一样的,如果分开写的话,怕会有部分人迷糊以至于代码拷过去确无法实现。
import com.xxx.demo.TaskJob;
import org.apache.log4j.Logger;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.*;
public class ScheduleUtils {
private ScheduleUtils() {
}
// task集合
private static final Map<String, Task> TASK_MANAGER = new HashMap<String, Task>();
// 定时器线程池
private static final ScheduledExecutorService EXECUTOR_POOL = Executors.newScheduledThreadPool(6);
// 定时任务队列
private static final BlockingQueue<Task> TASK_QUEUE = new LinkedBlockingQueue<Task>();
private final static Logger logger = Logger.getLogger(ScheduleUtils.class);
// 静态初始化方法
static {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
//while (true) 循环执行列表中的task
while (true) {
try {
Task task = TASK_QUEUE.take();
// 任务有效,则执行任务
if (task.isEffective()) {
task.execute();
}
} catch (Exception e) {
logger.error("定时任务执行异常:", e);
}
}
}
});
executor.shutdown();
}
/**
* @param taskJob
* @throws Exception
* @Title: add
* @Description: 添加定时任务
*/
public synchronized static void add(TaskJob taskJob) throws Exception {
cancel(taskJob); // 终结执行中的任务
Task task = new Task(TASK_QUEUE, EXECUTOR_POOL, taskJob.getClassName(), taskJob.getMethodName(), taskJob.getCron(), taskJob);
TASK_MANAGER.put(taskJob.getJobName(), task);
// 将任务加入队列
TASK_QUEUE.put(task);
}
/**
* @param taskJob
* @Title: cancel
* @Description: 取消任务
*/
public synchronized static void cancel(TaskJob taskJob) {
if (taskJob == null) {
return;
}
String jobName = taskJob.getJobName();
if (jobName == null) {
return;
}
Task task = TASK_MANAGER.get(jobName);
if (task != null) {
// 关闭任务,停止任务线程
task.setEffective(false);
ScheduledFuture<?> future = task.getFuture();
if (future != null) {
future.cancel(true);
}
}
TASK_MANAGER.remove(jobName);
}
/**
* @ClassName: Task
* @Description: 任务内部类
*/
private static class Task {
private BlockingQueue<Task> queue; // 任务队列
private CronTrigger trigger; // cron触发器
private ScheduledExecutorService executor; // 定时器线程池
private Class<?> clazz; // 反射类名
private Object targetObject; // 反射对象
private Method method; // 反射方法
private Task self; // task对象自己
private ScheduledFuture<?> future; // task对象的future
private boolean effective = true; // task对象状态
private TaskJob taskJob;
private final static Logger logger = Logger.getLogger(Task.class);
public Task(BlockingQueue<Task> queue, ScheduledExecutorService executor, String className, String methodName, String cron, TaskJob taskJob) throws Exception {
this.queue = queue;
this.executor = executor;
this.trigger = new CronTrigger(cron);
this.clazz = Class.forName(className);
this.targetObject = clazz.newInstance();
this.method = clazz.getDeclaredMethod(methodName, TaskJob.class);
this.self = this;
this.taskJob = taskJob;
}
public void execute() throws Exception {
Date now = new Date();
long delay = trigger.next(now).getTime() - now.getTime(); // 等待时间
this.future = executor.schedule(new Runnable() {
@Override
public void run() {
try {
method.invoke(targetObject, taskJob);
} catch (Exception e) {
logger.error("定时任务执行异常:", e);
} finally {
// 把当前任务加入队列
try {
queue.put(self);
} catch (InterruptedException e) {
logger.error("添加定时任务到队列异常:", e);
}
}
}
}, delay, TimeUnit.MILLISECONDS);
}
public ScheduledFuture<?> getFuture() {
return future;
}
public boolean isEffective() {
return effective;
}
public void setEffective(boolean effective) {
this.effective = effective;
}
}
}
import com.xxx.demo.TaskJobAggregateService;
import com.xxx.demo.TaskJob;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.*;
@Component
public class TaskUtils {
private final static Logger logger = Logger.getLogger(TaskUtils.class);
private static Map<String, TaskJob> jobs = Collections.synchronizedMap(new TreeMap<String, TaskJob>()); // 定时任务集合
//定时任务
private static TaskJobAggregateService taskJobAggregateService;
@Autowired
public void setTaskJobAggregateService(TaskJobAggregateService taskJobAggregateService) {
TaskUtils.taskJobAggregateService = taskJobAggregateService;
}
public static void init() {
logger.info("定时任务初始化");
List<TaskJob> list = taskJobAggregateService.queryInit();
for (TaskJob taskJob : list) {
jobs.put(taskJob.getJobName(), taskJob);
}
new Thread(() -> {
for (Map.Entry<String, TaskJob> entry : jobs.entrySet()) {
TaskJob taskJob = entry.getValue();
try {
if (taskJob.getStatus() == 1) {
ScheduleUtils.add(taskJob);
}
} catch (Exception e) {
logger.error("定时任务初始化异常", e);
}
}
}).start();
}
public static void add(TaskJob taskJob) {
logger.info("增加定时任务");
String key = taskJob.getJobName();
if (taskJob.getStatus() == null) {
taskJob.setStatus(0);
}
try {
jobs.put(key, taskJob);
ScheduleUtils.cancel(taskJob);
if (taskJob.getStatus() == 1) {
// 开始执行定时任务
ScheduleUtils.add(taskJob);
}
try {
taskJobAggregateService.saveOrUpdate(taskJob);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
logger.error("操作失败:", e);
}
}
public static void remove(TaskJob taskJob) {
logger.info("取消定时任务");
String key = taskJob.getJobName();
try {
ScheduleUtils.cancel(taskJob);
if (key != null && jobs.containsKey(key)) {
jobs.remove(taskJob.getJobName());
}
try {
taskJobAggregateService.removeByJobName(key);
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
logger.error("删除定时任务异常:", e);
}
}
public void everyDay(TaskJob taskJob) {
List<String> doList = new ArrayList<>();
List<String> keys = new ArrayList<>();
for (String s : jobs.keySet()) {
keys.add(s);
}
for (String key : keys) {
TaskJob job = jobs.get(key);
if (!"每日去除已执行的任务".equals(job.getJobName()) && job.getDoTime().isBefore(LocalDateTime.now())) {
job.setStatus(2);
ScheduleUtils.cancel(job);
jobs.remove(key);
doList.add(job.getJobName());
}
}
if (doList.size() > 0) {
try {
taskJobAggregateService.updateDoByJobName(doList);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
这里放上queryInit的内容,请根据自己的项目需求进行修改
public List<TaskJob> queryInit() {
List<TaskJob> list = taskJobRepository.list(new QueryWrapper<TaskJob>().eq("status", 1).gt("do_time", LocalDateTime.now()));
List<TaskJob> list1 = omsTaskJobRepository.list(new QueryWrapper<TaskJob>().eq("status", 1).le("do_time", LocalDateTime.now()));
for (TaskJob taskJob : list1) {
taskJob.setCron(null);
taskJob.setDoTime(LocalDateTime.now().plusSeconds(30));
}
list.addAll(list1);
TaskJob taskJob = new TaskJob();
taskJob.setCron("00 00 02 * * *");
taskJob.setJobName("每日去除已执行的任务");
taskJob.setClassName(TaskUtils.class.getName());
taskJob.setMethodName("everyDay");
taskJob.setStatus(1);
list.add(taskJob);
return list;
}
4.持久化的实现
import com.xxx.demo.TaskUtils;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
@Configuration
@EnableDiscoveryClient
@MapperScan(basePackages = "com.xxx.demo.mapper.*")
@ServletComponentScan
public class Application {
public static void main(String[] args) {
try {
SpringApplication.run(Application.class, args);
TaskUtils.init();
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结
如此就能完成基于@Scheduled的定时任务的持久化及动态设置了,不过要注意的是,目前该demo仅能实现固定时间点定时任务,如果需要完成诸如@Scheduled(* * 1 * * ?)的情况,可以通过该基础上进行略微修改就能达成,具体实现如有时间,后续会出新的文章说说。