Cron4j 是一个轻量级的 Java 定时任务调度库,默认情况下不支持秒级别的定时任务。如果需要扩展秒级别的定时任务,可以通过修改 Cron4j 的源码来实现。本文将详细介绍如何修改 Cron4j 的源码以支持秒级别的定时任务。
环境准备
首先,从 Cron4j 的 GitHub 仓库 下载源码,并导入到你的 IDE 中。pom地址如下:
<dependency><groupId>it.sauronsoftware.cron4j</groupId><artifactId>cron4j</artifactId><version>2.2.5</version>
</dependency>
修改 SchedulingPattern 类
SchedulingPattern 类负责解析和匹配调度表达式。我们需要修改它以支持秒级别的调度。
添加秒字段:在 SchedulingPattern 类中添加对秒字段的支持。
/** cron4j - A pure Java cron-like scheduler* * Copyright (C) 2007-2010 Carlo Pelliccia (www.sauronsoftware.it)* * This program is free software: you can redistribute it and/or modify* it under the terms of the GNU Lesser General Public License version* 2.1, as published by the Free Software Foundation.** This program is distributed in the hope that it will be useful,* but WITHOUT ANY WARRANTY; without even the implied warranty of* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the* GNU Lesser General Public License 2.1 for more details.** You should have received a copy of the GNU Lesser General Public* License version 2.1 along with this program.* If not, see <http://www.gnu.org/licenses/>.*/
package it.sauronsoftware.cron4j;import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.TimeZone;/*** <p>* A UNIX crontab-like pattern is a string split in five space separated parts.* Each part is intented as:* </p>* <ol>* <li><strong>Minutes sub-pattern</strong>. During which minutes of the hour* should the task been launched? The values range is from 0 to 59.</li>* <li><strong>Hours sub-pattern</strong>. During which hours of the day should* the task been launched? The values range is from 0 to 23.</li>* <li><strong>Days of month sub-pattern</strong>. During which days of the* month should the task been launched? The values range is from 1 to 31. The* special value L can be used to recognize the last day of month.</li>* <li><strong>Months sub-pattern</strong>. During which months of the year* should the task been launched? The values range is from 1 (January) to 12* (December), otherwise this sub-pattern allows the aliases "jan",* "feb", "mar", "apr", "may",* "jun", "jul", "aug", "sep",* "oct", "nov" and "dec".</li>* <li><strong>Days of week sub-pattern</strong>. During which days of the week* should the task been launched? The values range is from 0 (Sunday) to 6* (Saturday), otherwise this sub-pattern allows the aliases "sun",* "mon", "tue", "wed", "thu",* "fri" and "sat".</li>* </ol>* <p>* The star wildcard character is also admitted, indicating "every minute* of the hour", "every hour of the day", "every day of the* month", "every month of the year" and "every day of the* week", according to the sub-pattern in which it is used.* </p>* <p>* Once the scheduler is started, a task will be launched when the five parts in* its scheduling pattern will be true at the same time.* </p>* <p>* Some examples:* </p>* <p>* <strong>5 * * * *</strong><br />* This pattern causes a task to be launched once every hour, at the begin of* the fifth minute (00:05, 01:05, 02:05 etc.).* </p>* <p>* <strong>* * * * *</strong><br />* This pattern causes a task to be launched every minute.* </p>* <p>* <strong>* 12 * * Mon</strong><br />* This pattern causes a task to be launched every minute during the 12th hour* of Monday.* </p>* <p>* <strong>* 12 16 * Mon</strong><br />* This pattern causes a task to be launched every minute during the 12th hour* of Monday, 16th, but only if the day is the 16th of the month.* </p>* <p>* Every sub-pattern can contain two or more comma separated values.* </p>* <p>* <strong>59 11 * * 1,2,3,4,5</strong><br />* This pattern causes a task to be launched at 11:59AM on Monday, Tuesday,* Wednesday, Thursday and Friday.* </p>* <p>* Values intervals are admitted and defined using the minus character.* </p>* <p>* <strong>59 11 * * 1-5</strong><br />* This pattern is equivalent to the previous one.* </p>* <p>* The slash character can be used to identify step values within a range. It* can be used both in the form <em>*/c</em> and <em>a-b/c</em>. The* subpattern is matched every <em>c</em> values of the range* <em>0,maxvalue</em> or <em>a-b</em>.* </p>* <p>* <strong>*/5 * * * *</strong><br />* This pattern causes a task to be launched every 5 minutes (0:00, 0:05, 0:10,* 0:15 and so on).* </p>* <p>* <strong>3-18/5 * * * *</strong><br />* This pattern causes a task to be launched every 5 minutes starting from the* third minute of the hour, up to the 18th (0:03, 0:08, 0:13, 0:18, 1:03, 1:08* and so on).* </p>* <p>* <strong>*/15 9-17 * * *</strong><br />* This pattern causes a task to be launched every 15 minutes between the 9th* and 17th hour of the day (9:00, 9:15, 9:30, 9:45 and so on... note that the* last execution will be at 17:45).* </p>* <p>* All the fresh described syntax rules can be used together.* </p>* <p>* <strong>* 12 10-16/2 * *</strong><br />* This pattern causes a task to be launched every minute during the 12th hour* of the day, but only if the day is the 10th, the 12th, the 14th or the 16th* of the month.* </p>* <p>* <strong>* 12 1-15,17,20-25 * *</strong><br />* This pattern causes a task to be launched every minute during the 12th hour* of the day, but the day of the month must be between the 1st and the 15th,* the 20th and the 25, or at least it must be the 17th.* </p>* <p>* Finally cron4j lets you combine more scheduling patterns into one, with the* pipe character:* </p>* <p>* <strong>0 5 * * *|8 10 * * *|22 17 * * *</strong><br />* This pattern causes a task to be launched every day at 05:00, 10:08 and* 17:22.* </p>* * @author Carlo Pelliccia* @since 2.0*/
public class SchedulingPattern {/*** The parser for the second values.*/private static final ValueParser SECONDS_VALUE_PARSER = new SecondsValueParser();/*** The parser for the minute values.*/private static final ValueParser MINUTE_VALUE_PARSER = new MinuteValueParser();/*** The parser for the hour values.*/private static final ValueParser HOUR_VALUE_PARSER = new HourValueParser();/*** The parser for the day of month values.*/private static final ValueParser DAY_OF_MONTH_VALUE_PARSER = new DayOfMonthValueParser();/*** The parser for the month values.*/private static final ValueParser MONTH_VALUE_PARSER = new MonthValueParser();/*** The parser for the day of week values.*/private static final ValueParser DAY_OF_WEEK_VALUE_PARSER = new DayOfWeekValueParser();/*** Validates a string as a scheduling pattern.* * @param schedulingPattern* The pattern to validate.* @return true if the given string represents a valid scheduling pattern;* false otherwise.*/public static boolean validate(String schedulingPattern) {try {new SchedulingPattern(schedulingPattern);} catch (InvalidPatternException e) {return false;}return true;}/*** The pattern as a string.*/private String asString;/*** The ValueMatcher list for the "minute" field.*/protected ArrayList secondsMatchers = new ArrayList();/*** The ValueMatcher list for the "minute" field.*/protected ArrayList minuteMatchers = new ArrayList();/*** The ValueMatcher list for the "hour" field.*/protected ArrayList hourMatchers = new ArrayList();/*** The ValueMatcher list for the "day of month" field.*/protected ArrayList dayOfMonthMatchers = new ArrayList();/*** The ValueMatcher list for the "month" field.*/protected ArrayList monthMatchers = new ArrayList();/*** The ValueMatcher list for the "day of week" field.*/protected ArrayList dayOfWeekMatchers = new ArrayList();/*** How many matcher groups in this pattern?*/protected int matcherSize = 0;/*** Builds a SchedulingPattern parsing it from a string.* * @param pattern* The pattern as a crontab-like string.* @throws InvalidPatternException* If the supplied string is not a valid pattern.*/public SchedulingPattern(String pattern) throws InvalidPatternException {this.asString = pattern;StringTokenizer st1 = new StringTokenizer(pattern, "|");if (st1.countTokens() < 1) {throw new InvalidPatternException("invalid pattern: \"" + pattern + "\"");}while (st1.hasMoreTokens()) {String localPattern = st1.nextToken();StringTokenizer st2 = new StringTokenizer(localPattern, " \t");if (st2.countTokens() != 6) {throw new InvalidPatternException("invalid pattern: \"" + localPattern + "\"");}try {secondsMatchers.add(buildValueMatcher(st2.nextToken(), SECONDS_VALUE_PARSER));} catch (Exception e) {throw new InvalidPatternException("invalid pattern \""+ localPattern + "\". Error parsing minutes field: "+ e.getMessage() + ".");}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() + ".");}matcherSize++;}}/*** A ValueMatcher utility builder.* * @param str* The pattern part for the ValueMatcher creation.* @param parser* The parser used to parse the values.* @return The requested ValueMatcher.* @throws Exception* If the supplied pattern part is not valid.*/private ValueMatcher buildValueMatcher(String str, ValueParser parser)throws Exception {if (str.length() == 1 && str.equals("*")) {return new AlwaysTrueValueMatcher();}ArrayList values = new ArrayList();StringTokenizer st = new StringTokenizer(str, ",");while (st.hasMoreTokens()) {String element = st.nextToken();ArrayList local;try {local = parseListElement(element, parser);} catch (Exception e) {throw new Exception("invalid field \"" + str+ "\", invalid element \"" + element + "\", "+ e.getMessage());}for (Iterator i = local.iterator(); i.hasNext();) {Object value = i.next();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);}}/*** Parses an element of a list of values of the pattern.* * @param str* The element string.* @param parser* The parser used to parse the values.* @return A list of integers representing the allowed values.* @throws Exception* If the supplied pattern part is not valid.*/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();for (int i = 0; i < values.size(); i += div) {values2.add(values.get(i));}return values2;} else {return values;}}/*** Parses a range of values.* * @param str* The range string.* @param parser* The parser used to parse the values.* @return A list of integers representing the allowed values.* @throws Exception* If the supplied pattern part is not valid.*/private ArrayList parseRange(String str, ValueParser parser)throws Exception {if (str.equals("*")) {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 {v1 = parser.parse(v1Str);} catch (Exception e) {throw new Exception("invalid value \"" + v1Str + "\", "+ e.getMessage());}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();if (v1 < v2) {for (int i = v1; i <= v2; i++) {values.add(new Integer(i));}} 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 == v2values.add(new Integer(v1));}return values;}}/*** This methods returns true if the given timestamp (expressed as a UNIX-era* millis value) matches the pattern, according to the given time zone.* * @param timezone* A time zone.* @param millis* The timestamp, as a UNIX-era millis value.* @return true if the given timestamp matches the pattern.*/public boolean match(TimeZone timezone, long millis) {GregorianCalendar gc = new GregorianCalendar();gc.setTimeInMillis(millis);gc.setTimeZone(timezone);int seconds = gc.get(Calendar.SECOND);int minute = gc.get(Calendar.MINUTE);int hour = gc.get(Calendar.HOUR_OF_DAY);int dayOfMonth = gc.get(Calendar.DAY_OF_MONTH);int month = gc.get(Calendar.MONTH) + 1;int dayOfWeek = gc.get(Calendar.DAY_OF_WEEK) - 1;int year = gc.get(Calendar.YEAR);for (int i = 0; i < matcherSize; i++) {ValueMatcher secondsMatcher = (ValueMatcher) secondsMatchers.get(i);ValueMatcher minuteMatcher = (ValueMatcher) minuteMatchers.get(i);ValueMatcher hourMatcher = (ValueMatcher) hourMatchers.get(i);ValueMatcher dayOfMonthMatcher = (ValueMatcher) dayOfMonthMatchers.get(i);ValueMatcher monthMatcher = (ValueMatcher) monthMatchers.get(i);ValueMatcher dayOfWeekMatcher = (ValueMatcher) dayOfWeekMatchers.get(i);boolean eval = secondsMatcher.match(seconds)&&minuteMatcher.match(minute)&& hourMatcher.match(hour)&& ((dayOfMonthMatcher instanceof DayOfMonthValueMatcher) ? ((DayOfMonthValueMatcher) dayOfMonthMatcher).match(dayOfMonth, month, gc.isLeapYear(year)): dayOfMonthMatcher.match(dayOfMonth))&& monthMatcher.match(month)&& dayOfWeekMatcher.match(dayOfWeek);if (eval) {return true;}}return false;}/*** This methods returns true if the given timestamp (expressed as a UNIX-era* millis value) matches the pattern, according to the system default time* zone.* * @param millis* The timestamp, as a UNIX-era millis value.* @return true if the given timestamp matches the pattern.*/public boolean match(long millis) {return match(TimeZone.getDefault(), millis);}/*** Returns the pattern as a string.* * @return The pattern as a string.*/public String toString() {return asString;}/*** This utility method changes an alias to an int value.* * @param value* The value.* @param aliases* The aliases list.* @param offset* The offset appplied to the aliases list indices.* @return The parsed value.* @throws Exception* If the expressed values doesn't match any alias.*/private static int parseAlias(String value, String[] aliases, int offset)throws Exception {for (int i = 0; i < aliases.length; i++) {if (aliases[i].equalsIgnoreCase(value)) {return offset + i;}}throw new Exception("invalid alias \"" + value + "\"");}/*** Definition for a value parser.*/private static interface ValueParser {/*** Attempts to parse a value.* * @param value* The value.* @return The parsed value.* @throws Exception* If the value can't be parsed.*/public int parse(String value) throws Exception;/*** Returns the minimum value accepred by the parser.* * @return The minimum value accepred by the parser.*/public int getMinValue();/*** Returns the maximum value accepred by the parser.* * @return The maximum value accepred by the parser.*/public int getMaxValue();}/*** A simple value parser.*/private static class SimpleValueParser implements ValueParser {/*** The minimum allowed value.*/protected int minValue;/*** The maximum allowed value.*/protected int maxValue;/*** Builds the value parser.* * @param minValue* The minimum allowed value.* @param maxValue* The maximum allowed value.*/public SimpleValueParser(int minValue, int maxValue) {this.minValue = minValue;this.maxValue = maxValue;}public int parse(String value) throws Exception {int i;try {i = Integer.parseInt(value);} catch (NumberFormatException e) {throw new Exception("invalid integer value");}if (i < minValue || i > maxValue) {throw new Exception("value out of range");}return i;}public int getMinValue() {return minValue;}public int getMaxValue() {return maxValue;}}/*** The minutes value parser.*/private static class SecondsValueParser extends SimpleValueParser {/*** Builds the value parser.*/public SecondsValueParser() {super(0, 59);}}/*** The minutes value parser.*/private static class MinuteValueParser extends SimpleValueParser {/*** Builds the value parser.*/public MinuteValueParser() {super(0, 59);}}/*** The hours value parser.*/private static class HourValueParser extends SimpleValueParser {/*** Builds the value parser.*/public HourValueParser() {super(0, 23);}}/*** The days of month value parser.*/private static class DayOfMonthValueParser extends SimpleValueParser {/*** Builds the value parser.*/public DayOfMonthValueParser() {super(1, 31);}/*** Added to support last-day-of-month.* * @param value* The value to be parsed* @return the integer day of the month or 32 for last day of the month* @throws Exception* if the input value is invalid*/public int parse(String value) throws Exception {if (value.equalsIgnoreCase("L")) {return 32;} else {return super.parse(value);}}}/*** The value parser for the months field.*/private static class MonthValueParser extends SimpleValueParser {/*** Months aliases.*/private static String[] ALIASES = { "jan", "feb", "mar", "apr", "may","jun", "jul", "aug", "sep", "oct", "nov", "dec" };/*** Builds the months value parser.*/public MonthValueParser() {super(1, 12);}public int parse(String value) throws Exception {try {// try as a simple valuereturn super.parse(value);} catch (Exception e) {// try as an aliasreturn parseAlias(value, ALIASES, 1);}}}/*** The value parser for the months field.*/private static class DayOfWeekValueParser extends SimpleValueParser {/*** Days of week aliases.*/private static String[] ALIASES = { "sun", "mon", "tue", "wed", "thu", "fri", "sat" };/*** Builds the months value parser.*/public DayOfWeekValueParser() {super(0, 7);}public int parse(String value) throws Exception {try {// try as a simple valuereturn super.parse(value) % 7;} catch (Exception e) {// try as an aliasreturn parseAlias(value, ALIASES, 0);}}}}
使用方法:
首先,将上述修改后的类替换 cron4j 中的原类。
然后,传入的 cron 表达式要采用新的格式,包含秒级信息,例如 "0 0 0 * * *" 表示在每天的 0 点 0 分 0 秒执行任务,或者 "0-30 * * * * *" 表示每分钟的 0 到 30 秒都执行任务。
请注意,上述代码仅为示例,实际修改 cron4j 的源码可能会涉及更多复杂的细节,例如异常处理、并发问题、cron 表达式更复杂的匹配逻辑等,需要根据 cron4j 的实际源码进行更深入的修改和完善。此外,修改源码可能会影响其兼容性和稳定性,需要进行充分的测试。