Java开发过程中经常会用到定时任务job的场景,比如定时处理数据报表等问题,开源作业调度框架也非常多,常用的开源作业调度框架有:Spring Task、Quartz和xxl-job等。各个框架的具体使用不再这里讨论,这里主要讨论一下其中cron表达式的计算应用,xk-time中的应用。

1.Spring Task中cron表达式的计算应用

1.1 源码

Spring Task包含在spring-context的jar包中,org.springframework.scheduling.support.CronSequenceGenerator类。

(1)基本属性,秒 分 小时 月份中的日期 月份 星期中的星期 总共6位

public class CronSequenceGenerator {

    private final String expression;//cron表达式

    private final TimeZone timeZone;//时区

    private final BitSet months = new BitSet(12);//月

    private final BitSet daysOfMonth = new BitSet(31);//日

    private final BitSet daysOfWeek = new BitSet(7);//星期

    private final BitSet hours = new BitSet(24);//小时

    private final BitSet minutes = new BitSet(60);//分钟

    private final BitSet seconds = new BitSet(60);//秒

(2)表达式中支持的特殊字符:, - * ? / 分别是枚举,范围,任意值,任意值,间隔,不同字段使用有所区别,后面详细说明。

详细见 org.springframework.scheduling.support.CronSequenceGenerator.parse(String)

2.Quartz和xxl-job中cron表达式的计算应用

Quartz中的是org.quartz.CronExpression 在所有的cron表达式中使用比较广泛,xxl-job中借用了Quartz中的CronExpression com.xxl.job.admin.core.cron.CronExpression。

2.1 源码

(1)基本属性,秒 分钟 小时 天 月份 星期 年 总共7位,比SpringTask多一位年

    private final String cronExpression;//cron表达式
private TimeZone timeZone = null;//时区
protected transient TreeSet<Integer> seconds;//秒
protected transient TreeSet<Integer> minutes;//分钟
protected transient TreeSet<Integer> hours;//小时
protected transient TreeSet<Integer> daysOfMonth;//天
protected transient TreeSet<Integer> months;//月
protected transient TreeSet<Integer> daysOfWeek;//星期
protected transient TreeSet<Integer> years;//年

(2)表达式中支持的特殊字符:, - * ? /  L W #分别是枚举,范围,任意值,任意值,间隔,最后,有效工作日(周一到周五),每个月第几个星期几,不同字段使用有所区别,后面详细说明。比SpringTask多了3个 L W #。

详细见 org.quartz.CronExpression.buildExpression(String)

3.Cron表达式规则详解

从1和2中源码看出,Spring Task中cron规则为Quartz的子集。Quartz的使用更为广泛。下面以Quartz为例介绍:

Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下语法格式:

seconds minutes hours daysOfMonth months daysOfWeek years(可选)  其中years为可选。

3.1、结构

  corn从左到右(用空格隔开):秒 分 小时 日期 月份 星期 年份

3.2、各字段的含义

 
字段 允许值 允许的特殊字符
秒(Seconds) 0~59的整数 , - * /    四个字符
分(Minutes 0~59的整数 , - * /    四个字符
小时(Hours 0~23的整数 , - * /    四个字符
日期(DayofMonth 1~31的整数(但是你需要考虑你月的天数) ,- * ? / L W C     八个字符
月份(Months 1~12的整数或者 JAN-DEC , - * /    四个字符
星期(DayofWeek 1~7的整数或者 SUN-SAT (1=SUN) , - * ? / L C #     八个字符
年(可选,留空)(Years 1970~2099 , - * /    四个字符

  注意事项:

  每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

  (1)*:表示匹配该域的任意值。假如在Minutes域使用*, 即表示每分钟都会触发事件。

  (2)?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 * ?, 其中最后一位只能用?,而不能使用*,如果使用*表示不管星期几都会触发,实际上并不是这样。

  (3)-:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次

  (4)/:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.

  (5),:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。

  (6)L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。

  (7)W:表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。

  (8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

  (9)#:用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。

3.3、常用表达式例子

  (1)0 0 2 1 * ? *   表示在每月的1日的凌晨2点调整任务

  (2)0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业

  (3)0 15 10 ? * 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作

  (4)0 0 10,14,16 * * ?   每天上午10点,下午2点,4点

  (5)0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时

  (6)0 0 12 ? * WED    表示每个星期三中午12点

  (7)0 0 12 * * ?   每天中午12点触发

  (8)0 15 10 ? * *    每天上午10:15触发

  (9)0 15 10 * * ?     每天上午10:15触发

  (10)0 15 10 * * ? *    每天上午10:15触发

  (11)0 15 10 * * ? 2005    2005年的每天上午10:15触发

  (12)0 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发

  (13)0 0/5 14 * * ?    在每天下午2点到下午2:55期间的每5分钟触发

  (14)0 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

  (15)0 0-5 14 * * ?    在每天下午2点到下午2:05期间的每1分钟触发

  (16)0 10,44 14 ? 3 WED    每年三月的星期三的下午2:10和2:44触发

  (17)0 15 10 ? * MON-FRI    周一至周五的上午10:15触发

  (18)0 15 10 15 * ?    每月15日上午10:15触发

  (19)0 15 10 L * ?    每月最后一日的上午10:15触发

  (20)0 15 10 ? * 6L    每月的最后一个星期五上午10:15触发

  (21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发

  (22)0 15 10 ? * 6#3   每月的第三个星期五上午10:15触发

  

  注:

  (1)有些子表达式能包含一些范围或列表

  例如:子表达式(天(星期))可以为 “MON-FRI”,“MON,WED,FRI”,“MON-WED,SAT”

“*”字符代表所有可能的值

  因此,“*”在子表达式(月)里表示每个月的含义,“*”在子表达式(天(星期))表示星期的每一天

  “/”字符用来指定数值的增量 
  例如:在子表达式(分钟)里的“0/15”表示从第0分钟开始,每15分钟 
在子表达式(分钟)里的“3/20”表示从第3分钟开始,每20分钟(它和“3,23,43”)的含义一样

  “?”字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值 
  当2个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为“?”

  “L” 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写 
  但是它在两个子表达式里的含义是不同的。 
  在天(月)子表达式中,“L”表示一个月的最后一天 
  在天(星期)自表达式中,“L”表示一个星期的最后一天,也就是SAT

  如果在“L”前有具体的内容,它就具有其他的含义了

  例如:“6L”表示这个月的倒数第6天,“FRIL”表示这个月的最一个星期五 
  注意:在使用“L”参数时,不要指定列表或范围,因为这会导致问题

4.xk-time中的cron表达式工具 CronExpressionUtil

  本来想在xk-time中自己实现cron表达式解析和计算,但cron表达式是一个标准,有成熟的实现方案,并且后续维护也是问题,xxl-job其中使用了Quartz,Quartz会不断维护相关源码,这里也使用Quartz的CronExpression,并会定期根据Quartz更新代码。

包含
(1)验证和格式化Cron表达式方法,isValidExpression和formatExpression。
(2)生成下一个或多个执行时间方法,getNextTime和getNextTimeList。
(3)生成下一个或多个执行时间的日期格式化(yyyy-MM-dd HH:mm:ss)方法,getNextTimeStr和getNextTimeStrList。
(4)对比Cron表达式下一个执行时间是否与指定date相等方法,isSatisfiedBy。

注意: 底层使用 quartz的CronExpression处理。

4.1 源码

package com.xkzhangsan.time.cron;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List; import com.xkzhangsan.time.formatter.DateTimeFormatterUtil;
import com.xkzhangsan.time.utils.CollectionUtil; /**
* Cron表达式工具
* 包含
* 1.验证和格式化Cron表达式方法,isValidExpression和formatExpression
* 2.生成下一个或多个执行时间方法,getNextTime和getNextTimeList
* 3.生成下一个或多个执行时间的日期格式化(yyyy-MM-dd HH:mm:ss)方法,getNextTimeStr和getNextTimeStrList
* 4.对比Cron表达式下一个执行时间是否与指定date相等方法,isSatisfiedBy
*
* @ClassName: CronExpressionUtil
* @Description: CronExpressionUtil
* @author xkzhangsan
* @date 2020年4月16日
*
*使用 quartz CronExpression
*/
public class CronExpressionUtil { private CronExpressionUtil(){
} /**
* 验证Cron表达式
* @param cronExpression
* @return
*/
public static boolean isValidExpression(String cronExpression){
return CronExpression.isValidExpression(cronExpression);
} /**
* 格式化Cron表达式
* @param cronExpression
* @return
*/
public static String formatExpression(String cronExpression){
try {
return new CronExpression(cronExpression).toString();
} catch (ParseException e) {
throw new RuntimeException(e.getMessage());
}
} /**
* 生成下一个执行时间
* @param cronExpression
* @param date
* @return
*/
public static Date getNextTime(String cronExpression, Date date){
try {
if(date != null){
return new CronExpression(cronExpression).getNextValidTimeAfter(date);
}else{
return new CronExpression(cronExpression).getNextValidTimeAfter(new Date());
}
} catch (ParseException e) {
throw new RuntimeException(e.getMessage());
}
} /**
* 生成下一个执行时间
* @param cronExpression
* @return
*/
public static Date getNextTime(String cronExpression){
return getNextTime(cronExpression, null);
} /**
* 生成下一个执行时间的日期格式化
* @param cronExpression
* @param date
* @return 返回格式化时间 yyyy-MM-dd HH:mm:ss
*/
public static String getNextTimeStr(String cronExpression, Date date){
return DateTimeFormatterUtil.formatToDateTimeStr(getNextTime(cronExpression, date));
} /**
* 生成下一个执行时间的日期格式化
* @param cronExpression
* @return 返回格式化时间 yyyy-MM-dd HH:mm:ss
*/
public static String getNextTimeStr(String cronExpression){
return getNextTimeStr(cronExpression, null);
} /**
* 生成多个执行时间
* @param cronExpression
* @param date
* @param num
* @return
*/
public static List<Date> getNextTimeList(String cronExpression, Date date, int num){
List<Date> dateList = new ArrayList<>();
if(num < 1){
throw new RuntimeException("num must be greater than 0");
}
Date startDate = date != null ? date : new Date();
for(int i=0; i<num; i++){
startDate = getNextTime(cronExpression, startDate);
if(startDate != null){
dateList.add(startDate);
}
}
return dateList;
} /**
* 生成多个执行时间
* @param cronExpression
* @param num
* @return
*/
public static List<Date> getNextTimeList(String cronExpression, int num){
return getNextTimeList(cronExpression, null, num);
} /**
* 生成多个执行时间的日期格式化
* @param cronExpression
* @param date
* @param num
* @return 返回格式化时间 yyyy-MM-dd HH:mm:ss
*/
public static List<String> getNextTimeStrList(String cronExpression, Date date, int num){
List<String> dateStrList = new ArrayList<>();
List<Date> dateList = getNextTimeList(cronExpression, date, num);
if(CollectionUtil.isNotEmpty(dateList)){
dateList.stream().forEach(d->{
String dateStr = DateTimeFormatterUtil.formatToDateTimeStr(d);
dateStrList.add(dateStr);
});
}
return dateStrList;
} /**
* 生成多个执行时间的日期格式化
* @param cronExpression
* @param num
* @return 返回格式化时间 yyyy-MM-dd HH:mm:ss
*/
public static List<String> getNextTimeStrList(String cronExpression, int num){
return getNextTimeStrList(cronExpression, null, num);
} /**
* 对比Cron表达式下一个执行时间是否与指定date相等
* @param cronExpression
* @param date
* @return
*/
public static boolean isSatisfiedBy(String cronExpression, Date date){
try {
return new CronExpression(cronExpression).isSatisfiedBy(date);
} catch (ParseException e) {
throw new RuntimeException(e.getMessage());
}
} }

4.2 测试代码

package com.xkzhangsan.time.test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List; import org.junit.Test; import com.xkzhangsan.time.cron.CronExpressionUtil;
import com.xkzhangsan.time.formatter.DateTimeFormatterUtil; public class CronExpressionTest { /**
* Cron表达式工具测试
*/
@Test
public void cronExpressionTest(){
//验证cron,0 0 2 1 * ? * 表示在每月的1日的凌晨2点触发
System.out.println(CronExpressionUtil.isValidExpression("0 0 2 1 * ? *"));
//格式化cron
System.out.println(CronExpressionUtil.formatExpression(" * * * * * ? *"));
//下一个运行时间
System.out.println(CronExpressionUtil.getNextTimeStr("10 * * * * ?"));
//多个下一个运行时间
System.out.println(CronExpressionUtil.getNextTimeStrList("10 * * * * ?", 10));
//对比Cron表达式下一个执行时间是否与指定date相等
Date date = new Date();
String cronExpression = "0 10 * * * ?";
Date nextDate = CronExpressionUtil.getNextTime(cronExpression, date);
System.out.println(CronExpressionUtil.isSatisfiedBy(cronExpression, nextDate));
} /**
* 特殊周期cron,不能被字段范围整除的周期值,比如:每隔40s执行,需要多个cron组合使用
*/
@Test
public void cronExpressionSpecialPeriodTest(){
List<Date> dateList = new ArrayList<Date>();
dateList.addAll(CronExpressionUtil.getNextTimeList("0 0/2 * * * ? ", 3));
dateList.addAll(CronExpressionUtil.getNextTimeList("40 0/2 * * * ? ", 3));
dateList.addAll(CronExpressionUtil.getNextTimeList("20 1/2 * * * ? ", 3));
Collections.sort(dateList);
dateList.stream().forEach(date->{
System.out.println(DateTimeFormatterUtil.formatToDateTimeStr(date));
});
}
}

输出:

true
* * * * * ? *
2020-04-17 09:44:10
[2020-04-17 09:44:10, 2020-04-17 09:45:10, 2020-04-17 09:46:10, 2020-04-17 09:47:10, 2020-04-17 09:48:10, 2020-04-17 09:49:10, 2020-04-17 09:50:10, 2020-04-17 09:51:10, 2020-04-17 09:52:10, 2020-04-17 09:53:10]
true

2020-04-30 17:57:20
2020-04-30 17:58:00
2020-04-30 17:58:40
2020-04-30 17:59:20
2020-04-30 18:00:00
2020-04-30 18:00:40
2020-04-30 18:01:20
2020-04-30 18:02:00
2020-04-30 18:02:40

提示:

特殊周期cron,不能被字段范围整除的周期值,比如:每隔40s执行,需要多个cron组合使用 需要多个cron 组合使用(1) 0 0/3 * * * ? (2)40 1/3 * * * ? (3)20 2/3 * * * ?

如果只使用,0/40 * * * * ? 会出现执行间隔为 40,20.40等,达不到希望的效果。

参考:https://www.cnblogs.com/yanghj010/p/10875151.html

源代码地址:https://github.com/xkzhangsan/xk-time

Java日期时间API系列29-----Jdk8中java.time包中的新的日期时间API类,Java定时任务job中cron表达式计算应用。的更多相关文章

  1. Andriod项目开发实战(1)——如何在Eclipse中的一个包下建新包

    最开始是想将各个类分门别类地存放在不同的包中,所以想在项目源码包中新建几个不同功能的包eg:utils.model.receiver等,最后的结果应该是下图左边这样的:   很明显建立项目后的架构是上 ...

  2. Java8系列 (六) 新的日期和时间API

    概述 在Java8之前, 我们一般都是使用 SimpleDateFormat 来解析和格式化日期时间, 但它是线程不安全的. @Test public void test() { SimpleDate ...

  3. Java8 新的日期和时间API(笔记)

    LocalDate LocalTime Instant duration以及Period 使用LocalDate和LocalTime //2017-03-20 LocalDate date = Loc ...

  4. Java中常见的包

    目录 JDK自带的包 第三方包 JDK自带的包 JAVA提供了强大的应用程序接口,既JAVA类库.他包含大量已经设计好的工具类,帮助程序员进行字符串处理.绘图.数学计算和网络应用等方面的工作.下面简单 ...

  5. Redis总结(五)缓存雪崩和缓存穿透等问题 Web API系列(三)统一异常处理 C#总结(一)AutoResetEvent的使用介绍(用AutoResetEvent实现同步) C#总结(二)事件Event 介绍总结 C#总结(三)DataGridView增加全选列 Web API系列(二)接口安全和参数校验 RabbitMQ学习系列(六): RabbitMQ 高可用集群

    Redis总结(五)缓存雪崩和缓存穿透等问题   前面讲过一些redis 缓存的使用和数据持久化.感兴趣的朋友可以看看之前的文章,http://www.cnblogs.com/zhangweizhon ...

  6. QuartZ Cron表达式在java定时框架中的应用

    CronTrigger CronTriggers往往比SimpleTrigger更有用,如果您需要基于日历的概念,而非SimpleTrigger完全指定的时间间隔,复发的发射工作的时间表. CronT ...

  7. Web API系列(三)统一异常处理

    前面讲了webapi的安全验证和参数安全,不清楚的朋友,可以看看前面的文章,<Web API系列(二)接口安全和参数校验>,本文主要介绍Web API异常结果的处理.作为内部或者是对外提供 ...

  8. Java 之 I/O 系列 02 ——序列化(二)

    Java 之 I/O 系列 目录 Java 之 I/O 系列 01 ——基础 Java 之 I/O 系列 02 ——序列化(一) Java 之 I/O 系列 02 ——序列化(二) 继续上篇的第二个问 ...

  9. 3.Java 加解密技术系列之 SHA

    Java 加解密技术系列之 SHA 序 背景 正文 SHA-1 与 MD5 的比较 代码实现 结束语 序 上一篇文章中介绍了基本的单向加密算法 — — MD5,也大致的说了说它实现的原理.这篇文章继续 ...

  10. quartz.net 时间表达式----- Cron表达式详解

    序言 Cron表达式:就是用简单的xxoo符号按照一定的规则,就能把各种时间维度表达的淋漓尽致,无所不在其中,然后在用来做任务调度(定时服务)的quart.net中所认知执行,可想而知这是多么的天衣无 ...

随机推荐

  1. exceptionx:灵活便捷的Python异常处理库,让异常处理更高效!

    exceptionx English | 中文 exceptionx 是一个灵活且便捷的Python异常处理库,允许你动态创建异常类,并提供多种异常处理机制. exceptionx 的前身是 gqyl ...

  2. 如何使用工具下载B站非会员视频(下载B站免费web视频)

    最近准备从B站上下载几个web页面上的视频,但是B站的视频又没有提供相关的下载工具,于是找到了一款下载B站视频的工具( you-get ), 该工具不能下载会员版的视频,不能下载收费的视频,不过对于免 ...

  3. 使用Redis时的vm.overcommit_memory内存分配控制

    最近在使用Redis的时候遇到了linux系统中的vm.overcommit_memory参数设置,对此不是很了解,于是研究了一下,有了本文. ============================ ...

  4. 朋友吐槽我为什么这么傻不在源生成器中用string.GetHashCode, 而要用一个不够优化的hash方法

    明明有更好的hash方法 有位朋友对我吐槽前几天我列举的在源生成器的生成db映射实体的优化点 提前生成部分 hashcode 进行比较 所示代码 public static void Generate ...

  5. LuCI Themes

    Bootstrap Bootstrap Light Bootstrap Light 就是 Bootstrap Bootstrap Dark Material OpenWrt OpenWrt 2020

  6. Linux 网络设备命名规则

    在 Linux 系统中,网络接口的命名规则已经经历了几次重要变化,特别是从传统的以 eth 和 wlan 开头的名称,转变到更现代.更具描述性的命名方式.以下是这些变化的概述: 1. 传统命名约定 在 ...

  7. 【GitHub】上传代码通用操作等(附下载单个文件夹或文件)

    一.创建GitHub账号以及配置 参考我的另一篇文章:<[Mac系统 + Git]之上传项目代码到github上以及删除某个文件夹> 二.创建新的个人知识库 前面配置完之后,下面讲的再新建 ...

  8. Kubernetes-1:初识k8s 什么是kubernetes

    Kubernetes简介 为什么要用k8s? 容器间(Docker)在夸主机通信时,只能通过在主机做端口映射(DNAT)来实现,这种方式对于很多集群应用来说及其不方便.会影响整体处理速度,所以引入k8 ...

  9. win2003 密钥 vl_cd1_X13-46432.iso

    1.使用的 ISO为:cn_win_srv_2003_r2_enterprise_with_sp2_vl_cd1_X13-46432.iso 2.迅雷链接 ed2k://|file|cn_win_sr ...

  10. maven 网关应用:[NACOS ConnectException httpPost] currentServerAddr: http://localhost:8848,

    网关应用运行忽然报错:[NACOS ConnectException httpPost] currentServerAddr: http://localhost:8848, 虽然调整了代码逻辑,但是n ...