cron表达式是一个好东西,这个东西不仅Java的quartZ能用到,Go语言中也可以用到。我没有用过Linux的cron,但网上说Linux也是可以用crontab -e 命令来配置定时任务。Go语言和Java中都是可以精确到秒的,但是Linux中不行。
表示增长间隔,如第1个字段(minutes) 值是 3-59/15,表示每小时的第3分钟开始执行一次,之后每隔 15 分钟执行一次(即 3、18、33、48 这些时间点执行),这里也可以表示为:3/15
1 | `// SpecSchedule specifies a duty cycle (to the second granularity), based on a``// traditional crontab specification. It is computed initially and stored as bit sets.``type SpecSchedule struct {`` ``// 表达式中锁表明的,秒,分,时,日,月,周,每个都是uint64`` ``// Dom:Day of Month,Dow:Day of week`` ``Second, Minute, Hour, Dom, Month, Dow uint64``}` `// bounds provides a range of acceptable values (plus a map of name to value).``// 定义了表达式的结构体``type bounds struct {`` ``min, max uint`` ``names map[string]uint``}` `// The bounds for each field.``// 这样就能看出各个表达式的范围``var (`` ``seconds = bounds{0, 59, nil}`` ``minutes = bounds{0, 59, nil}`` ``hours = bounds{0, 23, nil}`` ``dom = bounds{1, 31, nil}`` ``months = bounds{1, 12, map[string]uint{`` ``"jan": 1,`` ``"feb": 2,`` ``"mar": 3,`` ``"apr": 4,`` ``"may": 5,`` ``"jun": 6,`` ``"jul": 7,`` ``"aug": 8,`` ``"sep": 9,`` ``"oct": 10,`` ``"nov": 11,`` ``"dec": 12,`` ``}}`` ``dow = bounds{0, 6, map[string]uint{`` ``"sun": 0,`` ``"mon": 1,`` ``"tue": 2,`` ``"wed": 3,`` ``"thu": 4,`` ``"fri": 5,`` ``"sat": 6,`` ``}}``)` `const (`` ``// Set the top bit if a star was included in the expression.`` ``starBit = 1 << 63``)` |
看了上面的东西肯定有人疑惑为什么秒分时这些都是定义了unit64,以及定义了一个常量starBit = 1 << 63这种写法,这是逻辑运算符。表示二进制1向左移动63位。原因如下:
cron表达式是用来表示一系列时间的,而时间是无法逃脱自己的区间的 , 分,秒 0 - 59 , 时 0 - 23 , 天/月 0 - 31 , 天/周 0 - 6 , 月0 - 11 。 这些本质上都是一个点集合,或者说是一个整数区间。 那么对于任意的整数区间 , 可以描述cron的如下部分规则。
至此, robfig/cron为什么不支持 L | W的原因已经明了了。去除这两条规则后, 其余的规则其实完全可以使用点的穷举来通用表示。 考虑到最大的区间也不过是60个点,那么使用一个uint64的整数的每一位来表示一个点便很合适了。所以定义unit64不为过
1 | `package cron` `import (`` ``"fmt"`` ``"math"`` ``"strconv"`` ``"strings"`` ``"time"``)` `// Configuration options for creating a parser. Most options specify which``// fields should be included, while others enable features. If a field is not``// included the parser will assume a default value. These options do not change``// the order fields are parse in.``type ParseOption int` `const (`` ``Second ParseOption = 1 << iota // Seconds field, default 0`` ``Minute // Minutes field, default 0`` ``Hour // Hours field, default 0`` ``Dom // Day of month field, default *`` ``Month // Month field, default *`` ``Dow // Day of week field, default *`` ``DowOptional // Optional day of week field, default *`` ``Descriptor // Allow descriptors such as @monthly, @weekly, etc.``)` `var places = []ParseOption{`` ``Second,`` ``Minute,`` ``Hour,`` ``Dom,`` ``Month,`` ``Dow,``}` `var defaults = []string{`` ``"0",`` ``"0",`` ``"0",`` ``"*",`` ``"*",`` ``"*",``}` `// A custom Parser that can be configured.``type Parser struct {`` ``options ParseOption`` ``optionals int``}` `// Creates a custom Parser with custom options.``//``// // Standard parser without descriptors``// specParser := NewParser(Minute | Hour | Dom | Month | Dow)``// sched, err := specParser.Parse("0 0 15 */3 *")``//``// // Same as above, just excludes time fields``// subsParser := NewParser(Dom | Month | Dow)``// sched, err := specParser.Parse("15 */3 *")``//``// // Same as above, just makes Dow optional``// subsParser := NewParser(Dom | Month | DowOptional)``// sched, err := specParser.Parse("15 */3")``//``func NewParser(options ParseOption) Parser {`` ``optionals := 0`` ``if options&DowOptional > 0 {`` ``options |= Dow`` ``optionals++`` ``}`` ``return Parser{options, optionals}``}` `// Parse returns a new crontab schedule representing the given spec.``// It returns a descriptive error if the spec is not valid.``// It accepts crontab specs and features configured by NewParser.``// 将字符串解析成为SpecSchedule 。 SpecSchedule符合Schedule接口` `func (p Parser) Parse(spec string) (Schedule, error) {`` // 直接处理特殊的特殊的字符串`` ``if spec[0] == '@' && p.options&Descriptor > 0 {`` ``return parseDescriptor(spec)`` ``}` ` ``// Figure out how many fields we need`` ``max := 0`` ``for _, place := range places {`` ``if p.options&place > 0 {`` ``max++`` ``}`` ``}`` ``min := max - p.optionals` ` ``// cron利用空白拆解出独立的items。`` ``fields := strings.Fields(spec)` ` ``// 验证表达式取值范围`` ``if count := len(fields); count < min || count > max {`` ``if min == max {`` ``return nil, fmt.Errorf("Expected exactly %d fields, found %d: %s", min, count, spec)`` ``}`` ``return nil, fmt.Errorf("Expected %d to %d fields, found %d: %s", min, max, count, spec)`` ``}` ` ``// Fill in missing fields`` ``fields = expandFields(fields, p.options)` ` ``var err error`` ``field := func(field string, r bounds) uint64 {`` ``if err != nil {`` ``return 0`` ``}`` ``var bits uint64`` ``bits, err = getField(field, r)`` ``return bits`` ``}` ` ``var (`` ``second = field(fields[0], seconds)`` ``minute = field(fields[1], minutes)`` ``hour = field(fields[2], hours)`` ``dayofmonth = field(fields[3], dom)`` ``month = field(fields[4], months)`` ``dayofweek = field(fields[5], dow)`` ``)`` ``if err != nil {`` ``return nil, err`` ``}`` ``// 返回所需要的SpecSchedule`` ``return &SpecSchedule{`` ``Second: second,`` ``Minute: minute,`` ``Hour: hour,`` ``Dom: dayofmonth,`` ``Month: month,`` ``Dow: dayofweek,`` ``}, nil``}` `func expandFields(fields []string, options ParseOption) []string {`` ``n := 0`` ``count := len(fields)`` ``expFields := make([]string, len(places))`` ``copy(expFields, defaults)`` ``for i, place := range places {`` ``if options&place > 0 {`` ``expFields[i] = fields[n]`` ``n++`` ``}`` ``if n == count {`` ``break`` ``}`` ``}`` ``return expFields``}` `var standardParser = NewParser(`` ``Minute | Hour | Dom | Month | Dow | Descriptor,``)` `// ParseStandard returns a new crontab schedule representing the given standardSpec``// (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always``// pass 5 entries representing: minute, hour, day of month, month and day of week,``// in that order. It returns a descriptive error if the spec is not valid.``//``// It accepts``// - Standard crontab specs, e.g. "* * * * ?"``// - Descriptors, e.g. "@midnight", "@every 1h30m"``// 这里表示不仅可以使用cron表达式,也可以使用@midnight @every等方法` `func ParseStandard(standardSpec string) (Schedule, error) {`` ``return standardParser.Parse(standardSpec)``}` `var defaultParser = NewParser(`` ``Second | Minute | Hour | Dom | Month | DowOptional | Descriptor,``)` `// Parse returns a new crontab schedule representing the given spec.``// It returns a descriptive error if the spec is not valid.``//``// It accepts``// - Full crontab specs, e.g. "* * * * * ?"``// - Descriptors, e.g. "@midnight", "@every 1h30m"``func Parse(spec string) (Schedule, error) {`` ``return defaultParser.Parse(spec)``}` `// getField returns an Int with the bits set representing all of the times that``// the field represents or error parsing field value. A "field" is a comma-separated``// list of "ranges".``func getField(field string, r bounds) (uint64, error) {`` ``var bits uint64`` ``ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })`` ``for _, expr := range ranges {`` ``bit, err := getRange(expr, r)`` ``if err != nil {`` ``return bits, err`` ``}`` ``bits |= bit`` ``}`` ``return bits, nil``}` `// getRange returns the bits indicated by the given expression:``// number | number "-" number [ "/" number ]``// or error parsing range.``func getRange(expr string, r bounds) (uint64, error) {`` ``var (`` ``start, end, step uint`` ``rangeAndStep = strings.Split(expr, "/")`` ``lowAndHigh = strings.Split(rangeAndStep[0], "-")`` ``singleDigit = len(lowAndHigh) == 1`` ``err error`` ``)` ` ``var extra uint64`` ``if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {`` ``start = r.min`` ``end = r.max`` ``extra = starBit`` ``} else {`` ``start, err = parseIntOrName(lowAndHigh[0], r.names)`` ``if err != nil {`` ``return 0, err`` ``}`` ``switch len(lowAndHigh) {`` ``case 1:`` ``end = start`` ``case 2:`` ``end, err = parseIntOrName(lowAndHigh[1], r.names)`` ``if err != nil {`` ``return 0, err`` ``}`` ``default:`` ``return 0, fmt.Errorf("Too many hyphens: %s", expr)`` ``}`` ``}` ` ``switch len(rangeAndStep) {`` ``case 1:`` ``step = 1`` ``case 2:`` ``step, err = mustParseInt(rangeAndStep[1])`` ``if err != nil {`` ``return 0, err`` ``}` ` ``// Special handling: "N/step" means "N-max/step".`` ``if singleDigit {`` ``end = r.max`` ``}`` ``default:`` ``return 0, fmt.Errorf("Too many slashes: %s", expr)`` ``}` ` ``if start < r.min {`` ``return 0, fmt.Errorf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)`` ``}`` ``if end > r.max {`` ``return 0, fmt.Errorf("End of range (%d) above maximum (%d): %s", end, r.max, expr)`` ``}`` ``if start > end {`` ``return 0, fmt.Errorf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)`` ``}`` ``if step == 0 {`` ``return 0, fmt.Errorf("Step of range should be a positive number: %s", expr)`` ``}` ` ``return getBits(start, end, step) | extra, nil``}` `// parseIntOrName returns the (possibly-named) integer contained in expr.``func parseIntOrName(expr string, names map[string]uint) (uint, error) {`` ``if names != nil {`` ``if namedInt, ok := names[strings.ToLower(expr)]; ok {`` ``return namedInt, nil`` ``}`` ``}`` ``return mustParseInt(expr)``}` `// mustParseInt parses the given expression as an int or returns an error.``func mustParseInt(expr string) (uint, error) {`` ``num, err := strconv.Atoi(expr)`` ``if err != nil {`` ``return 0, fmt.Errorf("Failed to parse int from %s: %s", expr, err)`` ``}`` ``if num < 0 {`` ``return 0, fmt.Errorf("Negative number (%d) not allowed: %s", num, expr)`` ``}` ` ``return uint(num), nil``}` `// getBits sets all bits in the range [min, max], modulo the given step size.``func getBits(min, max, step uint) uint64 {`` ``var bits uint64` ` ``// If step is 1, use shifts.`` ``if step == 1 {`` ``return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)`` ``}` ` ``// Else, use a simple loop.`` ``for i := min; i <= max; i += step {`` ``bits |= 1 << i`` ``}`` ``return bits``}` `// all returns all bits within the given bounds. (plus the star bit)``func all(r bounds) uint64 {`` ``return getBits(r.min, r.max, 1) | starBit``}` `// parseDescriptor returns a predefined schedule for the expression, or error if none matches.``func parseDescriptor(descriptor string) (Schedule, error) {`` ``switch descriptor {`` ``case "@yearly", "@annually":`` ``return &SpecSchedule{`` ``Second: 1 << seconds.min,`` ``Minute: 1 << minutes.min,`` ``Hour: 1 << hours.min,`` ``Dom: 1 << dom.min,`` ``Month: 1 << months.min,`` ``Dow: all(dow),`` ``}, nil` ` ``case "@monthly":`` ``return &SpecSchedule{`` ``Second: 1 << seconds.min,`` ``Minute: 1 << minutes.min,`` ``Hour: 1 << hours.min,`` ``Dom: 1 << dom.min,`` ``Month: all(months),`` ``Dow: all(dow),`` ``}, nil` ` ``case "@weekly":`` ``return &SpecSchedule{`` ``Second: 1 << seconds.min,`` ``Minute: 1 << minutes.min,`` ``Hour: 1 << hours.min,`` ``Dom: all(dom),`` ``Month: all(months),`` ``Dow: 1 << dow.min,`` ``}, nil` ` ``case "@daily", "@midnight":`` ``return &SpecSchedule{`` ``Second: 1 << seconds.min,`` ``Minute: 1 << minutes.min,`` ``Hour: 1 << hours.min,`` ``Dom: all(dom),`` ``Month: all(months),`` ``Dow: all(dow),`` ``}, nil` ` ``case "@hourly":`` ``return &SpecSchedule{`` ``Second: 1 << seconds.min,`` ``Minute: 1 << minutes.min,`` ``Hour: all(hours),`` ``Dom: all(dom),`` ``Month: all(months),`` ``Dow: all(dow),`` ``}, nil`` ``}` ` ``const every = "@every "`` ``if strings.HasPrefix(descriptor, every) {`` ``duration, err := time.ParseDuration(descriptor[len(every):])`` ``if err != nil {`` ``return nil, fmt.Errorf("Failed to parse duration %s: %s", descriptor, err)`` ``}`` ``return Every(duration), nil`` ``}` ` ``return nil, fmt.Errorf("Unrecognized descriptor: %s", descriptor)``}` |
注: @every 用法比较特殊,这是Go里面比较特色的用法。同样的还有 @yearly @annually @monthly @weekly @daily @midnight @hourly 这里面就不一一赘述了。希望大家能够自己探索。