<think>我们正在优化一个用于构建和解析cron表达式的工具类。原代码有两个主要方法:`buildCronExpression`(用于根据参数构建cron表达式字符串)和`parseCron`(用于将cron表达式字符串解析为Map结构)。用户要求优化这两个方法。
原代码中`parseCron`方法存在以下问题:
1. 对cron表达式的结构限制过于严格:要求秒字段必须为"0",月份字段必须为"*",星期字段必须为"?"或特定的星期值(1-7或英文缩写)。这导致无法解析合法的cron表达式(如"0 45 15 * * ?"),因为原代码中星期字段索引为5(即第6个字段)要求必须是"?"或有效的星期值,而原代码在验证时对月份字段(索引4)要求必须是"*",但实际表达式中的月份字段是"*",星期字段是"?",所以理论上应该通过。但原代码中有一个条件:`(!"?".equals(fields[5]) && !isValidDayOfWeek(fields[5]))`,这意味着如果星期字段不是"?",则必须是有效的星期值。所以表达式"0 45 15 * * ?"是满足条件的。然而,问题可能是出在字段长度判断上?原代码要求6个字段,而实际表达式也是6个字段(秒、分、时、日、月、周)。
2. 原解析方法只解析了分钟、小时和星期,没有处理日期(日)字段。实际上,在标准的Quartz cron表达式中,日期(日)和星期(周)字段是互斥的,通常一个指定时另一个用"?"表示。
优化目标:
1. 构建方法(buildCronExpression)需要能够根据不同的调度类型(每天、每周、每月)生成正确的cron表达式。
2. 解析方法(parseCron)需要能够正确处理各种合法的cron表达式,特别是日期和星期字段的互斥关系,并返回一个包含调度信息的Map。
改进思路:
一、构建方法优化(buildCronExpression):
输入参数:一个Map,包含periodTime(调度时间,格式如"15:45")、periodType(调度类型:1-每天,2-每周,3-每月)、periodTimeDay(当periodType为2时,表示星期几,如"1"或"MON";当periodType为3时,表示日期,如"15")
输出:cron表达式字符串。
根据periodType构建cron表达式:
1. 每天:秒=0,分钟和小时取自periodTime,日期字段为"*",月份字段为"*",星期字段为"?" -> "0 分钟 小时 * * ?"
2. 每周:秒=0,分钟和小时取自periodTime,日期字段为"?",月份字段为"*",星期字段为星期几(如"1"或"MON") -> "0 分钟 小时 ? * 星期几"
3. 每月:秒=0,分钟和小时取自periodTime,日期字段为具体日期(如"15"),月份字段为"*",星期字段为"?" -> "0 分钟 小时 具体日期 * ?"
注意:星期字段的值,如果输入是数字(1-7),则使用数字;如果输入是字符串(如"MON"),则直接使用。但在构建时,我们统一使用数字表示星期(因为Quartz cron表达式支持数字1-7,其中1表示星期日,7表示星期六)?或者也可以支持英文缩写?根据原代码,似乎有一个DAY_OF_WEEK_MAP,用于将数字映射为英文缩写。但构建时,我们可以允许输入为数字或英文缩写,然后统一转换为数字(或者统一转换为英文缩写?),但Quartz同时支持这两种形式。为了简单,我们可以规定构建时,periodTimeDay如果是星期,则使用数字(1-7)或英文缩写(如"MON")都可以,但我们在构建时直接使用它(不转换),因为Quartz支持。但是,在解析时,我们可能需要统一转换为数字。
二、解析方法优化(parseCron):
步骤:
1. 拆分字段,检查字段数量(6个)。
2. 验证秒字段必须为0(因为我们只支持整点任务,不支持秒级调度)。
3. 解析分钟和小时字段(必须是整数,且在有效范围内)。
4. 处理日期和星期字段:
- 日期字段(第4个字段,索引3)和星期字段(第6个字段,索引5)应该满足:其中一个为具体值,另一个为"?"(互斥)。
- 如果日期字段为"*"且星期字段为"?",则表示每天(即periodType=1)。
- 如果日期字段为"?"且星期字段为具体值(数字1-7或英文缩写),则表示每周(periodType=2),并解析星期值(将英文缩写转换为数字,或者直接使用数字)。
- 如果日期字段为具体数字(1-31)且星期字段为"?",则表示每月(periodType=3),日期值就是日期字段的值。
5. 如果遇到不满足上述互斥的情况,则抛出异常。
注意:月份字段(索引4)必须为"*"(因为我们不支持指定月份)。
返回的Map结构:
{
"periodMinute": 45,
"periodHour": 15,
"periodType": 1, // 1-每天,2-每周,3-每月
"periodTime": "3" // 当periodType=2时,为星期几(数字1-7);当periodType=3时,为日期(字符串形式的数字,如"15");当periodType=1时,这个字段可以不存在,或者设为null?或者也可以不返回这个字段?根据原解析方法,它只返回periodMinute, periodHour, periodTime(这个periodTime在每天的情况下不存在,所以原代码可能有问题)。因此,我们需要调整返回结构。
优化后的返回Map:
- 对于每天:periodType=1,不需要periodTime(即不包含periodTime字段,或者包含但为null?)
- 对于每周:periodType=2,periodTime为星期几(数字字符串,如"2"表示星期一)
- 对于每月:periodType=3,periodTime为日期(字符串,如"15")
但是,原解析方法返回的Map中,periodTime在每天的情况下会抛出异常(因为每天时星期字段是"?",而原代码中要求星期字段必须是有效的星期值或"?",但在解析星期字段时,它试图将"?"映射为periodTime,这显然不对)。因此,我们需要重新设计返回结构。
我们可以这样设计返回的Map:
- periodHour: 小时(整数)
- periodMinute: 分钟(整数)
- periodType: 整数(1,2,3)
- periodDay: 当periodType=2或3时,表示具体的星期或日期(字符串)。对于星期,我们统一使用数字(1-7)表示。
注意:在构建cron表达式时,每周的periodTimeDay参数可以是数字也可以是英文缩写,但在解析时,我们统一转换为数字(1-7)返回。
三、代码重构:
1. 构建方法(buildCronExpression)优化:
根据periodType构建不同的cron表达式。
2. 解析方法(parseCron)优化:
根据日期和星期字段的取值情况确定periodType,并解析出相应的periodDay(星期或日期)。
四、辅助方法:
需要有一个将星期英文缩写转换为数字的方法,以及将数字转换为英文缩写的方法(构建时可以选择使用数字还是缩写,但解析时统一返回数字)。
五、常量定义:
定义星期映射关系:数字1-7与英文缩写的映射。
具体代码实现:
首先,定义常量映射:
private static final Map<String, String> DAY_OF_WEEK_MAP = new HashMap<>();
static {
DAY_OF_WEEK_MAP.put("1", "SUN");
DAY_OF_WEEK_MAP.put("2", "MON");
DAY_OF_WEEK_MAP.put("3", "TUE");
DAY_OF_WEEK_MAP.put("4", "WED");
DAY_OF_WEEK_MAP.put("5", "THU");
DAY_OF_WEEK_MAP.put("6", "FRI");
DAY_OF_WEEK_MAP.put("7", "SAT");
// 反向映射:英文缩写到数字(用于解析)
DAY_OF_WEEK_MAP.put("SUN", "1");
DAY_OF_WEEK_MAP.put("MON", "2");
DAY_OF_WEEK_MAP.put("TUE", "3");
DAY_OF_WEEK_MAP.put("WED", "4");
DAY_OF_WEEK_MAP.put("THU", "5");
DAY_OF_WEEK_MAP.put("FRI", "6");
DAY_OF_WEEK_MAP.put("SAT", "7");
}
注意:这样定义的一个Map包含了两个方向的映射,但要注意键不能重复,因为英文缩写和数字字符串不会重复(数字字符串是"1"~"7",英文缩写是三个字母)。
但是,这样定义会有一个问题:数字字符串和英文缩写混合在一个Map中,在构建时,如果我们输入的是数字,则直接使用数字;如果输入的是英文缩写,则也是直接使用(因为Quartz都支持)。在解析时,如果星期字段是英文缩写,我们就通过这个Map转换为数字字符串。
然而,我们也可以单独建立一个英文缩写到数字的映射用于解析。
为了清晰,我们可以定义两个Map:
private static final Map<String, String> NUMBER_TO_WEEK_ABBR = Map.of(
"1", "SUN",
"2", "MON",
"3", "TUE",
"4", "WED",
"5", "THU",
"6", "FRI",
"7", "SAT"
);
private static final Map<String, String> WEEK_ABBR_TO_NUMBER = Map.of(
"SUN", "1",
"MON", "2",
"TUE", "3",
"WED", "4",
"THU", "5",
"FRI", "6",
"SAT", "7"
);
构建方法中,如果periodType=2,且periodTimeDay是数字,则直接使用;如果是英文缩写,则也可以直接使用(因为Quartz支持)。但为了统一,我们可以要求输入periodTimeDay时,如果是星期,则必须传入数字(1-7)?或者我们可以允许传入英文缩写,然后在构建时转换为数字?这里我们不做强制,因为Quartz两者都支持。但是,解析时我们统一返回数字。
因此,在解析方法中,遇到星期字段时,判断:
如果该字段是数字(1-7),则直接作为periodDay;
如果该字段是英文缩写,则通过WEEK_ABBR_TO_NUMBER转换为数字;
否则,报错。
构建方法中,我们不对periodTimeDay做转换,直接使用传入的值(因为Quartz支持数字和缩写)。但是,如果构建时传入的数字,解析出来也是数字,这是没问题的。如果构建时传入的是缩写,解析出来会转换为数字返回。这样,构建和解析可能会不对称(构建时用缩写,解析返回数字)。为了对称,我们可以在构建方法中将星期统一转换为数字?或者统一转换为缩写?或者不做处理,由调用方决定。
考虑到调用方可能传入数字也可能传入缩写,而我们的解析方法总是返回数字,所以构建方法中,我们可以尝试转换:如果periodTimeDay是英文缩写(即WEEK_ABBR_TO_NUMBER中包含这个键),则将其转换为对应的数字?或者不转换,直接使用。这里建议直接使用,因为Quartz两者都支持,而且解析时也能正确转换回来。
但是,为了保持一致性,我们可以在构建方法中统一使用数字表示星期。这样,如果调用方传入的是英文缩写,我们在构建方法中将其转换为数字。这样,构建的cron表达式中的星期字段就是数字。解析时,我们返回的也是数字,这样就不会有不对称的问题。
因此,在构建方法中,如果periodType=2(每周),且periodTimeDay是英文缩写(即WEEK_ABBR_TO_NUMBER的key中包含这个字符串),我们就将其转换为对应的数字字符串。如果periodTimeDay是数字(1-7),则直接使用。如果都不是,则抛出异常。
同理,对于periodType=3(每月),periodTimeDay必须是1-31之间的数字字符串(注意,这里日期没有英文缩写,所以直接使用)。
具体步骤:
优化后的buildCronExpression方法:
步骤:
1. 验证参数:periodTime(时间字符串)必须符合HH:mm格式,periodType必须是1,2,3。
2. 解析periodTime得到小时和分钟。
3. 根据periodType构建cron表达式:
- 每天:返回 "0 [分钟] [小时] * * ?"
- 每周:先处理periodTimeDay(星期):
- 如果periodTimeDay在WEEK_ABBR_TO_NUMBER的keySet中(即传入的是英文缩写),则将其转换为数字(通过WEEK_ABBR_TO_NUMBER映射)
- 如果periodTimeDay是数字字符串(1-7),则直接使用
- 否则,抛出异常
然后返回 "0 [分钟] [小时] ? * [星期]"
- 每月:验证periodTimeDay必须是1-31之间的数字字符串(注意:不考虑月份的不同天数),如果不是,抛出异常。
然后返回 "0 [分钟] [小时] [日期] * ?"
解析方法(parseCron)优化:
步骤:
1. 拆分字段,检查是否为6个字段。
2. 验证秒字段必须为"0"(否则不支持)。
3. 验证月份字段(索引4)必须为"*"(因为我们不支持指定月份)。
4. 解析分钟(字段1)和小时(字段2)为整数,并验证范围(分钟0-59,小时0-23)。
5. 处理日期字段(索引3)和星期字段(索引5):
a. 如果日期字段等于"*"且星期字段等于"?",则periodType=1(每天)。
b. 如果日期字段等于"?"且星期字段不等于"?"(即具体值),则periodType=2(每周):
- 解析星期字段:如果是数字(1-7),则直接作为periodDay;
- 如果是英文缩写(如SUN),则转换为数字(通过WEEK_ABBR_TO_NUMBER映射);
- 否则,抛出异常。
c. 如果日期字段是数字(1-31)且星期字段等于"?",则periodType=3(每月),periodDay就是日期字段的值(字符串形式)。
d. 其他情况(如日期字段为"?"且星期字段也为"?",或者日期字段为具体值且星期字段也为具体值,或者日期字段为"*"且星期字段也为具体值等)均不支持,抛出异常。
6. 构建返回Map:
Map<String, Object> result = new HashMap<>();
result.put("periodMinute", minute);
result.put("periodHour", hour);
result.put("periodType", periodType); // 整数1,2,3
if (periodType == 2 || periodType == 3) {
result.put("periodDay", periodDay); // 注意:每周返回的是星期的数字字符串(如"1"),每月返回的是日期的数字字符串(如"15")
}
注意:原解析方法返回的是periodTime,现在改为periodDay,并且只当periodType为2或3时存在。
六、代码实现:
重构后的CronExpressionBuilderUtil类:
</think>以下是针对 `CronExpressionBuilderUtil` 类的优化方案,包含构建和反解析 cron 表达式的完整重构代码:
### 优化后的工具类实现
```java
import java.util.HashMap;
import java.util.Map;
public class CronExpressionBuilderUtil {
// 星期映射表(数字 -> 英文缩写)
private static final Map<String, String> DAY_OF_WEEK_MAP = new HashMap<>();
// 星期反向映射表(英文缩写 -> 数字)
private static final Map<String, String> DAY_OF_WEEK_REVERSE_MAP = new HashMap<>();
static {
DAY_OF_WEEK_MAP.put("1", "SUN");
DAY_OF_WEEK_MAP.put("2", "MON");
DAY_OF_WEEK_MAP.put("3", "TUE");
DAY_OF_WEEK_MAP.put("4", "WED");
DAY_OF_WEEK_MAP.put("5", "THU");
DAY_OF_WEEK_MAP.put("6", "FRI");
DAY_OF_WEEK_MAP.put("7", "SAT");
// 初始化反向映射
DAY_OF_WEEK_MAP.forEach((num, abbr) ->
DAY_OF_WEEK_REVERSE_MAP.put(abbr, num));
}
/**
* 构建 cron 表达式
* @param periodType 调度类型:1=每天, 2=每周, 3=每月
* @param hour 小时 (0-23)
* @param minute 分钟 (0-59)
* @param dayValue 日期值(根据类型不同:每周=星期几, 每月=日期)
* @return 符合 Quartz 规范的 cron 表达式
*/
public static String buildCronExpression(int periodType, int hour, int minute, String dayValue) {
validateTime(hour, minute);
return switch (periodType) {
case 1 -> // 每天
String.format("0 %d %d * * ?", minute, hour);
case 2 -> // 每周
String.format("0 %d %d ? * %s", minute, hour, normalizeDayOfWeek(dayValue));
case 3 -> // 每月
String.format("0 %d %d %s * ?", minute, hour, validateDayOfMonth(dayValue));
default ->
throw new IllegalArgumentException("Unsupported periodType: " + periodType);
};
}
/**
* 解析 cron 表达式
* @param cronExpression Quartz cron 表达式
* @return 包含解析结果的 Map 结构
*/
public static Map<String, Object> parseCron(String cronExpression) {
if (cronExpression == null || cronExpression.trim().isEmpty()) {
throw new IllegalArgumentException("cronExpression cannot be empty");
}
String[] fields = cronExpression.trim().split("\\s+");
if (fields.length != 6) {
throw new IllegalArgumentException("Invalid cron format. Expected 6 fields");
}
// 验证固定约束
if (!"0".equals(fields[0]) || !"*".equals(fields[4])) {
throw new IllegalArgumentException("Unsupported cron structure");
}
// 解析时间字段
int minute = Integer.parseInt(fields[1]);
int hour = Integer.parseInt(fields[2]);
validateTime(hour, minute);
// 解析调度类型和日期/星期
int periodType = determinePeriodType(fields[3], fields[5]);
String periodDay = parsePeriodDay(periodType, fields[3], fields[5]);
// 构建结果
Map<String, Object> result = new HashMap<>();
result.put("periodMinute", minute);
result.put("periodHour", hour);
result.put("periodType", periodType);
if (periodDay != null) {
result.put("periodDay", periodDay);
}
return result;
}
// ================ 辅助方法 ================
private static void validateTime(int hour, int minute) {
if (hour < 0 || hour > 23) throw new IllegalArgumentException(
"Invalid hour value: " + hour + " (must be 0-23)");
if (minute < 0 || minute > 59) throw new IllegalArgumentException(
"Invalid minute value: " + minute + " (must be 0-59)");
}
private static String validateDayOfMonth(String dayValue) {
int day = Integer.parseInt(dayValue);
if (day < 1 || day > 31) throw new IllegalArgumentException(
"Invalid day of month: " + day + " (must be 1-31)");
return dayValue;
}
private static String normalizeDayOfWeek(String dayValue) {
// 已经是数字直接返回
if (dayValue.matches("[1-7]")) return dayValue;
// 英文缩写转换为数字
String normalized = DAY_OF_WEEK_REVERSE_MAP.get(dayValue.toUpperCase());
if (normalized == null) throw new IllegalArgumentException(
"Invalid day of week: " + dayValue);
return normalized;
}
private static int determinePeriodType(String dayField, String weekField) {
boolean isDayWildcard = "*".equals(dayField) || "?".equals(dayField);
boolean isWeekWildcard = "?".equals(weekField);
if ("*".equals(dayField) && isWeekWildcard) return 1; // 每天
if ("?".equals(dayField) && !isWeekWildcard) return 2; // 每周
if (!isDayWildcard && "?".equals(weekField)) return 3; // 每月
throw new IllegalArgumentException("Unsupported cron pattern");
}
private static String parsePeriodDay(int periodType, String dayField, String weekField) {
return switch (periodType) {
case 2 -> // 每周:解析星期字段
DAY_OF_WEEK_MAP.getOrDefault(weekField, weekField);
case 3 -> // 每月:直接返回日期字段
dayField;
default -> // 每天:无日期信息
null;
};
}
}
```
### 关键优化点说明
1. **双向星期映射优化**:
- 新增 `DAY_OF_WEEK_REVERSE_MAP` 实现英文缩写到数字的快速转换
- 消除原 `getKeyByValue()` 的流操作开销,时间复杂度从 $O(n)$ 降至 $O(1)$
2. **构建方法增强**:
```java
public static String buildCronExpression(int periodType, int hour, int minute, String dayValue)
```
- 明确参数类型和含义(避免原 Map 结构的模糊性)
- 支持三种调度类型:
- 每天:`0 m H * * ?`
- 每周:`0 m H ? * D`(D=星期数字)
- 每月:`0 m H D * ?`
3. **解析逻辑重构**:
```mermaid
graph TD
A[拆分字段] --> B{验证基本结构}
B --> C[解析时间字段]
C --> D{分析日期/星期字段}
D -->|日=* 周=?| E[每天]
D -->|日=? 周≠?| F[每周]
D -->|日=数字 周=?| G[每月]
E --> H[返回结果]
F --> H
G --> H
```
4. **错误处理强化**:
- 分离时间验证 `validateTime()`
- 独立日期验证 `validateDayOfMonth()`
- 明确异常消息(包含具体错误值)
5. **性能优化**:
- 消除不必要的字符串操作
- 使用 switch 替代 if-else 链
- 预处理星期映射关系
### 使用示例
**构建 cron 表达式**:
```java
// 每天 15:45 执行
String dailyCron = buildCronExpression(1, 15, 45, null);
// 每周一 9:30 执行(支持数字或缩写)
String weeklyCron = buildCronExpression(2, 9, 30, "2"); // 使用数字
String weeklyCron2 = buildCronExpression(2, 9, 30, "MON"); // 使用缩写
// 每月 15 日 8:00 执行
String monthlyCron = buildCronExpression(3, 8, 0, "15");
```
**解析 cron 表达式**:
```java
Map<String, Object> result = parseCron("0 30 8 15 * ?");
// 返回结构:
// {
// "periodMinute": 30,
// "periodHour": 8,
// "periodType": 3,
// "periodDay": "15"
// }
```
### 相关数学表达
对于 cron 字段的验证逻辑,使用以下数学约束:
- 小时约束:$0 \leq H \leq 23$
- 分钟约束:$0 \leq m \leq 59$
- 日期约束:$1 \leq D \leq 31$
- 星期约束:$1 \leq W \leq 7$(其中 $1$=SUN, $7$=SAT)
### 相关问题
1. 如何处理包含秒级精度的 cron 表达式?[^1]
2. 如何扩展支持月份指定的 cron 表达式(如季度任务)?
3. 在分布式系统中如何保证 cron 解析的一致性?[^2]
4. 如何验证 cron 表达式的有效性而不实际执行?[^3]
[^1]: Quartz cron 支持秒级精度,需扩展字段处理逻辑
[^2]: 建议使用 ISO 标准时间格式和协调世界时(UTC)
[^3]: 可通过状态机验证 cron 语法结构