Oracle 解析cron表达式
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wangguogangll/article/details/91359528
1.概述
最近工作中有一个需求,需要在数据库中解析cron定时表达式,获取定时任务的预期运行时间,然后和已运行的任务日志表关联查询,判断定时任务是否按时执行、应用程序是否出现过异常停止的情况。
找了很久没找到合适的,就根据java的CronExpression类中的getNextValidTimeAfter()方法,写了一个Oracle的自定义函数,用于实现根据传入的cron表达式、开始时间,获取下一次定时任务的执行时间。基于这个函数,可以拓展为获取指定时间段内的定时任务运行时间、以某一个时间开始运行多少次的每次运行时间。
2.步骤
解析过程主要分为以下几个步骤:
- 规范cron表达式 ,去除换行符、首尾两端空格、小写转大写、正则校验合规性,保证表达式格式正确;
- 根据 ‘空格’ 分割cron表达式,依次分割为秒、分、时、日、月、星、年(可为空)等6个或7个字段串,同时将月、星中的英文转换为数字;
- 根据 ‘,’ 分割每一个时间位,依次解析每一个分割后的串,获取每一个时间位的预期运行list;
- 将开始时间加一秒,然后获取到每一个时间位的开始值;
- 根据开始值、预期运行list,依次按照年、月、日、时、分、秒的顺序,初始化超范围的下级开始值,例如开始‘年’为2019年,预期运行年的值为【2020-2030】,则应将其他下级位的开始值置为对应位的最小值;
- 根据重新初始化后的开始值、预期运行list,依次按照秒、分、时、日、月、年的顺序,计算对应位的下次运行值,如果当前位的预期值小于开始值,则将它的上级位的开始值进位加1,例如开始‘秒’为23、开始‘分’为9,预期运行秒的值为【5、15、20】,则将‘分’的开始值加1,变为10;
- 根据计算出的每一个时间位的值,判断该时间日期是否合法,如果是2月30日、2月31日、4月31日、6月31日、9月31日、11月31日、非闰年的2月29日(由于上下两个整百非闰年分别为1900年、2100年,因此为了计算方便不考虑整百的非闰年情况),则将开始‘日’置为1、开始‘月’进位加1,回到 【步骤5】 继续计算;
- 返回生成的时间字符串,如果没有符合的则返回为空。
2.1 规范cron表达式
由于本人能力有限,暂时不考虑星期、L、W、C、#的计算,只实现了常规用法的功能,如果有人有好的思路或方法,期待您的共同探讨。
2.1.1 格式详解
字段名 | 字典范围 | 特殊字符 | 必填 |
---|---|---|---|
Seconds | 0-59 | , - * / | 是 |
Minutes | 0-59 | , - * / | 是 |
Hours | 0-23 | , - * / | 是 |
Day-of-month | 1-31 | , - * / ? L W C | 是 |
Month | 1-12 or JAN-DEC | , - * / | 是 |
Day-of-Week | 1-7 or SUN-SAT | , - * / ? L C # | 是 |
Year | 0001-2299 | , - * / | 否 |
* : 用来表示任意值;当"*"出现在"/"前时,表示该字段的开始值。
? : 只能用在“Day-of-month”和“Day-of-Week”这两个字段,表示没有特定的值,且两个字段有且必须有一个值为"?"。
- : 用来表示范围,例如在Hours字段配置“10-12”,解析过来就是小时数为10,11和12都满足。
, : 用来表示枚举值,例如在Day-of-Week字段配置“MON,WED,FRI”,解析过来就是星期一,星期三和星期五都满足。
/ : 用来表示增量逻辑,格式为“初始值/增量值”,例如在Seconds字段配置“5/15”,解析过来就是5,20,35,50都符合。
L : last的简写,只能用在“Day-of-month”和“Day-of-Week”这两个字段。
W : weekday的简写,只能用在“Day-of-month”字段,表示最靠近指定日期的工作日(星期一到星期五)。
C : 用来表示日历,但是浏览了网上已有的cron表达式解析,基本没有人用到此字符,所以本文也不考虑此字符。
# : 只能用在Day-of-Week字段,“m#n”表示这个月的第n个星期m,且n的范围为1-5、m的范围不限;当字段存在"#"时,其他分隔符不起作用,且会找最小值当m;当有"-"存在时,"#"不起作用。
2.1.2 规范cron字符串
--首尾两端去空格/去空白符换行符/去两个以上空格/转大写
cron_var := upper(regexp_replace(replace(replace(replace(trim(cron),chr(9),''),chr(10),''),chr(13),''),'( ){2,}',' '));
--判断格式是否合规,否则提示异常
if regexp_count(cron_var,' ') is null or regexp_count(cron_var,' ') not in (5,6) then
raise_application_error(-20001,'定时字符串格式错误');
end if;
2.2 根据空格切割cron字符串
在此过程中,同时将月、星期中的英文字符转换为数字方便后续计算。
切割完成后应该校验字符串的合规性,本方法暂时没处理,直接按照正确的格式看待。
--取值,根据空格分别获取
cron_seconds := regexp_substr(cron_var,'[^ ]+',1,1);--秒
cron_minutes := regexp_substr(cron_var,'[^ ]+',1,2);--分
cron_hours := regexp_substr(cron_var,'[^ ]+',1,3);--时
cron_day := regexp_substr(cron_var,'[^ ]+',1,4);--日
cron_month := cron_replace_month(regexp_substr(cron_var,'[^ ]+',1,5));--月
cron_week := cron_replace_week(regexp_substr(cron_var,'[^ ]+',1,6));--星
cron_year := nvl(regexp_substr(cron_var,'[^ ]+',1,7),'*');--年
--翻译替换,将月份中的英文字符转换为数字
function cron_replace_month(str varchar2)
return varchar2
as
result_str varchar2(128);
begin
result_str := replace(str,'JAN','1');
result_str := replace(result_str,'FEB','2');
result_str := replace(result_str,'MAR','3');
result_str := replace(result_str,'APR','4');
result_str := replace(result_str,'MAY','5');
result_str := replace(result_str,'JUN','6');
result_str := replace(result_str,'JUL','7');
result_str := replace(result_str,'AUG','8');
result_str := replace(result_str,'SEP','9');
result_str := replace(result_str,'OCT','10');
result_str := replace(result_str,'NOV','11');
result_str := replace(result_str,'DEC','12');
return result_str;
end;
--翻译替换,将星期中的英文字符转换为数字,特别说明英文中星期日为一周的开始
function cron_replace_week(str varchar2)
return varchar2
as
result_str varchar2(128);
begin
result_str := replace(str,'SUN','1');
result_str := replace(result_str,'MON','2');
result_str := replace(result_str,'TUE','3');
result_str := replace(result_str,'WED','4');
result_str := replace(result_str,'THU','5');
result_str := replace(result_str,'FRI','6');
result_str := replace(result_str,'SAT','7');
return result_str;
end;
2.3 根据‘,’分割每一个时间位,获取预期运行list
--获取预期的运行数组
--为了提高效率,本函数将年的范围限定为1949年-2049年,如果有实际需要,可以酌情调整
array_seconds := cron_get_str_array(cron_seconds,0,59);
array_minutes := cron_get_str_array(cron_minutes,0,59);
array_hours := cron_get_str_array(cron_hours,0,23);
array_day := cron_get_str_array(cron_day,1,31);
array_month := cron_get_str_array(cron_month,1,12);
array_week := cron_get_str_array(cron_week,1,7);
array_year := cron_get_str_array(cron_year,1949,2049);
--获取运行数组
function cron_get_str_array(str varchar2,min_num number,max_num number)
return cron_type_number is
temp_result_array cron_type_number;
num number default 1;
temp_array_1 cron_type_array;--临时数组1
temp_array_2 l_cron_type_number;--临时数组2
begin
temp_result_array := cron_type_number();
temp_array_1 := cron_get_str(str);
for i in 1..temp_array_1.count loop
temp_array_2 := cron_get_array(temp_array_1(i),min_num,max_num);
temp_result_array.extend(temp_array_2.count);
for j in 1..temp_array_2.count loop
temp_result_array(num) := temp_array_2(j);
num := num+1;
end loop;
end loop;
return temp_result_array;
end;
--根据','分割字符串,生成list
function cron_get_str(str varchar2)
return cron_type_array is
temp_result_array cron_type_array;
begin
for i in 1..regexp_count(str,'[^,]+')+1 loop
temp_result_array(i) := regexp_substr(str,'[^,]+',1,i);
end loop;
return temp_result_array;
end;
--解析字符串,返回运行数组
function cron_get_array(str varchar2,min_num number,max_num number)
return l_cron_type_number is
temp_result_array_number l_cron_type_number;
num number default 1;
i number default 0;
temp_str_1 varchar2(128);
temp_str_2 varchar2(128);
temp_str_3 varchar2(128);
temp_str_4 varchar2(128);
temp_min_num number default -1;