从 Cron 表达式到作品展览馆:一次真实的工具开发与发布记录
为什么写一个 Cron 表达式解析器
写定时任务最常犯的错,是「我以为它每天九点跑,其实它每小时都跑」。
这个错误不容易当场发现。定时任务通常安静地跑在后台,配置错了不报错、不告警,只是该执行的时候没执行,或者执行得比预期频繁几十倍。等到有人发现数据不对,往往已经过了几天。
所以我做了个小工具,叫 CronLab:输入一串 Cron 表达式,立刻给出三样东西——一句人话解释、每个字段展开后的取值、接下来五次准确的执行时间。纯前端实现,零依赖,打开页面就能用。
Cron 的三个坑
第一个坑是 ? 和 *。很多人以为两者等价,实际上在 Quartz 体系里 ? 表示「不指定」,只允许出现在「日」和「周」两个字段。写 0 0 12 ? * * 和写 0 0 12 * * * 在多数实现里结果一样,但在严格解析器里前者合法、后者在特定组合下会被判冲突。
第二个坑更隐蔽:「日」和「周」同时限定时,是并集不是交集。
这条规则来自 Vixie cron 的原始实现,一直沿用至今。0 0 1 * 1 的含义不是「每月一号且是周一」,而是「每月一号,或者每周一」。也就是说它一个月会触发五到六次,而不是平均每三个月一次。很多人第一次看到这个行为都以为是 bug,但它就是规范。
CronLab 在判断日期是否匹配时,把这条规则显式写了出来:
if (dom.wildcard) return dowMatch;
if (dow.wildcard) return domMatch;
return domMatch || dowMatch; // 两者都限定:取并集
第三个坑是段数。5 段是「分 时 日 月 周」,6 段是「秒 分 时 日 月 周」。多写一个字段,整体错位一格,多出来的那个字段变成了秒。0 9 * * 1-5 是工作日九点,而 0 0 9 * * 1-5 才是工作日九点整——两者的第一个 0 含义完全不同。工具会自动识别段数,5 段时补一个 0 秒。
求下次执行时间:不要暴力遍历
最直观的写法是从当前时间起一秒一秒往后试,直到凑够五个结果。这个写法在简单表达式上没问题,遇到 0 0 29 2 *(四年一次的闰日)就废了——要试几亿次。
CronLab 用的是逐级跳跃:先看月份对不对,不对就跳到下个月一号零点;再看日期,不对就跳到明天零点;再看小时、分钟、秒,逐层收敛。这样哪怕是最稀疏的表达式,也能在几十次迭代内出结果。
代价是实现复杂一些,每个字段都要有独立的进位逻辑。但换来的是确定性——不会因为表达式特殊就卡住。
发布到开发者作品展览馆
工具做完之后,我把它发布到了华为云开发者作品展览馆。这个过程比写代码本身曲折。
第一件事是找入口。按直觉去试开发者官网的几个路径,全是 404。真正的入口在一个独立子域名下,和主站不在一套路由里。
第二件事是发布方式。页面上写着「在 AI 对话中安装 Skill 并发出发布指令」,看起来是个对话式流程。但拆开看,底层是调平台的开放接口,提交一个 multipart 表单,包含作品名、简介、封面图、详情压缩包、代码仓库地址和分支。
第三件事,也是最卡的一步:身份凭证。接口不认访问密钥直连,只认最小权限的临时凭证,而临时凭证要通过委托机制换取。这里踩了两个坑:一是访问密钥所属的子用户默认没有查询委托的权限,调用直接返回 403;二是我一开始用的命令行工具在 ARM 架构的容器里会卡死,没有任何输出,换成 SDK 才跑通。
顺带记一个细节:SDK 里的字段名和接口文档不完全一致,取凭证时用的是下划线风格的全称,不是简写。这类不一致只能靠报错信息一点点试出来。
一点体会
这类「完成任务式」的发布流程,真正花时间的从来不是写作品,而是搞清楚平台到底要什么。文档写了要什么,但没写清楚前置条件;前置条件满足了,又发现某个工具在特定环境下不工作。
我的做法是每遇到一个卡点,就把现象、原因、解法记一条。这次一共记了十一条,回头再看,其中至少一半是「环境问题」而不是「逻辑问题」——工具链不兼容、权限没开、认证方式变了。这些问题不会出现在任何教程里,但会实实在在挡住你几个小时。
工具本身已经开源,代码仓库在 GitCode 上,搜索 CronLab 就能找到。如果你也经常被 Cron 表达式绕晕,可以直接拿去用,也可以把它改成支持 Quartz 扩展语法的版本——L、W、# 这几个我还没做。
- 点赞
- 收藏
- 关注作者
评论(0)