一、例子
下面是一个简单的例子,它表示每分钟执行一次
<dependency>
<groupId>it.sauronsoftware.cron4j</groupId>
<artifactId>cron4j</artifactId>
<version>2.2.5</version>
</dependency>
public class Test {
public static void main(String[] args) {
//创建调度器
Scheduler scheduler = new Scheduler();
//设置需要调度的任务
scheduler.schedule("*/1 * * * *", new Task() {
@Override
public void execute(TaskExecutionContext context) throws RuntimeException {
System.out.println("hello world!");
}
});
//启动调度
scheduler.start();
}
}
cron4j的cron表达式分别表示 分 时 日 月 周
二、类介绍
2.1 TaskCollector
/**
* @author Carlo Pelliccia
* @since 2.0
*/
public interface TaskCollector {
public TaskTable getTasks();
}
TaskCollector是一个接口,它表示任务收集器,顾名思义就是存储多个需要调度任务的储存器,接口方法getTasks用于获取任务,内部通过两个list集合分别记录cron表达式和
具体的Task,它有两个实现MemoryTaskCollector,FileTaskCollector,其中FileTaskCollector中储存的是File对象,FileTaskCollector我们不会去研究它,我们主要研究一
下MemoryTaskCollector
//表示任务个数
private int size = 0;
//cron表达式
private ArrayList patterns = new ArrayList();
//Task列表
private ArrayList tasks = new ArrayList();
//cron4j生成的唯一id
private ArrayList ids = new ArrayList();
public synchronized String add(SchedulingPattern pattern, Task task) {
//生成唯一id
String id = GUIDGenerator.generate();
//加入任务
patterns.add(pattern);
tasks.add(task);
ids.add(id);
return id;
}
集合patterns,tasks,tasks是通过索引下标一一对应的,其实我个人觉得可以封装成一个对象
2.2 TaskTable
//模式集合,用于存储cron表达式
private ArrayList patterns = new ArrayList();
//任务集合,用于存储任务对象
private ArrayList tasks = new ArrayList();
//获取SchedulingPattern,SchedulingPattern是用于解析了cron表达式的调度匹配模式
public SchedulingPattern getSchedulingPattern(int index)
throws IndexOutOfBoundsException {
return (SchedulingPattern) patterns.get(index);
}
2.3 ValueParser
//将cron表达式中解析出来的时间值转化成int
public int parse(String value) throws Exception;
//获取对应时间的最小值,比如分钟解析器,那么最小值就是0
public int getMinValue();
//获取对应时间的最大值,比如分钟的59,小时的23
public int getMaxValue();
对应的实现类目前有MinuteValueParser,HourValueParser,DayOfMonthValueParser,MonthValueParser,DayOfWeekValueParser
具体的实现比较简单,此处不做过多的介绍
2.4 SchedulingPattern
//用于解析分钟的解析器
private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser();
//用于小时的解析器
private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser();
//用于日期的解析器
private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser();
//月份解析器
private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser();
//星期解析器
private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser();
//被解析的原始cron表达式
private String asString;
//记录用于匹配分的匹配器
protected ArrayList minuteMatchers = new ArrayList();
//记录用于匹配小时的匹配器
protected ArrayList hourMatchers = new ArrayList();
//记录用于匹配日期的匹配器
protected ArrayList dayOfMonthMatchers = new ArrayList();
//用于匹配月份的匹配器
protected ArrayList monthMatchers = new ArrayList();
//用于匹配周的匹配器
protected ArrayList dayOfWeekMatchers = new ArrayList();
2.4.1 构造器
//pattern是传入的cron表达式
public SchedulingPattern(String pattern) throws InvalidPatternException {
this.asString = pattern;
//多个cron表达式通过 | 分割,表示或的关系,只要满足其中一个cron表达式即可
StringTokenizer st1 = new StringTokenizer(pattern, "|");
if (st1.countTokens() < 1) {
throw new InvalidPatternException("invalid pattern: \"" + pattern + "\"");
}
while (st1.hasMoreTokens()) {
String localPattern = st1.nextToken();
//cron表达式可以通过空格和table符分割,比如 */1 * * * *
StringTokenizer st2 = new StringTokenizer(localPattern, " \t");
//必须要有5个时间值,分 时 日 月 周
if (st2.countTokens() != 5) {
throw new InvalidPatternException("invalid pattern: \"" + localPattern + "\"");
}
try {
//使用分钟解析器解析 分
minuteMatchers.add(buildValueMatcher(st2.nextToken(), MINUTE_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern + "\". Error parsing minutes field: "
+ e.getMessage() + ".");
}
try {
//使用小时解析器解析 时
hourMatchers.add(buildValueMatcher(st2.nextToken(), HOUR_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern + "\". Error parsing hours field: "
+ e.getMessage() + ".");
}
try {
//使用日期解析器解析 日期
dayOfMonthMatchers.add(buildValueMatcher(st2.nextToken(), DAY_OF_MONTH_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern
+ "\". Error parsing days of month field: "
+ e.getMessage() + ".");
}
try {
//使用月份解析器解析 月份
monthMatchers.add(buildValueMatcher(st2.nextToken(), MONTH_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern + "\". Error parsing months field: "
+ e.getMessage() + ".");
}
try {
//使用周解析器解析 周
dayOfWeekMatchers.add(buildValueMatcher(st2.nextToken(), DAY_OF_WEEK_VALUE_PARSER));
} catch (Exception e) {
throw new InvalidPatternException("invalid pattern \""
+ localPattern
+ "\". Error parsing days of week field: "
+ e.getMessage() + ".");
}
//匹配器个数,这个个数指的是cron表达式的个数 */1 * * * * | */2 * * * * 表示两个
matcherSize++;
}
}
SchedulingPattern的构造器将传入的cron表达式进行解析,把 分 时 日 月 周 封装到对应类型的匹配器中,以便后面进行匹配
2.4.2 构建匹配器
从SchedulingPattern的构造器中我们可以看到,分 时 日 月 周 各个时间字段的解析都是通过一个叫做的buildValueMatcher的方法进行处理的
private ValueMatcher buildValueMatcher(String str, ValueParser parser)
throws Exception {
//如果当前时间字段只有一个字符并且是*的话,那么表示匹配任意时间
//直接返回一个总是匹配的matcher
if (str.length() == 1 && str.equals("*")) {
return new AlwaysTrueValueMatcher();
}
ArrayList values = new ArrayList();
//如果不是*,那么可能是逗号分割的时间值,现在我们假设要解析的cron表达式为 1-6,6-0 1,23 * * *
//这个表达式表示每天的凌晨1点和23点的每分钟执行
StringTokenizer st = new StringTokenizer(str, ",");
while (st.hasMoreTokens()) {
//假设此时我们解析的是分这个字段,也就是 1-6,6-0
//那么此时获取的element的值为1-6
String element = st.nextToken();
ArrayList local;
try {
//解析1-6获取到一个范围集合 1,2,3,4,5,6
local = parseListElement(element, parser);
} catch (Exception e) {
throw new Exception("invalid field \"" + str
+ "\", invalid element \"" + element + "\", "
+ e.getMessage());
}
//循环将上面1-6范围的时间值合并到大的范围集合,如果把分这个时间字段全部进行解析了之后
//values集合的值应为 0 - 59
for (Iterator i = local.iterator(); i.hasNext();) {
Object value = i.next();
//排除重复的,比如第一次解析1-6时,values集合中已经存在一个6了,那么下次解析6-0时就不会将6添加进去
if (!values.contains(value)) {
values.add(value);
}
}
}
if (values.size() == 0) {
throw new Exception("invalid field \"" + str + "\"");
}
//日期的与其他的匹配器稍微不同,因为存在闰年与平年之分
if (parser == DAY_OF_MONTH_VALUE_PARSER) {
return new DayOfMonthValueMatcher(values);
} else {
//构建匹配器
return new IntArrayValueMatcher(values);
}
}
2.4.3 解析时间范围
private ArrayList parseListElement(String str, ValueParser parser)
throws Exception {
//时间字段可能存在/,表示每隔多少时间执行一次
StringTokenizer st = new StringTokenizer(str, "/");
int size = st.countTokens();
if (size < 1 || size > 2) {
throw new Exception("syntax error");
}
ArrayList values;
try {
values = parseRange(st.nextToken(), parser);
} catch (Exception e) {
throw new Exception("invalid range, " + e.getMessage());
}
//如果有两个元素,那么表示存在 /
if (size == 2) {
String dStr = st.nextToken();
int div;
try {
//解析 / 后面的值
div = Integer.parseInt(dStr);
} catch (NumberFormatException e) {
throw new Exception("invalid divisor \"" + dStr + "\"");
}
if (div < 1) {
throw new Exception("non positive divisor \"" + div + "\"");
}
ArrayList values2 = new ArrayList();
//每隔div执行一次,将符合的时间存入,然后返回values2集合
for (int i = 0; i < values.size(); i += div) {
values2.add(values.get(i));
}
return values2;
} else {
return values;
}
}
上面这个方法将时间字段解析成范围,通过list存储,如果存在 / (表示没隔多少就执行一次),那么将范围值通过/后面这个值进行间隔,将间隔后的集合返回,下面我们再
看看是怎么解析范围值的
private ArrayList parseRange(String str, ValueParser parser)
throws Exception {
//如果是 * ,表示任意值
if (str.equals("*")) {
//范围为 min - max,如果是 小时解析器,那么 就是 0 - 23
int min = parser.getMinValue();
int max = parser.getMaxValue();
ArrayList values = new ArrayList();
for (int i = min; i <= max; i++) {
values.add(new Integer(i));
}
return values;
}
//时间字段可能是 - 分割的
StringTokenizer st = new StringTokenizer(str, "-");
int size = st.countTokens();
if (size < 1 || size > 2) {
throw new Exception("syntax error");
}
String v1Str = st.nextToken();
int v1;
try {
//解析为int
v1 = parser.parse(v1Str);
} catch (Exception e) {
throw new Exception("invalid value \"" + v1Str + "\", "
+ e.getMessage());
}
//size等于表示当前时间字段不是通过 - 分割的
if (size == 1) {
ArrayList values = new ArrayList();
values.add(new Integer(v1));
return values;
} else {
String v2Str = st.nextToken();
int v2;
try {
//解析 - 后面的值
v2 = parser.parse(v2Str);
} catch (Exception e) {
throw new Exception("invalid value \"" + v2Str + "\", "
+ e.getMessage());
}
ArrayList values = new ArrayList();
//范围为 v1 - v2
if (v1 < v2) {
for (int i = v1; i <= v2; i++) {
values.add(new Integer(i));
}
//如果v1大于v2,那么表示 v1 - max , min - v2
} else if (v1 > v2) {
int min = parser.getMinValue();
int max = parser.getMaxValue();
for (int i = v1; i <= max; i++) {
values.add(new Integer(i));
}
for (int i = min; i <= v2; i++) {
values.add(new Integer(i));
}
} else {
// v1 == v2
values.add(new Integer(v1));
}
return values;
}
}
2.5 ValueMatcher
用于匹配值
public boolean match(int value);
下面分析下 ValueMatcher其中的一个实现IntArrayValueMatcher
class IntArrayValueMatcher implements ValueMatcher {
//这个就是SchedulingPattern解析后的时间范围
private int[] values;
//。。。。。。省略部分代码
public boolean match(int value) {
for (int i = 0; i < values.length; i++) {
if (values[i] == value) {
return true;
}
}
return false;
}
}
只要对应的时间字段能够匹配到上面范围内的值,那么返回true
看到这我想象大家大概可以猜到它是怎么匹配表达式的了吧,就是获取一个时间的 分 时 日 月 周这5个时间去匹配对应时间类型的范围中是否包含
2.6 Scheduler
//生成唯一id
private String guid = GUIDGenerator.generate();
//时间区域
private TimeZone timezone = null;
//调度线程是否设置为守护线程
private boolean daemon = false;
//表示调度是否已经开始,如果没有开始,任务不能被执行
private boolean started = false;
//用于记录收集器的集合
private ArrayList collectors = new ArrayList();
//内存收集器,通常我们使用的就是这个收集器
private MemoryTaskCollector memoryTaskCollector = new MemoryTaskCollector();
//文件任务收集器,用于存放File对象
private FileTaskCollector fileTaskCollector = new FileTaskCollector();
//用于注册监听器
private ArrayList listeners = new ArrayList();
//检查时间匹配线程,在cron4j中由于cron表达式最小的时候为分,那么这个线程每分钟检查一次
private TimerThread timer = null;
//当前正在启动的执行任务的线程
private ArrayList launchers = null;
//记录执行任务及上下文的集合
private ArrayList executors = null;
//用于同步的对象
private Object lock = new Object();
Scheduler是启动调度任务的入口,我们在下一节分析其具体的启动过程。