简介
文档版本:xxl-job2.1.2
官网地址:https://www.*xu**xueli.com/xxl-job/
gitee地址:https://gi*tee.c*o*m/xuxueli0323/xxl-job
名称说明:大众点评许雪里名字的缩写
是一个轻量级分布式任务调度平台
现已开放源代码并接入多家公司线上产品线:大众点评,京东,优信二手车,北京尚德,360金融 (360),联想集团 (联想),易信 (网易)等等
系统组成
调度模块(调度中心):
负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器故障转移。
执行模块(执行器):
负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
接收“调度中心”的执行请求、终止请求和日志请求等。
项目目录文件说明
xxl-job-admin调度中心项目
xxl-job-executor-samples 各种版本的执行器
各组件说明
xxl-job-admin
调度中心项目
作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
xxl-job-core
公共依赖
xxl-job-executor-samples
执行器负责接收“调度中心”的调度并执行;
各种版本的执行器,选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器
包括
xxl-job-executor-sample-frameless
无框架版本;
xxl-job-executor-sample-jboot
jboot版本,通过jboot管理执行器;
xxl-job-executor-sample-jfinal
JFinal版本,通过JFinal管理执行器;
xxl-job-executor-sample-nutz
Nutz版本,通过Nutz管理执行器;
xxl-job-executor-sample-spring
Spring版本,通过Spring容器管理执行器,比较通用;
xxl-job-executor-sample-springboot
Springboot版本,通过Springboot管理执行器,推荐这种方式;
xxl-job运行
初始化"调度数据库”
执行数据库脚本
注意:调度中心支持集群部署,集群情况下各节点务必连接同一个mysql实例;
如果mysql做主从,调度中心集群节点务必强制走主库
SQL脚本位置:/xxl-job/doc/db/tables_xxl_job.sql
表说明
xxl_job_group 执行器信息表
xxl_job_info 任务信息表
xxl_job_lock 用户调度中心高可用悲观锁
xxl_job_log 调度日志表
xxl_job_log_report 调度日志统计表
xxl_job_logglue 任务GLUE日志:用于保存GLUE更新历史,用于支持GLUE的版本回溯功能;
xxl_job_registry 执行器机器注册信息表
xxl_job_user 系统用户信息表
部署调度中心
步骤一:调度中心配置
调度中心配置文件地址:/xxl-job/xxl-job-admin/src/main/resources/xxl-job-admin.properties
## 调度中心JDBC链接 spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT spring.datasource.username=root spring.datasource.password=root_pwd spring.datasource.driver-class-name=com.mysql.jdbc.Driver ### 报警邮箱 spring.mail.host=smtp.qq.com spring.mail.port=25 [email protected] spring.mail.password=xxx spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true ### 登录账号 xxl.job.login.username=admin xxl.job.login.password=123456 ### 调度中心通讯TOKEN,用于调度中心和执行器之间的通讯进行数据加密,非空时启用 xxl.job.accessToken= ### 调度中心国际化设置,默认为中文版本,值设置为“en”时切换为英文版本 xxl.job.i18n=
步骤二:部署项目
如果已经正确进行上述配置,可将项目编译打包部署。
该工程是一个springboot项目,我们只需要在IDEA中执行 XxlJobAdminApplication 类即可运行该工程:
调度中心访问地址:http://*loc*alhos*t:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址),登录后运行界面如下图所示:
默认的登录用户名/密码:admin/123456
可以在/xxl-job/xxl-job-admin/src/main/resources/xxl-job-admin.properties中配置
xxl.job.login.username=admin
xxl.job.login.password=123456
至此“调度中心”项目已经部署成功。
步骤三:调度中心集群可选
调度中心支持集群部署,提升调度系统容灾和可用性。
调度中心集群部署时,几点要求和建议:
DB配置保持一致;
登陆账号配置保持一致;
集群机器时钟保持一致(单机集群忽视);
建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。
部署执行器项目
在源码中作者已经给出了多种执行器项目示例,可根据你的喜好直接将其部署作为你自己的执行器,当然你也可以将执行器集成到现有业务项目中去
将执行器项目编译打部署,源码中执行器示例项目各自的部署方式如下:
xxl-job-executor-sample-springboot:项目编译打包成springboot类型的可执行JAR包,命令启动即可,ide中执行com.xxl.job.executor.XxlJobExecutorApplication
xxl-job-executor-sample-spring:项目编译打包成WAR包,并部署到tomcat中
xxl-job-executor-sample-jfinal:同上
xxl-job-executor-sample-nutz:同上
执行器项目集群(可选)
执行器支持集群部署,提升调度系统可用性,同时提升任务处理能力。
执行器集群部署时,要求和建议:
执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作
同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表
现有项目集成执行器
spring-boot项目
步骤一:在你的项目里引入xxl-job-core的依赖
<dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>2.0.1</version> </dependency>
步骤二:执行器配置
将源码中:
/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties下的配置信息copy一份,添加到你的项目的application.properties文件中去,注意修改自己的配置信息
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; xxl.job.admin.addresses=http://127.**0.*0.1:8080/xxl-job-admin ### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 xxl.job.executor.appname=xxl-job-executor-xgss ### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; xxl.job.executor.ip= ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; xxl.job.executor.port=9999 ### 执行器通讯TOKEN [选填]:非空时启用; xxl.job.accessToken= ### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler ### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能; xxl.job.executor.logretentiondays=-1
步骤三:执行器组件配置
执行器组件配置信息位置:
/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java
找到这个XxlJobConfig.java复制一份到你的项目里即可
配置内容说明:
@Bean public XxlJobSpringExecutor xxlJobExecutor() { logger.info(">>>>>>>>>>> xxl-job config init."); XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppName(appName); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); return xxlJobSpringExecutor; }
spring项目
步骤一:在你的项目里引入xxl-job-core的依赖
<dependency> <groupId>com.xuxueli</groupId> <artifactId>xxl-job-core</artifactId> <version>2.2.0-SNAPSHOT</version> </dependency>
步骤二:
properties配置文件中加入配置
### xxl-job admin address list, such as "http://ad*d*re*ss" or "http://ad*d*re*ss01,http://ad*d*re*ss02" xxl.job.admin.addresses=http://127.**0.*0.1:8080/xxl-job-admin ### xxl-job executor address xxl.job.executor.appname=xxl-job-executor-sample-spring xxl.job.executor.ip= xxl.job.executor.port=9998 ### xxl-job, access token xxl.job.accessToken= ### xxl-job log path xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler ### xxl-job log retention days xxl.job.executor.logretentiondays=30
步骤三:
加入applicationcontext-xxl-job.xml(文件名可以自定义,位置自定义)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.*springframew*o*rk.org/schema/beans" xmlns:xsi="http://www.***w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframe*wor**k.org/schema/context" xsi:schemaLocation="http://www.*springframew*o*rk.org/schema/beans http://www.*springframew*o*rk.org/schema/beans/spring-beans.xsd http://www.springframe*wor**k.org/schema/context http://www.springframe*wor**k.org/schema/context/spring-context.xsd"> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="fileEncoding" value="utf-8" /> <property name="locations"> <list> <value>classpath*:xxl-job-executor.properties</value> </list> </property> </bean> <!-- ********************************* 基础配置 ********************************* --> <!-- 配置01、JobHandler 扫描路径 --> <context:component-scan base-package="com.xxl.job.executor.service.jobhandler" /> <!-- 配置02、执行器 --> <bean id="xxlJobSpringExecutor" class="com.xxl.job.core.executor.impl.XxlJobSpringExecutor" > <!-- 执行器注册中心地址[选填],为空则关闭自动注册 --> <property name="adminAddresses" value="${xxl.job.admin.addresses}" /> <!-- 执行器AppName[选填],为空则关闭自动注册 --> <property name="appName" value="${xxl.job.executor.appname}" /> <!-- 执行器IP[选填],为空则自动获取 --> <property name="ip" value="${xxl.job.executor.ip}" /> <!-- 执行器端口号[选填],小于等于0则自动获取 --> <property name="port" value="${xxl.job.executor.port}" /> <!-- 访问令牌[选填],非空则进行匹配校验 --> <property name="accessToken" value="${xxl.job.accessToken}" /> <!-- 执行器日志路径[选填],为空则使用默认路径 --> <property name="logPath" value="${xxl.job.executor.logpath}" /> <!-- 日志保存天数[选填],值大于3时生效 --> <property name="logRetentionDays" value="${xxl.job.executor.logretentiondays}" /> </bean> </beans>
该文件由spring的applicationContext.xml引入加载或者用web.xml进行加载都可以
步骤四:
编写任务,要编写在配置文件扫描中的包中
package com.xxl.job.executor.service.jobhandler; import com.xxl.job.core.biz.model.ReturnT; import com.xxl.job.core.handler.IJobHandler; import com.xxl.job.core.handler.annotation.XxlJob; import com.xxl.job.core.log.XxlJobLogger; import com.xxl.job.core.util.ShardingUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.TimeUnit; /** * XxlJob开发示例(Bean模式) * * 开发步骤: * 1、在Spring Bean实例中,开发Job方法,方式格式要求为 "public ReturnT<String> execute(String param)" * 2、为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。 * 3、执行日志:需要通过 "XxlJobLogger.log" 打印执行日志; * * @author xuxueli 2019-12-11 21:52:51 */ @Component public class SampleXxlJob { private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class); /** * 1、简单任务示例(Bean模式) */ @XxlJob("demoJobHandler") public ReturnT<String> demoJobHandler(String param) throws Exception { XxlJobLogger.log("XXL-JOB, Hello World."); for (int i = 0; i < 5; i++) { XxlJobLogger.log("beat at:" + i); TimeUnit.SECONDS.sleep(2); } return ReturnT.SUCCESS; } /** * 2、分片广播任务 */ @XxlJob("shardingJobHandler") public ReturnT<String> shardingJobHandler(String param) throws Exception { // 分片参数 ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo(); XxlJobLogger.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVO.getTotal()); // 业务逻辑 for (int i = 0; i < shardingVO.getTotal(); i++) { if (i == shardingVO.getIndex()) { XxlJobLogger.log("第 {} 片, 命中分片开始处理", i); } else { XxlJobLogger.log("第 {} 片, 忽略", i); } } return ReturnT.SUCCESS; } /** * 3、命令行任务 */ @XxlJob("commandJobHandler") public ReturnT<String> commandJobHandler(String param) throws Exception { String command = param; int exitValue = -1; BufferedReader bufferedReader = null; try { // command process Process process = Runtime.getRuntime().exec(command); BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream()); bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream)); // command log String line; while ((line = bufferedReader.readLine()) != null) { XxlJobLogger.log(line); } // command exit process.waitFor(); exitValue = process.exitValue(); } catch (Exception e) { XxlJobLogger.log(e); } finally { if (bufferedReader != null) { bufferedReader.close(); } } if (exitValue == 0) { return IJobHandler.SUCCESS; } else { return new ReturnT<String>(IJobHandler.FAIL.getCode(), "command exit value("+exitValue+") is failed"); } } /** * 4、跨平台Http任务 */ @XxlJob("httpJobHandler") public ReturnT<String> httpJobHandler(String param) throws Exception { // request HttpURLConnection connection = null; BufferedReader bufferedReader = null; try { // connection URL realUrl = new URL(param); connection = (HttpURLConnection) realUrl.openConnection(); // connection setting connection.setRequestMethod("GET"); connection.setDoOutput(true); connection.setDoInput(true); connection.setUseCaches(false); connection.setReadTimeout(5 * 1000); connection.setConnectTimeout(3 * 1000); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); // do connection connection.connect(); //Map<String, List<String>> map = connection.getHeaderFields(); // valid StatusCode int statusCode = connection.getResponseCode(); if (statusCode != 200) { throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid."); } // result bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); StringBuilder result = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { result.append(line); } String responseMsg = result.toString(); XxlJobLogger.log(responseMsg); return ReturnT.SUCCESS; } catch (Exception e) { XxlJobLogger.log(e); return ReturnT.FAIL; } finally { try { if (bufferedReader != null) { bufferedReader.close(); } if (connection != null) { connection.disconnect(); } } catch (Exception e2) { XxlJobLogger.log(e2); } } } /** * 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑; */ @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy") public ReturnT<String> demoJobHandler2(String param) throws Exception { XxlJobLogger.log("XXL-JOB, Hello World."); return ReturnT.SUCCESS; } public void init(){ logger.info("init"); } public void destroy(){ logger.info("destory"); } }
开发自己的任务
在执行器项目中新建自己的任务,代码示例:
package com.tp.athena.jobhandler; import com.xxl.job.core.biz.model.ReturnT; import com.xxl.job.core.handler.IJobHandler; import com.xxl.job.core.handler.annotation.JobHandler; import com.xxl.job.core.log.XxlJobLogger; import org.springframework.stereotype.Component; import javax.sound.midi.Soundbank; import java.util.concurrent.TimeUnit; /** * 任务Handler示例(Bean模式) * <p> * 开发步骤: * 1、继承"IJobHandler":“com.xxl.job.core.handler.IJobHandler”; * 2、注册到Spring容器:添加“@Component”注解,被Spring容器扫描为Bean实例; * 3、注册到执行器工厂:添加“@JobHandler(value="自定义jobhandler名称")”注解,注解value值对应的是调度中心新建任务的JobHandler属性的值。 * 4、执行日志:需要通过 "XxlJobLogger.log" 打印执行日志; * */ @JobHandler(value = "demoJobHandler") @Component public class DemoJobHandler extends IJobHandler { @Override public ReturnT<String> execute(String param) throws Exception { System.out.println("XXL-JOB Hello World"); return SUCCESS; } }
在调度中心新建任务
登录调度中心,点击下图所示“新建任务”按钮,新建示例任务。然后,参考下面截图中任务的参数配置,点击保存。
配置说明
调度中心配置说明
配置文件地址:
/xxl-job/xxl-job-admin/src/main/resources/application.properties
配置参数说明参考部署调度中心
报警邮箱,因为该工程任务失败后有失败告警功能,可以通过邮件来提醒,如果我们需要此功能
执行器项目配置说明
/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.**0.*0.1:8080/xxl-job-admin
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-athena
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.accessToken配置说明:
为提升系统安全性,调度中心和执行器进行安全性校验,双方AccessToken匹配才允许通讯;
调度中心和执行器,可通过配置项 "xxl.job.accessToken" 进行AccessToken的设置。
调度中心和执行器,如果需要正常通讯,只有两种设置;
设置一:调度中心和执行器,均不设置AccessToken;关闭安全性校验;
设置二:调度中心和执行器,设置了相同的AccessToken;
功能使用
配置执行器
点击 执行器管理----》新增执行器---》,如下如下界面,然后填充此表格,点击保存即可。
配置说明
下面是对这几个参数的介绍:
AppName:是每个执行器集群的唯一标示AppName, 执行器会周期性以AppName为对象进行自动注册。可通过该配置自动发现注册成功的执行器, 供任务调度时使用;
当执行器项目启动,调度中心就能够发现执行器,并进行注册
在执行器所在的项目的配置文件中,有对appname的定义
名称:执行器的名称, 因为AppName限制字母数字等组成,可读性不强, 名称为了提高执行器的可读性;
排序: 执行器的排序, 系统中需要执行器的地方,如任务新增, 将会按照该排序读取可用的执行器列表;
注册方式:调度中心获取执行器地址的方式,
自动注册:执行器自动进行执行器注册,调度中心通过底层注册表可以动态发现执行器机器地址;
手动录入:人工手动录入执行器的地址信息,多地址逗号分隔,供调度中心使用;
机器地址:"注册方式"为"手动录入"时有效,支持人工维护执行器的地址信息;
创建任务
点击 任务管理---》新增任务---》
配置说明
执行器:
配置这个任务将要通过那个执行器执行
任务描述:任务的描述信息,便于任务管理;
路由策略:当执行器集群部署时,提供丰富的路由策略,包括;
FIRST(第一个):固定选择第一个机器;
LAST(最后一个):固定选择最后一个机器;
ROUND(轮询):;
RANDOM(随机):随机选择在线的机器;
CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
LEAST_RECENTLY_USED(最近最久未使用):最久为使用的机器优先被选举;
FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
Cron:触发任务执行的Cron表达式;
运行模式:
BEAN模式:任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;Bean模式任务,支持基于类的开发方式,每个任务对应一个Java类。
- 优点:不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持
GLUE模式(Java):任务以源码方式维护在调度中心;该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
GLUE模式(Shell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "shell" 脚本;
GLUE模式(Python):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "python" 脚本;
GLUE模式(PHP):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "php" 脚本;
GLUE模式(NodeJS):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "nodejs" 脚本;
GLUE模式(PowerShell):任务以源码方式维护在调度中心;该模式的任务实际上是一段 "PowerShell" 脚本;
JobHandler:
运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@JobHandler”注解自定义的value值;
阻塞处理策略:调度过于密集执行器来不及处理时的处理策略;
单机串行(默认):调度请求进入单机执行器后,调度请求进入FIFO队列并以串行方式运行;
丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败;
覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务;
子任务ID:每个任务都拥有一个唯一的任务ID(任务ID可以从任务列表获取),当本任务执行结束并且执行成功时,将会触发子任务ID所对应的任务的一次主动调度。
任务超时时间:支持自定义任务超时时间,任务运行超时将会主动中断任务;
失败重试次数;支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;
报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔;
负责人:任务的负责人;
执行参数:任务执行所需的参数
启动任务
列表操作说明
配置完执行器以及任务,我们只需要启动该任务,便可以运行了。
可以手动执行一次,也可以进行启动,进行按照规则自动执行
查询日志会跳转到调度日志查看这个任务的日志信息
注册节点可以查看该任务的执行器注册信息
下次执行时间,可以看到如果任务启动状态将会在什么时间执行
注意:
此处的启动/停止仅针对任务的后续调度触发行为,不会影响到已经触发的调度任务,如需终止已经触发的调度任务
调度日志
启动之后,我们查看日志:
注意:在项目中,只有通过 XxlJobLogger.log() 代码才能将日志打印到上面。
在任务日志界面,点击任务的“执行备注”的“查看”按钮,可以看到匹配子任务以及触发子任务执行的日志信息,如无信息则表示未触发子任务执行,可参考下图。
列表操作说明
调度任务列表说明:
调度时间:"调度中心"触发本次调度并向"执行器"发送任务执行信号的时间;
调度结果:"调度中心"触发本次调度的结果,200表示成功,500或其他表示失败;
调度备注:"调度中心"触发本次调度的日志信息;
执行器地址:本次任务执行的机器地址
运行模式:触发调度时任务的运行模式,运行模式可参考章节 "三、任务详解";
任务参数:本地任务执行的入参
执行时间:"执行器"中本次任务执行结束后回调的时间;
执行结果:"执行器"中本次任务执行的结果,200表示成功,500或其他表示失败;
执行备注:"执行器"中本次任务执行的日志信息;
操作:
"执行日志"按钮:点击可查看本地任务执行的详细日志信息;
"终止任务"按钮:点击可终止本地调度对应执行器上本任务的执行线程,包括未执行的阻塞任务一并被终止;
仅针对执行中的任务。
在任务日志界面,点击右侧的“终止任务”按钮,将会向本次任务对应的执行器发送任务终止请求,将会终止掉本次任务,同时会清空掉整个任务执行队列。
任务终止时通过 “interrupt” 执行线程的方式实现, 将会触发 “InterruptedException” 异常。因此如果JobHandler内部catch到了该异常并消化掉的话, 任务终止功能将不可用。
因此, 如果遇到上述任务终止不可用的情况, 需要在JobHandler中应该针对 “InterruptedException” 异常进行特殊处理 (向上抛出) , 正确逻辑如下:
try{ // do something } catch (Exception e) { if (e instanceof InterruptedException) { throw e; } logger.warn("{}", e); }
而且,在JobHandler中开启子线程时,子线程也不可catch处理”InterruptedException”,应该主动向上抛出。
任务终止时会执行对应JobHandler的”destroy()”方法,可以借助该方法处理一些资源回收的逻辑。
查看任务信息:
点击任务id可以查看任务信息
日志清理:
调度日志列表页面有清理按钮点击
前两项是根据你的筛选条件写上去的,第三项可以进行选择,清理指定的执行日志
用户管理
这里可以看到admin的账户信息
可以新增账户
普通用户可以进行设置执行器的管理权限
管理员不需要设置权限
普通的用户没有用户管理和执行器管理的的权限
任务管理可以对配置的执行器的权限管理任务
运行报表
任务数量:指配置的任务的总数量,无论有没有启动都算
调度次数:跟调度日志的条数一致
执行器数量:在线的执行器数量
集群使用
调度中心集群
集群情况下各节点务必连接同一个mysql实例;
如果mysql做主从,调度中心集群节点务必强制走主库;
集群机器时钟保持一致(单机集群忽视);
调度中心在集群部署时会自动进行任务平均分配,触发组件每次获取与线程池数量(调度中心支持自定义调度线程池大小)相关数量的任务,避免大量任务集中在单个调度中心集群节点;
建议:推荐通过nginx为调度中心集群做负载均衡,分配域名。调度中心访问、执行器回调配置、调用API服务等操作均通过该域名进行。
执行器集群
可以在调度中心任务管理对任务选择路由策略决定服务器的选择方式
执行器回调地址(xxl.job.admin.addresses)需要保持一致;执行器根据该配置进行执行器自动注册等操作。
同一个执行器集群内AppName(xxl.job.executor.appname)需要保持一致;调度中心根据该配置动态发现不同集群的在线执行器列表。
功能扩展
告警扩展
2.2.0的是:
修改邮件模板
EmailJobAlarm.java中可以对邮件模板进行修改
其他告警扩展:
项目里提供了Email告警方式
我们需要 编写JobAlarm的实现类,实现doAlarm来完成其他方式的告警方式
com.xxl.job.admin.core.alarm.JobAlarmer中
会获取到所有的警告类型的对象
所有警告类型的警告会被触发
在com.xxl.job.admin.core.thread.JobFailMonitorHelper类中进行调用告警
工作原理
架构图
执行流程
任务执行器根据配置的调度中心的地址,自动注册到调度中心
达到任务触发条件,调度中心下发任务
执行器基于线程池执行任务,并把执行结果放入内存队列中、把执行日志写入日志文件中
执行器的回调线程消费内存队列中的执行结果,主动上报给调度中心
当用户在调度中心查看任务日志,调度中心请求任务执行器,任务执行器读取任务日志文件并返回日志详情
调度模块
XXL-JOB最终选择自研调度组件(早期调度组件基于Quartz);一方面是为了精简系统降低冗余依赖,另一方面是为了提供系统的可控度与稳定性;
调度采用线程池方式实现,避免单线程因阻塞而引起任务调度延迟。
XXL-JOB的每个调度任务虽然在调度模块是并行调度执行的,但是任务调度传递到任务模块的“执行器”确实串行执行的,同时支持任务终止。
过期处理策略
任务调度错过触发时间时的处理策略:
可能原因:服务重启;调度线程被阻塞,线程被耗尽;上次调度持续阻塞,下次调度被错过;
处理策略:
过期超5s:本次忽略,当前时间开始计算下次触发时间
过期5s内:立即触发一次,当前时间开始计算下次触发时间
日志回调服务:
调度模块的“调度中心”作为Web服务部署时,一方面承担调度中心功能,另一方面也为执行器提供API服务。
调度中心提供的”日志回调服务API服务”代码位置如下:
xxl-job-admin#com.xxl.job.admin.controller.JobApiController.callback
“执行器”在接收到任务执行请求后,执行任务,在执行结束之后会将执行结果回调通知“调度中心”
调度中心高可用
调度中心支持多节点部署,基于数据库行锁保证同时只有一个调度中心节点触发任务调度
参考com.xxl.job.admin.core.thread.JobScheduleHelper#start
Connection conn = XxlJobAdminConfig.getAdminConfig().getDataSource().getConnection(); connAutoCommit = conn.getAutoCommit(); conn.setAutoCommit(false); preparedStatement = conn.prepareStatement( "select * from xxl_job_lock where lock_name = 'schedule_lock' for update" ); preparedStatement.execute(); # 触发任务调度 # 事务提交 conn.commit();
当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放
注册机器地址原理
执行器端会每30秒去尝试调用一次调用中心接口进行尝试注册
采用自研RPC:xxl-rpc调用来实现执行器的注册和任务的调度
执行器注册摘除:执行器销毁时,将会主动上报调度中心并摘除对应的执行器机器信息,提高心跳注册的实时性;
全异步化 & 轻量级
全异步化设计:
XXL-JOB系统中业务逻辑在远程执行器执行,触发流程全异步化设计。相比直接在调度中心内部执行业务逻辑,极大的降低了调度线程占用时间;
异步调度:
调度中心每次任务触发时仅发送一次调度请求,该调度请求首先推送“异步调度队列”,然后异步推送给远程执行器
异步执行:
执行器会将请求存入“异步执行队列”并且立即响应调度中心,异步运行。
轻量级设计:
XXL-JOB调度中心中每个JOB逻辑非常 “轻”,在全异步化的基础上,单个JOB一次运行平均耗时基本在 "10ms" 之内(基本为一次请求的网络开销);因此,可以保证使用有限的线程支撑大量的JOB并发运行;
得益于上述两点优化,理论上默认配置下的调度中心,单机能够支撑 5000 任务并发运行稳定运行;
如若需要支撑更多的任务量,可以通过 “调大调度线程数” 、”降低调度中心与执行器ping延迟” 和 “提升机器配置” 几种方式优化。
运行模式的原理
GLUE模式(Java):
原理:每个 “GLUE模式(Java)” 任务的代码,实际上是“一个继承自“IJobHandler”的实现类的类代码”,“执行器”接收到“调度中心”的调度请求时,会通过Groovy类加载器加载此代码,实例化成Java对象,同时注入此代码中声明的Spring服务(请确保Glue代码中的服务和类引用在“执行器”项目中存在),然后调用该对象的execute方法,执行任务逻辑。
GLUE模式(Shell) + GLUE模式(Python) + GLUE模式(NodeJS)
原理:脚本任务的源码托管在调度中心,脚本逻辑在执行器运行。当触发脚本任务时,执行器会加载脚本源码在执行器机器上生成一份脚本文件,然后通过Java代码调用该脚本;并且实时将脚本输出日志写到任务日志文件中,从而在调度中心可以实时监控脚本运行情况;
通讯数据加密
调度中心向执行器发送的调度请求时使用RequestModel和ResponseModel两个对象封装调度请求参数和响应数据, 在进行通讯之前底层会将上述两个对象对象序列化,并进行数据协议以及时间戳检验,从而达到数据加密的功能;
任务超时控制
支持设置任务超时时间,任务运行超时的情况下,将会主动中断任务;
需要注意的是,任务超时中断时与任务终止机制类似,也是通过 “interrupt” 中断任务,因此业务代码需要将 “InterruptedException” 外抛,否则功能不可用。
其他总结
Cron表达式总结:
cron表达式结构
1.Seconds (秒)
2.Minutes(分)
3.Hours(小时)
4.Day-of-Month (天)
5.Month(月)
6.Day-of-Week (周)
7.Year(年)
各符号的含义:
(1)*:表示匹配域的任意值。
假如在Minutes域中使用*,表示每分钟都会触发。
0 * 9 * * ? 表示每天从9点开始,每分钟触发一次,运行一个小时
(2)?:只能用在DayofMonth和DayofWeek两个域,指没有具体的值。当这两个表达式其中一个被指定后,为了避免冲突,需要将另一个值设为?。
例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ?,其中最后一位只能用“?”,而不能用“*”。
0 0 0 20 * ? 表示每个月20号触发
0 0 0 ? * WED 表示每个星期三触发
0 0 0 ? * 4 表示每个星期三触发
(3),:表示枚举值。
0 0 0 1,20 * ? 表示在每月的1号、20号触发
(4)-:表示指定范围。
0 0 0 1-20 * ? 表示每个月的1号到20号触发
(5)/:被用于指定增量。
0 15/30 0 20 * ? 从每月20号的0点15分运行,每隔30分钟触发一次
(6)L:只能出现在DayofMonth和DayofWeek域。在DayofMonth使用L,表示每月最后一天触发,在DayofWeek使用
0 0 0 L * ? 表示每个月的最后一天触发
0 0 0 ? * L 表示每个星期六触发
0 0 0 ? * 7L 表示每个月的最后一个星期六触发
(7)W:表示有效工作日(周一到周五)。只能出现在DayofMonth,配合指定日期使用。系统将在指定日期的最近的有效工作日触发。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
0 0 0 5W * ? 表示在每月5号最近的一个有效工作日触发
0 0 0 LW * ? 表示在每个月的最后一天最近的有效工作日
(8)#:用于第几个星期几,只能在DayofWeek中使用
0 0 0 ? * 4#2 表示每个月的第二个星期三触发
任务运行模式的总结
BEAN模式:
BEAN模式(类形式)
支持基于类的开发方式,每个任务对应一个Java类。
优点:不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持,可以参考示例项目 “xxl-job-executor-sample-frameless”;
缺点:
每个任务需要占用一个Java类,造成类的浪费;
不支持自动扫描任务并注入到执行器容器,需要手动注入。
步骤:
开发一个继承自"com.xxl.job.core.handler.IJobHandler"的JobHandler类,实现其中任务方法。
手动通过如下方式注入到执行器容器。
XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());
BEAN模式(方法形式)
优点:
每个任务只需要开发一个方法,并添加”@XxlJob”注解即可,更加方便、快速。
支持自动扫描任务并注入到执行器容器。
缺点:要求Spring容器环境;
任务以JobHandler方式维护在执行器端;需要结合 "JobHandler" 属性匹配执行器中任务;
在Spring Bean实例中开发Job方法,每个任务对应一个方法,方式格式要求为 "public ReturnT<String> execute(String param)"
为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
执行器项目中实例代码:
@Component public class SampleXxlJob { private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class); /** * 简单任务示例(Bean模式) */ @XxlJob("demoJobHandler") public ReturnT<String> demoJobHandler(String param) throws Exception { XxlJobLogger.log("XXL-JOB, Hello World."); for (int i = 0; i < 5; i++) { XxlJobLogger.log("beat at:" + i); //TimeUnit.SECONDS.sleep(2);//休息两秒 } return ReturnT.SUCCESS; } /** * 生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑; */ @XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy") public ReturnT<String> demoJobHandler2(String param) throws Exception { XxlJobLogger.log("XXL-JOB, Hello World."); return ReturnT.SUCCESS; } public void init(){ logger.info("init"); } public void destroy(){ logger.info("destory"); } }
调度中心配置
任务管理,这里要配置上执行器要执行的@XxlJob注解的值
GLUE模式(Java):
“GLUE模式(Java)”的执行代码托管到调度中心在线维护,相比“Bean模式任务”需要在执行器项目开发部署上线,更加简便轻量
任务以源码方式维护在调度中心;调度中心执行的任务就是这个java代码
该模式的任务实际上是一段继承自IJobHandler的Java类代码并 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器里中的其他服务;
配置中心配置:
可以先在eclipse中写好,然后粘贴到GLUE IDE的页面输入框内,如:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.xxl.job.core.biz.model.ReturnT; import com.xxl.job.core.handler.IJobHandler; public class DemoGlueJobHandler extends IJobHandler{ private static Logger logger = LoggerFactory.getLogger(DemoGlueJobHandler.class); @Override public ReturnT<String> execute(String param) throws Exception { logger.info("xgssDemoGlueJob,Hello World"); return ReturnT.SUCCESS ; } }
版本回溯功能(支持30个版本的版本回溯):在GLUE任务的Web IDE界面,选择右上角下拉框“版本回溯”,会列出该GLUE的更新历史,选择相应版本即可显示该版本代码,保存后GLUE代码即回退到对应的历史版本;
其他的脚本方式类似,编写对应语言的脚本信息就行
命令行任务
可参考xxl-job-executor-samples中的CommandJobHandler.java
http任务
可参考xxl-job-executor-samples中的HttpJobHandler.java
http任务特点就是可以垮语言跨平台,对方提供http接口即可
日志说明
执行日志:需要通过 "XxlJobLogger.log" 打印执行日志;
如:
XxlJobLogger.log("hello world.");
日志文件存放的位置可在“执行器”配置文件进行自定义,默认目录格式为:/data/applogs/xxl-job/jobhandler/“格式化日期”/“数据库调度日志记录的主键ID.log”。
在JobHandler中开启子线程时,子线程将会将会把日志打印在父线程即JobHandler的执行日志中,方便日志追踪。
日志自动清理
XXL-JOB日志主要包含如下两部分,均支持日志自动清理,说明如下:
调度中心日志表数据:可借助配置项 “xxl.job.logretentiondays” 设置日志表数据保存天数,过期日志自动清理;详情可查看上文配置说明;
执行器日志文件数据:可借助配置项 “xxl.job.executor.logretentiondays” 设置日志文件数据保存天数,过期日志自动清理;详情可查看上文配置说明;
任务执行结果
任务执行结果通过返回值 “ReturnT” 进行判断;
当返回值符合 “ReturnT.code == ReturnT.SUCCESS_CODE” 时表示任务执行成功,否则表示任务执行失败,而且可以通过 “ReturnT.msg” 回调错误信息给调度中心;
分片广播 & 动态分片
执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;
“分片广播” 以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。
“分片广播” 和普通任务开发流程一致,不同之处在于可以可以获取分片参数,获取分片参数进行分片业务处理。
Java语言任务获取分片参数方式:BEAN、GLUE模式(Java)
// 可参考Sample示例执行器中的示例任务"ShardingJobHandler"了解试用
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
脚本语言任务获取分片参数方式:GLUE模式(Shell)、GLUE模式(Python)、GLUE模式(Nodejs)
// 脚本任务入参固定为三个,依次为:任务传参、分片序号、分片总数。以Shell模式任务为例,获取分片参数代码如下
echo "分片序号 index = $2"
echo "分片总数 total = $3"
分片参数属性说明:
index:当前分片序号(从0开始),执行器集群列表中当前执行器的序号;
total:总分片数,执行器集群的总机器数量;
该特性适用场景如:
1、分片任务场景:10个执行器的集群来处理10w条数据,每台机器只需要处理1w条数据,耗时降低10倍;
2、广播任务场景:广播执行器机器运行shell脚本、广播集群节点进行缓存更新等
执行器灰度上线
调度中心与业务解耦,只需部署一次后常年不需要维护。但是,执行器中托管运行着业务作业,作业上线和变更需要重启执行器,尤其是Bean模式任务。
执行器重启可能会中断运行中的任务。但是,XXL-JOB得益于自建执行器与自建注册中心,可以通过灰度上线的方式,避免因重启导致的任务中断的问题。
步骤如下:
1、执行器改为手动注册,下线一半机器列表(A组),线上运行另一半机器列表(B组);
2、等待A组机器任务运行结束并编译上线;执行器注册地址替换为A组;
3、等待B组机器任务运行结束并编译上线;执行器注册地址替换为A组+B组;
操作结束;
任务状态
成功:任务触发成功并执行成功
失败:任务触发失败或任务返回失败状态
进行中:任务触发成功还没有回调执行结果,如果任务在进行中,执行器服务死掉,这个任务的不会再进行触发,一直是进行中状态,也不会触发重试
进行中时,进行终止任务,如果任务线程已经死掉(比如执行器服务一次宕机),也会触发告警,调度失败
子任务说明
主任务配置多个子任务:
如:配置三个子任务:
执行备注中有子任务触发情况说明:并不代表执行情况
主任务执行日志中只有主任务的日志,子任务执行日志有自己的日志打印
主任务下面有多个子任务,子任务的调度失败不影响其他子任务的执行
子任务嵌套
7调8,8调9,,9调10
可以正常调用
如果调用过程中有的调用失败,比如9失败,后续的子任务就不会得到执行
注意事项
1 、时钟同步问题
调度中心和任务执行器需要时间同步,同步时间误差需要在3分钟内,否则抛出异常
参考:
core项目的xxl-rpc-core包中:
com.xxl.rpc.remoting.provider.XxlRpcProviderFactory#invokeService
if (System.currentTimeMillis() - xxlRpcRequest.getCreateMillisTime() > 3*60*1000) { xxlRpcResponse.setErrorMsg("The timestamp difference between admin and executor exceeds the limit."); return xxlRpcResponse; }
2、时区问题
任务由调度中心触发,按照在调度中心设置任务的cron表达式触发时,需要注意部署调度中心的机器所在的时区,按照该时区定制化cron表达式
3、任务执行中服务宕掉问题
调度中心完成任务下发,执行器在执行任务的过程中,如果执行器突然服务宕掉,会导致任务的执行问题在调度中心是执行中,调度中心并不会发起失败重试。即使任务设置了超时时间,执行器宕掉导致导致任务长时间未执行完成,调度中心界面也不会看到任务超时,因为任务超时是由执行器检测的并上报给调度中心的
因此遇到任务长时间未执行完成,可以关注是否发生了执行器突然服务宕掉
4、优雅停机问题
执行器执行任务基于线程池异步执行,当需要重启时需要注意线程池中还有未执行完成任务的问题,需要优雅停机,可以直接基于XxlJobExecutor.destroy()优雅停机,注意该方法在v2.0.2之前的版本存在bug导致无法优雅停机,v2.0.2及之后的版本才修复
(参考:https://git*h*u*b.com/xuxueli/xxl-job/issues/727)
5、失败重试问题
当执行器节点部分服务不可用,例如节点磁盘损坏,但在调度中心仍然处于在线时,调度中心仍可能基于路由策略(包括故障转移策略)路由到该未下线的节点,并不断重试,不断失败,导致重试次数耗尽。所以路由策略尽量不要采用固定化策略,例如固定第一个、固定最后一个
项目运行错误
项目会报Log4jConfigListener这个类找不到,在spring5.0及以上版本已经废弃删除,spring建议用log4j2 来替换这个类
定时任务传参
编辑任务可以填写默认参数,手动执行的时候也可以修改参数
要获取传入的参数可以通过下面的代码获取到:
String param = XxlJobHelper.getJobParam();
动态添加定时任务
在代码中进行操作创建任务、开启任务、删除任务、执行任务等操作,开源代码提供了这些功能的即可,我们只要在业务系统中调用即可。
配置文件中添加xxl-job服务的服务地址和登录账号密码等
xxl-job: loginAddress: http://172.**20*.61.122:30015/xxl-job-admin userName: admin passWord: 123456 excutor: appname: xgss-job
封装xxl-job服务的http调用的工具类,业务代码直接调用该工具类就可以
package com.xgss.xxljob; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpUtil; import cn.hutool.json.JSONUtil; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.List; import java.util.Map; @Component @Slf4j public class XxlJobApiUtils { @Value("${xxl-job.loginAddress}") private String xxlJobLoginAddress; @Value("${xxl-job.excutor.appname}") private String xxlJobAppName; @Value("${xxl-job.userName}") private String xxlJobLoginUserName; @Value("${xxl-job.password}") private String xxlJobLoginPassword; @Test public void test1(){ XxlJobGroup xxlJobGroup =new XxlJobGroup(); xxlJobGroup.setAppname("xgss-job"); List<XxlJobGroup> xxlJobGroups = selectActuator(xxlJobGroup); XxlJobGroup operateGroup=null; for(XxlJobGroup group: xxlJobGroups){ if(xxlJobGroup.getAppname().equals(group.getAppname()) ){ operateGroup=group; break; } } if(operateGroup==null){ throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"执行器不存在:"+xxlJobGroup.getAppname()); } int groupId = operateGroup.getId(); XxlJobInfo xxlJobInfo=new XxlJobInfo(); xxlJobInfo.setJobGroup(groupId); xxlJobInfo.setJobDesc("xgss测试自动创建"); xxlJobInfo.setExecutorRouteStrategy("ROUND");// 执行器路由策略 xxlJobInfo.setExecutorHandler("xgssTask2"); xxlJobInfo.setExecutorBlockStrategy("SERIAL_EXECUTION"); xxlJobInfo.setExecutorTimeout(0); xxlJobInfo.setExecutorFailRetryCount(0); xxlJobInfo.setJobCron("0 0 0 1 1 ? *"); xxlJobInfo.setAuthor("xgss"); xxlJobInfo.setExecutorParam("sssss"); xxlJobInfo.setTriggerStatus(1); XxlJobResponseInfo task = createTask(xxlJobInfo); System.out.println(JSONUtil.toJsonStr(task));//{"msg":"路由策略非法","code":500}{"code":200,"content":"45"} } /** * 创建并启动任务 * @param xxlJobInfo * @return */ public String createOperatePatrolPlanTask(PatrolPlanXxlJobSaveDTO xxlJobInfo){ XxlJobGroup operateGroup=null; String cookie = loginTaskCenter();//获取登录cookie //查询执行器 Map<String, Object> form = new HashMap<>(); form.put("appname", xxlJobAppName); //创建执行器管理器地址 HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobgroup/pageList"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); XxlJobActuatorManagerInfo xxlJobActuatorManagerInfo = JSONUtil.toBean(body, XxlJobActuatorManagerInfo.class); List<XxlJobGroup> xxlJobGroups = xxlJobActuatorManagerInfo.getData(); for(XxlJobGroup group: xxlJobGroups){ if(xxlJobAppName.equals(group.getAppname()) ){ operateGroup=group; break; } } if(operateGroup==null){ throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"执行器不存在:"+xxlJobAppName); } //创建任务 Map<String, Object> createTaskForm = new HashMap<>(); createTaskForm.put("jobGroup", operateGroup.getId() + "");//执行器主键ID createTaskForm.put("jobDesc", xxlJobInfo.getJobDesc()); createTaskForm.put("executorRouteStrategy", "ROUND");//路由策略为:轮询 createTaskForm.put("jobCron", xxlJobInfo.getJobCron()); createTaskForm.put("glueType", "BEAN"); createTaskForm.put("executorHandler", "patrolPlanTask"); createTaskForm.put("executorBlockStrategy", "SERIAL_EXECUTION");//阻塞策略为:单机串行 createTaskForm.put("author", "system"); createTaskForm.put("executorParam",xxlJobInfo.getExecutorParam()); createTaskForm.put("triggerStatus",xxlJobInfo.getTriggerStatus()); postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobinfo/add"); response = postRequest.form(createTaskForm).header("Cookie",cookie).execute(); XxlJobResponseInfo xxlJobResponseInfo = JSONUtil.toBean(response.body(), XxlJobResponseInfo.class); if(xxlJobResponseInfo.getCode()!=HttpStatus.OK.value()){ log.error("xxl-job任务创建失败"); throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"任务创建失败,请重新尝试"); } return xxlJobResponseInfo.getContent(); } /** * 登录任务调度平台 */ public String loginTaskCenter() { Map<String, Object> loginForm = new HashMap<>(); loginForm.put("userName", xxlJobLoginUserName); loginForm.put("password", xxlJobLoginPassword); HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/login"); HttpResponse response = postRequest.form(loginForm).execute(); if(response.isOk() && StrUtil.isNotBlank(response.body()) ){ Map<String, List<String>> headers = response.headers(); if(CollectionUtil.isNotEmpty(headers)){ System.out.println(JSONUtil.toJsonStr(headers)); return headers.get("Set-Cookie").get(0); } } throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"xxl-job登录失败"); } /** * 启动xxl-job任务管理 * @param taskId 任务管理id */ public void startTask(Integer taskId){ //获取登录cookie String cookie = loginTaskCenter(); //创建参数 Map<String, Object> form = new HashMap<>(); form.put("id", "" + taskId); HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobinfo/start"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); XxlJobResponseInfo xxlJobResponseInfo = JSONUtil.toBean(body, XxlJobResponseInfo.class); if(xxlJobResponseInfo ==null || xxlJobResponseInfo.getCode()!=HttpStatus.OK.value()){ log.error("xxl-job任务启动失败,任务ID:"+taskId); throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"xxl-job任务启动失败,任务ID:"+taskId); } } /** * 删除 xxl-job任务管理 */ public void deleteTask(Integer taskId){ //获取登录cookie String cookie = loginTaskCenter(); //创建参数 Map<String, Object> form = new HashMap<>(); form.put("id", "" + taskId); HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobinfo/remove"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); XxlJobResponseInfo xxlJobResponseInfo = JSONUtil.toBean(body, XxlJobResponseInfo.class); if(xxlJobResponseInfo ==null || xxlJobResponseInfo.getCode()!=HttpStatus.OK.value()){ log.error("xxl-job任务删除失败,任务ID:"+taskId); throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"定时任务删除失败"); } } /** * 编辑 xxl-job任务 * @param xxlJobInfo 查询参数 */ public void editTask(XxlJobInfo xxlJobInfo){ //获取登录cookie String cookie = loginTaskCenter(); //创建任务管理参数 Map<String, Object> form = new HashMap<>(); form.put("id",xxlJobInfo.getId() + ""); form.put("jobGroup", xxlJobInfo.getJobGroup() + ""); form.put("jobDesc", xxlJobInfo.getJobDesc()); form.put("executorRouteStrategy", "ROUND"); form.put("jobCron", xxlJobInfo.getJobCron()); form.put("glueType", "BEAN"); form.put("executorHandler", xxlJobInfo.getExecutorHandler()); form.put("executorBlockStrategy", "SERIAL_EXECUTION"); form.put("author", "mye"); HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobinfo/update"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); XxlJobResponseInfo xxlJobResponseInfo = JSONUtil.toBean(body, XxlJobResponseInfo.class); if(xxlJobResponseInfo ==null || xxlJobResponseInfo.getCode()!=HttpStatus.OK.value()){ log.error("xxl-job任务编辑失败,任务ID:"+xxlJobInfo.getId()); throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"xxl-job任务编辑失败,任务ID:"+xxlJobInfo.getId()); } } /** * 查询所有的task * @param xxlJobInfo * @return */ public XxlJobTaskManagerInfo selectAllTask(XxlJobInfo xxlJobInfo) { //获取登录cookie String cookie = loginTaskCenter(); //创建任务管理参数 Map<String, Object> form = new HashMap<>(); form.put("jobGroup", xxlJobInfo.getJobGroup() + ""); form.put("triggerStatus", "-1"); HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobinfo/pageList"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); return JSONUtil.toBean(body, XxlJobTaskManagerInfo.class); } /** * 查询 xxl-job任务 * @param xxlJobInfo 查询参数 */ public XxlJobTaskManagerInfo selectTask(XxlJobInfo xxlJobInfo){ //获取登录cookie String cookie = loginTaskCenter(); //创建任务管理参数 Map<String, Object> form = new HashMap<>(); form.put("jobGroup", xxlJobInfo.getJobGroup() + ""); form.put("jobDesc", xxlJobInfo.getJobDesc()); form.put("executorHandler", xxlJobInfo.getExecutorHandler()); form.put("author", xxlJobInfo.getAuthor()); form.put("triggerStatus", "-1"); HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobinfo/pageList"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); return JSONUtil.toBean(body, XxlJobTaskManagerInfo.class); } /** * 创建任务 * @param xxlJobInfo 创建参数 */ public XxlJobResponseInfo createTask(XxlJobInfo xxlJobInfo){ //获取登录cookie String cookie = loginTaskCenter(); //创建任务管理参数 Map<String, Object> form = new HashMap<>(); form.put("jobGroup", xxlJobInfo.getJobGroup() + "");//执行器主键ID form.put("jobDesc", xxlJobInfo.getJobDesc()); form.put("executorRouteStrategy", xxlJobInfo.getExecutorRouteStrategy()); form.put("jobCron", xxlJobInfo.getJobCron()); form.put("glueType", "BEAN"); form.put("executorHandler", xxlJobInfo.getExecutorHandler()); form.put("executorBlockStrategy", xxlJobInfo.getExecutorBlockStrategy()); form.put("author", xxlJobInfo.getAuthor()); form.put("executorParam",xxlJobInfo.getExecutorParam()); form.put("triggerStatus",xxlJobInfo.getTriggerStatus()); HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobinfo/add"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); return JSONUtil.toBean(body, XxlJobResponseInfo.class); } /** * 删除执行器 */ public void deleteActuator(XxlJobGroup xxlJobGroup) { //获取登录cookie String cookie = loginTaskCenter(); //创建查询执行器管理器参数 Map<String, Object> form = new HashMap<>(); form.put("id", xxlJobGroup.getId() + ""); //创建执行器管理器地址 HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobgroup/remove"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); XxlJobResponseInfo xxlJobResponseInfo = JSONUtil.toBean(body, XxlJobResponseInfo.class); if (xxlJobResponseInfo==null || xxlJobResponseInfo.getCode() != HttpStatus.OK.value()) { throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"xxl-job删除执行器失败,执行器ID:"+xxlJobGroup.getId()); } } /** * 编辑执行器 */ public void editActuator(XxlJobGroup xxlJobGroup){ //获取登录cookie String cookie = loginTaskCenter(); //创建查询执行器管理器参数 Map<String, Object> form = new HashMap<>(); form.put("appname", xxlJobGroup.getAppname()); form.put("title", xxlJobGroup.getTitle()); form.put("addressType", xxlJobGroup.getAddressType() + ""); form.put("id", xxlJobGroup.getId() + ""); //创建执行器管理器地址 HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobgroup/update"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); XxlJobResponseInfo xxlJobResponseInfo = JSONUtil.toBean(body, XxlJobResponseInfo.class); if (xxlJobResponseInfo==null || xxlJobResponseInfo.getCode() != HttpStatus.OK.value()) { throw new GlobalDefaultException(ResponseCode.ERROR.getCode(),"xxl-job删除执行器失败,执行器ID:"+xxlJobGroup.getId()); } } /** * 查询执行器 (appname 和 title 都是模糊查询) * @param xxlJobGroup XxlJobGroup * @return xxlJobGroup 集合 */ public List<XxlJobGroup> selectActuator(XxlJobGroup xxlJobGroup){ //获取登录cookie String cookie = loginTaskCenter(); //创建查询执行器管理器参数 Map<String, Object> form = new HashMap<>(); form.put("appname", xxlJobGroup.getAppname()); form.put("title", xxlJobGroup.getTitle()); //创建执行器管理器地址 HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobgroup/pageList"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); XxlJobActuatorManagerInfo xxlJobActuatorManagerInfo = JSONUtil.toBean(body, XxlJobActuatorManagerInfo.class); return xxlJobActuatorManagerInfo.getData(); } /** * 创建执行器 * * @param xxlJobGroup 创建参数 */ public XxlJobResponseInfo createActuator(XxlJobGroup xxlJobGroup) { //获取登录cookie String cookie = loginTaskCenter(); //创建执行器管理器参数 Map<String, Object> form = new HashMap<>(); form.put("appname", xxlJobGroup.getAppname()); form.put("title", xxlJobGroup.getTitle()); form.put("addressType", xxlJobGroup.getAddressType() + ""); //创建执行器管理器地址 HttpRequest postRequest = HttpUtil.createPost(xxlJobLoginAddress + "/jobgroup/save"); HttpResponse response = postRequest.form(form).header("Cookie",cookie).execute(); String body = response.body(); return JSONUtil.toBean(body, XxlJobResponseInfo.class); } }
其他需要的类
GlobalDefaultException为自定义异常,ResponseCode为自定义异常枚举类型这里不做赘述
package com.xgss.xxljob; import java.io.Serializable; import java.text.ParseException; import java.util.*; public final class CronExpression implements Serializable, Cloneable { private static final long serialVersionUID = 12423409423L; protected static final int SECOND = 0; protected static final int MINUTE = 1; protected static final int HOUR = 2; protected static final int DAY_OF_MONTH = 3; protected static final int MONTH = 4; protected static final int DAY_OF_WEEK = 5; protected static final int YEAR = 6; protected static final int ALL_SPEC_INT = 99; // '*' protected static final int NO_SPEC_INT = 98; // '?' protected static final Integer ALL_SPEC = ALL_SPEC_INT; protected static final Integer NO_SPEC = NO_SPEC_INT; protected static final Map<String, Integer> monthMap = new HashMap<String, Integer>(20); protected static final Map<String, Integer> dayMap = new HashMap<String, Integer>(60); static { monthMap.put("JAN", 0); monthMap.put("FEB", 1); monthMap.put("MAR", 2); monthMap.put("APR", 3); monthMap.put("MAY", 4); monthMap.put("JUN", 5); monthMap.put("JUL", 6); monthMap.put("AUG", 7); monthMap.put("SEP", 8); monthMap.put("OCT", 9); monthMap.put("NOV", 10); monthMap.put("DEC", 11); dayMap.put("SUN", 1); dayMap.put("MON", 2); dayMap.put("TUE", 3); dayMap.put("WED", 4); dayMap.put("THU", 5); dayMap.put("FRI", 6); dayMap.put("SAT", 7); } private final String cronExpression; 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; protected transient boolean lastdayOfWeek = false; protected transient int nthdayOfWeek = 0; protected transient boolean lastdayOfMonth = false; protected transient boolean nearestWeekday = false; protected transient int lastdayOffset = 0; protected transient boolean expressionParsed = false; public static final int MAX_YEAR = Calendar.getInstance().get(Calendar.YEAR) + 100; /** * Constructs a new <CODE>CronExpression</CODE> based on the specified * parameter. * * @param cronExpression String representation of the cron expression the * new object should represent * @throws ParseException * if the string expression cannot be parsed into a valid * <CODE>CronExpression</CODE> */ public CronExpression(String cronExpression) throws ParseException { if (cronExpression == null) { throw new IllegalArgumentException("cronExpression cannot be null"); } this.cronExpression = cronExpression.toUpperCase(Locale.US); buildExpression(this.cronExpression); } /** * Constructs a new {@code CronExpression} as a copy of an existing * instance. * * @param expression * The existing cron expression to be copied */ public CronExpression(CronExpression expression) { /* * We don't call the other constructor here since we need to swallow the * ParseException. We also elide some of the sanity checking as it is * not logically trippable. */ this.cronExpression = expression.getCronExpression(); try { buildExpression(cronExpression); } catch (ParseException ex) { throw new AssertionError(); } if (expression.getTimeZone() != null) { setTimeZone((TimeZone) expression.getTimeZone().clone()); } } /** * Indicates whether the given date satisfies the cron expression. Note that * milliseconds are ignored, so two Dates falling on different milliseconds * of the same second will always have the same result here. * * @param date the date to evaluate * @return a boolean indicating whether the given date satisfies the cron * expression */ public boolean isSatisfiedBy(Date date) { Calendar testDateCal = Calendar.getInstance(getTimeZone()); testDateCal.setTime(date); testDateCal.set(Calendar.MILLISECOND, 0); Date originalDate = testDateCal.getTime(); testDateCal.add(Calendar.SECOND, -1); Date timeAfter = getTimeAfter(testDateCal.getTime()); return ((timeAfter != null) && (timeAfter.equals(originalDate))); } /** * Returns the next date/time <I>after</I> the given date/time which * satisfies the cron expression. * * @param date the date/time at which to begin the search for the next valid * date/time * @return the next valid date/time */ public Date getNextValidTimeAfter(Date date) { return getTimeAfter(date); } /** * Returns the next date/time <I>after</I> the given date/time which does * <I>not</I> satisfy the expression * * @param date the date/time at which to begin the search for the next * invalid date/time * @return the next valid date/time */ public Date getNextInvalidTimeAfter(Date date) { long difference = 1000; //move back to the nearest second so differences will be accurate Calendar adjustCal = Calendar.getInstance(getTimeZone()); adjustCal.setTime(date); adjustCal.set(Calendar.MILLISECOND, 0); Date lastDate = adjustCal.getTime(); Date newDate; //FUTURE_TODO: (QUARTZ-481) IMPROVE THIS! The following is a BAD solution to this problem. Performance will be very bad here, depending on the cron expression. It is, however A solution. //keep getting the next included time until it's farther than one second // apart. At that point, lastDate is the last valid fire time. We return // the second immediately following it. while (difference == 1000) { newDate = getTimeAfter(lastDate); if(newDate == null) break; difference = newDate.getTime() - lastDate.getTime(); if (difference == 1000) { lastDate = newDate; } } return new Date(lastDate.getTime() + 1000); } /** * Returns the time zone for which this <code>CronExpression</code> * will be resolved. */ public TimeZone getTimeZone() { if (timeZone == null) { timeZone = TimeZone.getDefault(); } return timeZone; } /** * Sets the time zone for which this <code>CronExpression</code> * will be resolved. */ public void setTimeZone(TimeZone timeZone) { this.timeZone = timeZone; } /** * Returns the string representation of the <CODE>CronExpression</CODE> * * @return a string representation of the <CODE>CronExpression</CODE> */ @Override public String toString() { return cronExpression; } /** * Indicates whether the specified cron expression can be parsed into a * valid cron expression * * @param cronExpression the expression to evaluate * @return a boolean indicating whether the given expression is a valid cron * expression */ public static boolean isValidExpression(String cronExpression) { try { new CronExpression(cronExpression); } catch (ParseException pe) { return false; } return true; } public static void validateExpression(String cronExpression) throws ParseException { new CronExpression(cronExpression); } //////////////////////////////////////////////////////////////////////////// // // Expression Parsing Functions // //////////////////////////////////////////////////////////////////////////// protected void buildExpression(String expression) throws ParseException { expressionParsed = true; try { if (seconds == null) { seconds = new TreeSet<Integer>(); } if (minutes == null) { minutes = new TreeSet<Integer>(); } if (hours == null) { hours = new TreeSet<Integer>(); } if (daysOfMonth == null) { daysOfMonth = new TreeSet<Integer>(); } if (months == null) { months = new TreeSet<Integer>(); } if (daysOfWeek == null) { daysOfWeek = new TreeSet<Integer>(); } if (years == null) { years = new TreeSet<Integer>(); } int exprOn = SECOND; StringTokenizer exprsTok = new StringTokenizer(expression, " t", false); while (exprsTok.hasMoreTokens() && exprOn <= YEAR) { String expr = exprsTok.nextToken().trim(); // throw an exception if L is used with other days of the month if(exprOn == DAY_OF_MONTH && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { throw new ParseException("Support for specifying 'L' and 'LW' with other days of the month is not implemented", -1); } // throw an exception if L is used with other days of the week if(exprOn == DAY_OF_WEEK && expr.indexOf('L') != -1 && expr.length() > 1 && expr.contains(",")) { throw new ParseException("Support for specifying 'L' with other days of the week is not implemented", -1); } if(exprOn == DAY_OF_WEEK && expr.indexOf('#') != -1 && expr.indexOf('#', expr.indexOf('#') +1) != -1) { throw new ParseException("Support for specifying multiple "nth" days is not implemented.", -1); } StringTokenizer vTok = new StringTokenizer(expr, ","); while (vTok.hasMoreTokens()) { String v = vTok.nextToken(); storeExpressionVals(0, v, exprOn); } exprOn++; } if (exprOn <= DAY_OF_WEEK) { throw new ParseException("Unexpected end of expression.", expression.length()); } if (exprOn <= YEAR) { storeExpressionVals(0, "*", YEAR); } TreeSet<Integer> dow = getSet(DAY_OF_WEEK); TreeSet<Integer> dom = getSet(DAY_OF_MONTH); // Copying the logic from the UnsupportedOperationException below boolean dayOfMSpec = !dom.contains(NO_SPEC); boolean dayOfWSpec = !dow.contains(NO_SPEC); if (!dayOfMSpec || dayOfWSpec) { if (!dayOfWSpec || dayOfMSpec) { throw new ParseException( "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); } } } catch (ParseException pe) { throw pe; } catch (Exception e) { throw new ParseException("Illegal cron expression format (" + e.toString() + ")", 0); } } protected int storeExpressionVals(int pos, String s, int type) throws ParseException { int incr = 0; int i = skipWhiteSpace(pos, s); if (i >= s.length()) { return i; } char c = s.charAt(i); if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW")) && (!s.matches("^L-[0-9]*[W]?"))) { String sub = s.substring(i, i + 3); int sval = -1; int eval = -1; if (type == MONTH) { sval = getMonthNumber(sub) + 1; if (sval <= 0) { throw new ParseException("Invalid Month value: '" + sub + "'", i); } if (s.length() > i + 3) { c = s.charAt(i + 3); if (c == '-') { i += 4; sub = s.substring(i, i + 3); eval = getMonthNumber(sub) + 1; if (eval <= 0) { throw new ParseException("Invalid Month value: '" + sub + "'", i); } } } } else if (type == DAY_OF_WEEK) { sval = getDayOfWeekNumber(sub); if (sval < 0) { throw new ParseException("Invalid Day-of-Week value: '" + sub + "'", i); } if (s.length() > i + 3) { c = s.charAt(i + 3); if (c == '-') { i += 4; sub = s.substring(i, i + 3); eval = getDayOfWeekNumber(sub); if (eval < 0) { throw new ParseException( "Invalid Day-of-Week value: '" + sub + "'", i); } } else if (c == '#') { try { i += 4; nthdayOfWeek = Integer.parseInt(s.substring(i)); if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { throw new Exception(); } } catch (Exception e) { throw new ParseException( "A numeric value between 1 and 5 must follow the '#' option", i); } } else if (c == 'L') { lastdayOfWeek = true; i++; } } } else { throw new ParseException( "Illegal characters for this position: '" + sub + "'", i); } if (eval != -1) { incr = 1; } addToSet(sval, eval, incr, type); return (i + 3); } if (c == '?') { i++; if ((i + 1) < s.length() && (s.charAt(i) != ' ' && s.charAt(i + 1) != 't')) { throw new ParseException("Illegal character after '?': " + s.charAt(i), i); } if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) { throw new ParseException( "'?' can only be specified for Day-of-Month or Day-of-Week.", i); } if (type == DAY_OF_WEEK && !lastdayOfMonth) { int val = daysOfMonth.last(); if (val == NO_SPEC_INT) { throw new ParseException( "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", i); } } addToSet(NO_SPEC_INT, -1, 0, type); return i; } if (c == '*' || c == '/') { if (c == '*' && (i + 1) >= s.length()) { addToSet(ALL_SPEC_INT, -1, incr, type); return i + 1; } else if (c == '/' && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s .charAt(i + 1) == 't')) { throw new ParseException("'/' must be followed by an integer.", i); } else if (c == '*') { i++; } c = s.charAt(i); if (c == '/') { // is an increment specified? i++; if (i >= s.length()) { throw new ParseException("Unexpected end of string.", i); } incr = getNumericValue(s, i); i++; if (incr > 10) { i++; } checkIncrementRange(incr, type, i); } else { incr = 1; } addToSet(ALL_SPEC_INT, -1, incr, type); return i; } else if (c == 'L') { i++; if (type == DAY_OF_MONTH) { lastdayOfMonth = true; } if (type == DAY_OF_WEEK) { addToSet(7, 7, 0, type); } if(type == DAY_OF_MONTH && s.length() > i) { c = s.charAt(i); if(c == '-') { ValueSet vs = getValue(0, s, i+1); lastdayOffset = vs.value; if(lastdayOffset > 30) throw new ParseException("Offset from last day must be <= 30", i+1); i = vs.pos; } if(s.length() > i) { c = s.charAt(i); if(c == 'W') { nearestWeekday = true; i++; } } } return i; } else if (c >= '0' && c <= '9') { int val = Integer.parseInt(String.valueOf(c)); i++; if (i >= s.length()) { addToSet(val, -1, -1, type); } else { c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(val, s, i); val = vs.value; i = vs.pos; } i = checkNext(i, s, val, type); return i; } } else { throw new ParseException("Unexpected character: " + c, i); } return i; } private void checkIncrementRange(int incr, int type, int idxPos) throws ParseException { if (incr > 59 && (type == SECOND || type == MINUTE)) { throw new ParseException("Increment > 60 : " + incr, idxPos); } else if (incr > 23 && (type == HOUR)) { throw new ParseException("Increment > 24 : " + incr, idxPos); } else if (incr > 31 && (type == DAY_OF_MONTH)) { throw new ParseException("Increment > 31 : " + incr, idxPos); } else if (incr > 7 && (type == DAY_OF_WEEK)) { throw new ParseException("Increment > 7 : " + incr, idxPos); } else if (incr > 12 && (type == MONTH)) { throw new ParseException("Increment > 12 : " + incr, idxPos); } } protected int checkNext(int pos, String s, int val, int type) throws ParseException { int end = -1; int i = pos; if (i >= s.length()) { addToSet(val, end, -1, type); return i; } char c = s.charAt(pos); if (c == 'L') { if (type == DAY_OF_WEEK) { if(val < 1 || val > 7) throw new ParseException("Day-of-Week values must be between 1 and 7", -1); lastdayOfWeek = true; } else { throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i); } TreeSet<Integer> set = getSet(type); set.add(val); i++; return i; } if (c == 'W') { if (type == DAY_OF_MONTH) { nearestWeekday = true; } else { throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i); } if(val > 31) throw new ParseException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)", i); TreeSet<Integer> set = getSet(type); set.add(val); i++; return i; } if (c == '#') { if (type != DAY_OF_WEEK) { throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i); } i++; try { nthdayOfWeek = Integer.parseInt(s.substring(i)); if (nthdayOfWeek < 1 || nthdayOfWeek > 5) { throw new Exception(); } } catch (Exception e) { throw new ParseException( "A numeric value between 1 and 5 must follow the '#' option", i); } TreeSet<Integer> set = getSet(type); set.add(val); i++; return i; } if (c == '-') { i++; c = s.charAt(i); int v = Integer.parseInt(String.valueOf(c)); end = v; i++; if (i >= s.length()) { addToSet(val, end, 1, type); return i; } c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(v, s, i); end = vs.value; i = vs.pos; } if (i < s.length() && ((c = s.charAt(i)) == '/')) { i++; c = s.charAt(i); int v2 = Integer.parseInt(String.valueOf(c)); i++; if (i >= s.length()) { addToSet(val, end, v2, type); return i; } c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(v2, s, i); int v3 = vs.value; addToSet(val, end, v3, type); i = vs.pos; return i; } else { addToSet(val, end, v2, type); return i; } } else { addToSet(val, end, 1, type); return i; } } if (c == '/') { if ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == 't') { throw new ParseException("'/' must be followed by an integer.", i); } i++; c = s.charAt(i); int v2 = Integer.parseInt(String.valueOf(c)); i++; if (i >= s.length()) { checkIncrementRange(v2, type, i); addToSet(val, end, v2, type); return i; } c = s.charAt(i); if (c >= '0' && c <= '9') { ValueSet vs = getValue(v2, s, i); int v3 = vs.value; checkIncrementRange(v3, type, i); addToSet(val, end, v3, type); i = vs.pos; return i; } else { throw new ParseException("Unexpected character '" + c + "' after '/'", i); } } addToSet(val, end, 0, type); i++; return i; } public String getCronExpression() { return cronExpression; } public String getExpressionSummary() { StringBuilder buf = new StringBuilder(); buf.append("seconds: "); buf.append(getExpressionSetSummary(seconds)); buf.append("n"); buf.append("minutes: "); buf.append(getExpressionSetSummary(minutes)); buf.append("n"); buf.append("hours: "); buf.append(getExpressionSetSummary(hours)); buf.append("n"); buf.append("daysOfMonth: "); buf.append(getExpressionSetSummary(daysOfMonth)); buf.append("n"); buf.append("months: "); buf.append(getExpressionSetSummary(months)); buf.append("n"); buf.append("daysOfWeek: "); buf.append(getExpressionSetSummary(daysOfWeek)); buf.append("n"); buf.append("lastdayOfWeek: "); buf.append(lastdayOfWeek); buf.append("n"); buf.append("nearestWeekday: "); buf.append(nearestWeekday); buf.append("n"); buf.append("NthDayOfWeek: "); buf.append(nthdayOfWeek); buf.append("n"); buf.append("lastdayOfMonth: "); buf.append(lastdayOfMonth); buf.append("n"); buf.append("years: "); buf.append(getExpressionSetSummary(years)); buf.append("n"); return buf.toString(); } protected String getExpressionSetSummary(java.util.Set<Integer> set) { if (set.contains(NO_SPEC)) { return "?"; } if (set.contains(ALL_SPEC)) { return "*"; } StringBuilder buf = new StringBuilder(); Iterator<Integer> itr = set.iterator(); boolean first = true; while (itr.hasNext()) { Integer iVal = itr.next(); String val = iVal.toString(); if (!first) { buf.append(","); } buf.append(val); first = false; } return buf.toString(); } protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) { if (list.contains(NO_SPEC)) { return "?"; } if (list.contains(ALL_SPEC)) { return "*"; } StringBuilder buf = new StringBuilder(); Iterator<Integer> itr = list.iterator(); boolean first = true; while (itr.hasNext()) { Integer iVal = itr.next(); String val = iVal.toString(); if (!first) { buf.append(","); } buf.append(val); first = false; } return buf.toString(); } protected int skipWhiteSpace(int i, String s) { for (; i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == 't'); i++) { } return i; } protected int findNextWhiteSpace(int i, String s) { for (; i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != 't'); i++) { } return i; } protected void addToSet(int val, int end, int incr, int type) throws ParseException { TreeSet<Integer> set = getSet(type); if (type == SECOND || type == MINUTE) { if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) { throw new ParseException( "Minute and Second values must be between 0 and 59", -1); } } else if (type == HOUR) { if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) { throw new ParseException( "Hour values must be between 0 and 23", -1); } } else if (type == DAY_OF_MONTH) { if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) { throw new ParseException( "Day of month values must be between 1 and 31", -1); } } else if (type == MONTH) { if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) { throw new ParseException( "Month values must be between 1 and 12", -1); } } else if (type == DAY_OF_WEEK) { if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) { throw new ParseException( "Day-of-Week values must be between 1 and 7", -1); } } if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) { if (val != -1) { set.add(val); } else { set.add(NO_SPEC); } return; } int startAt = val; int stopAt = end; if (val == ALL_SPEC_INT && incr <= 0) { incr = 1; set.add(ALL_SPEC); // put in a marker, but also fill values } if (type == SECOND || type == MINUTE) { if (stopAt == -1) { stopAt = 59; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 0; } } else if (type == HOUR) { if (stopAt == -1) { stopAt = 23; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 0; } } else if (type == DAY_OF_MONTH) { if (stopAt == -1) { stopAt = 31; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1; } } else if (type == MONTH) { if (stopAt == -1) { stopAt = 12; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1; } } else if (type == DAY_OF_WEEK) { if (stopAt == -1) { stopAt = 7; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1; } } else if (type == YEAR) { if (stopAt == -1) { stopAt = MAX_YEAR; } if (startAt == -1 || startAt == ALL_SPEC_INT) { startAt = 1970; } } // if the end of the range is before the start, then we need to overflow into // the next day, month etc. This is done by adding the maximum amount for that // type, and using modulus max to determine the value being added. int max = -1; if (stopAt < startAt) { switch (type) { case SECOND : max = 60; break; case MINUTE : max = 60; break; case HOUR : max = 24; break; case MONTH : max = 12; break; case DAY_OF_WEEK : max = 7; break; case DAY_OF_MONTH : max = 31; break; case YEAR : throw new IllegalArgumentException("Start year must be less than stop year"); default : throw new IllegalArgumentException("Unexpected type encountered"); } stopAt += max; } for (int i = startAt; i <= stopAt; i += incr) { if (max == -1) { // ie: there's no max to overflow over set.add(i); } else { // take the modulus to get the real value int i2 = i % max; // 1-indexed ranges should not include 0, and should include their max if (i2 == 0 && (type == MONTH || type == DAY_OF_WEEK || type == DAY_OF_MONTH) ) { i2 = max; } set.add(i2); } } } TreeSet<Integer> getSet(int type) { switch (type) { case SECOND: return seconds; case MINUTE: return minutes; case HOUR: return hours; case DAY_OF_MONTH: return daysOfMonth; case MONTH: return months; case DAY_OF_WEEK: return daysOfWeek; case YEAR: return years; default: return null; } } protected ValueSet getValue(int v, String s, int i) { char c = s.charAt(i); StringBuilder s1 = new StringBuilder(String.valueOf(v)); while (c >= '0' && c <= '9') { s1.append(c); i++; if (i >= s.length()) { break; } c = s.charAt(i); } ValueSet val = new ValueSet(); val.pos = (i < s.length()) ? i : i + 1; val.value = Integer.parseInt(s1.toString()); return val; } protected int getNumericValue(String s, int i) { int endOfVal = findNextWhiteSpace(i, s); String val = s.substring(i, endOfVal); return Integer.parseInt(val); } protected int getMonthNumber(String s) { Integer integer = monthMap.get(s); if (integer == null) { return -1; } return integer; } protected int getDayOfWeekNumber(String s) { Integer integer = dayMap.get(s); if (integer == null) { return -1; } return integer; } //////////////////////////////////////////////////////////////////////////// // // Computation Functions // //////////////////////////////////////////////////////////////////////////// public Date getTimeAfter(Date afterTime) { // Computation is based on Gregorian year only. Calendar cl = new java.util.GregorianCalendar(getTimeZone()); // move ahead one second, since we're computing the time *after* the // given time afterTime = new Date(afterTime.getTime() + 1000); // CronTrigger does not deal with milliseconds cl.setTime(afterTime); cl.set(Calendar.MILLISECOND, 0); boolean gotOne = false; // loop until we've computed the next time, or we've past the endTime while (!gotOne) { //if (endTime != null && cl.getTime().after(endTime)) return null; if(cl.get(Calendar.YEAR) > 2999) { // prevent endless loop... return null; } SortedSet<Integer> st = null; int t = 0; int sec = cl.get(Calendar.SECOND); int min = cl.get(Calendar.MINUTE); // get second................................................. st = seconds.tailSet(sec); if (st != null && st.size() != 0) { sec = st.first(); } else { sec = seconds.first(); min++; cl.set(Calendar.MINUTE, min); } cl.set(Calendar.SECOND, sec); min = cl.get(Calendar.MINUTE); int hr = cl.get(Calendar.HOUR_OF_DAY); t = -1; // get minute................................................. st = minutes.tailSet(min); if (st != null && st.size() != 0) { t = min; min = st.first(); } else { min = minutes.first(); hr++; } if (min != t) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, min); setCalendarHour(cl, hr); continue; } cl.set(Calendar.MINUTE, min); hr = cl.get(Calendar.HOUR_OF_DAY); int day = cl.get(Calendar.DAY_OF_MONTH); t = -1; // get hour................................................... st = hours.tailSet(hr); if (st != null && st.size() != 0) { t = hr; hr = st.first(); } else { hr = hours.first(); day++; } if (hr != t) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.DAY_OF_MONTH, day); setCalendarHour(cl, hr); continue; } cl.set(Calendar.HOUR_OF_DAY, hr); day = cl.get(Calendar.DAY_OF_MONTH); int mon = cl.get(Calendar.MONTH) + 1; // '+ 1' because calendar is 0-based for this field, and we are // 1-based t = -1; int tmon = mon; // get day................................................... boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC); boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC); if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule st = daysOfMonth.tailSet(day); if (lastdayOfMonth) { if(!nearestWeekday) { t = day; day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); day -= lastdayOffset; if(t > day) { mon++; if(mon > 12) { mon = 1; tmon = 3333; // ensure test of mon != tmon further below fails cl.add(Calendar.YEAR, 1); } day = 1; } } else { t = day; day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); day -= lastdayOffset; Calendar tcal = Calendar.getInstance(getTimeZone()); tcal.set(Calendar.SECOND, 0); tcal.set(Calendar.MINUTE, 0); tcal.set(Calendar.HOUR_OF_DAY, 0); tcal.set(Calendar.DAY_OF_MONTH, day); tcal.set(Calendar.MONTH, mon - 1); tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); int dow = tcal.get(Calendar.DAY_OF_WEEK); if(dow == Calendar.SATURDAY && day == 1) { day += 2; } else if(dow == Calendar.SATURDAY) { day -= 1; } else if(dow == Calendar.SUNDAY && day == ldom) { day -= 2; } else if(dow == Calendar.SUNDAY) { day += 1; } tcal.set(Calendar.SECOND, sec); tcal.set(Calendar.MINUTE, min); tcal.set(Calendar.HOUR_OF_DAY, hr); tcal.set(Calendar.DAY_OF_MONTH, day); tcal.set(Calendar.MONTH, mon - 1); Date nTime = tcal.getTime(); if(nTime.before(afterTime)) { day = 1; mon++; } } } else if(nearestWeekday) { t = day; day = daysOfMonth.first(); Calendar tcal = Calendar.getInstance(getTimeZone()); tcal.set(Calendar.SECOND, 0); tcal.set(Calendar.MINUTE, 0); tcal.set(Calendar.HOUR_OF_DAY, 0); tcal.set(Calendar.DAY_OF_MONTH, day); tcal.set(Calendar.MONTH, mon - 1); tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR)); int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); int dow = tcal.get(Calendar.DAY_OF_WEEK); if(dow == Calendar.SATURDAY && day == 1) { day += 2; } else if(dow == Calendar.SATURDAY) { day -= 1; } else if(dow == Calendar.SUNDAY && day == ldom) { day -= 2; } else if(dow == Calendar.SUNDAY) { day += 1; } tcal.set(Calendar.SECOND, sec); tcal.set(Calendar.MINUTE, min); tcal.set(Calendar.HOUR_OF_DAY, hr); tcal.set(Calendar.DAY_OF_MONTH, day); tcal.set(Calendar.MONTH, mon - 1); Date nTime = tcal.getTime(); if(nTime.before(afterTime)) { day = daysOfMonth.first(); mon++; } } else if (st != null && st.size() != 0) { t = day; day = st.first(); // make sure we don't over-run a short month, such as february int lastDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); if (day > lastDay) { day = daysOfMonth.first(); mon++; } } else { day = daysOfMonth.first(); mon++; } if (day != t || mon != tmon) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, day); cl.set(Calendar.MONTH, mon - 1); // '- 1' because calendar is 0-based for this field, and we // are 1-based continue; } } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule if (lastdayOfWeek) { // are we looking for the last XXX day of // the month? int dow = daysOfWeek.first(); // desired // d-o-w int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w int daysToAdd = 0; if (cDow < dow) { daysToAdd = dow - cDow; } if (cDow > dow) { daysToAdd = dow + (7 - cDow); } int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); if (day + daysToAdd > lDay) { // did we already miss the // last one? cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, 1); cl.set(Calendar.MONTH, mon); // no '- 1' here because we are promoting the month continue; } // find date of last occurrence of this day in this month... while ((day + daysToAdd + 7) <= lDay) { daysToAdd += 7; } day += daysToAdd; if (daysToAdd > 0) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, day); cl.set(Calendar.MONTH, mon - 1); // '- 1' here because we are not promoting the month continue; } } else if (nthdayOfWeek != 0) { // are we looking for the Nth XXX day in the month? int dow = daysOfWeek.first(); // desired // d-o-w int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w int daysToAdd = 0; if (cDow < dow) { daysToAdd = dow - cDow; } else if (cDow > dow) { daysToAdd = dow + (7 - cDow); } boolean dayShifted = false; if (daysToAdd > 0) { dayShifted = true; } day += daysToAdd; int weekOfMonth = day / 7; if (day % 7 > 0) { weekOfMonth++; } daysToAdd = (nthdayOfWeek - weekOfMonth) * 7; day += daysToAdd; if (daysToAdd < 0 || day > getLastDayOfMonth(mon, cl .get(Calendar.YEAR))) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, 1); cl.set(Calendar.MONTH, mon); // no '- 1' here because we are promoting the month continue; } else if (daysToAdd > 0 || dayShifted) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, day); cl.set(Calendar.MONTH, mon - 1); // '- 1' here because we are NOT promoting the month continue; } } else { int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w int dow = daysOfWeek.first(); // desired // d-o-w st = daysOfWeek.tailSet(cDow); if (st != null && st.size() > 0) { dow = st.first(); } int daysToAdd = 0; if (cDow < dow) { daysToAdd = dow - cDow; } if (cDow > dow) { daysToAdd = dow + (7 - cDow); } int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR)); if (day + daysToAdd > lDay) { // will we pass the end of // the month? cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, 1); cl.set(Calendar.MONTH, mon); // no '- 1' here because we are promoting the month continue; } else if (daysToAdd > 0) { // are we swithing days? cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd); cl.set(Calendar.MONTH, mon - 1); // '- 1' because calendar is 0-based for this field, // and we are 1-based continue; } } } else { // dayOfWSpec && !dayOfMSpec throw new UnsupportedOperationException( "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); } cl.set(Calendar.DAY_OF_MONTH, day); mon = cl.get(Calendar.MONTH) + 1; // '+ 1' because calendar is 0-based for this field, and we are // 1-based int year = cl.get(Calendar.YEAR); t = -1; // test for expressions that never generate a valid fire date, // but keep looping... if (year > MAX_YEAR) { return null; } // get month................................................... st = months.tailSet(mon); if (st != null && st.size() != 0) { t = mon; mon = st.first(); } else { mon = months.first(); year++; } if (mon != t) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, 1); cl.set(Calendar.MONTH, mon - 1); // '- 1' because calendar is 0-based for this field, and we are // 1-based cl.set(Calendar.YEAR, year); continue; } cl.set(Calendar.MONTH, mon - 1); // '- 1' because calendar is 0-based for this field, and we are // 1-based year = cl.get(Calendar.YEAR); t = -1; // get year................................................... st = years.tailSet(year); if (st != null && st.size() != 0) { t = year; year = st.first(); } else { return null; // ran out of years... } if (year != t) { cl.set(Calendar.SECOND, 0); cl.set(Calendar.MINUTE, 0); cl.set(Calendar.HOUR_OF_DAY, 0); cl.set(Calendar.DAY_OF_MONTH, 1); cl.set(Calendar.MONTH, 0); // '- 1' because calendar is 0-based for this field, and we are // 1-based cl.set(Calendar.YEAR, year); continue; } cl.set(Calendar.YEAR, year); gotOne = true; } // while( !done ) return cl.getTime(); } /** * Advance the calendar to the particular hour paying particular attention * to daylight saving problems. * * @param cal the calendar to operate on * @param hour the hour to set */ protected void setCalendarHour(Calendar cal, int hour) { cal.set(Calendar.HOUR_OF_DAY, hour); if (cal.get(Calendar.HOUR_OF_DAY) != hour && hour != 24) { cal.set(Calendar.HOUR_OF_DAY, hour + 1); } } /** * NOT YET IMPLEMENTED: Returns the time before the given time * that the <code>CronExpression</code> matches. */ public Date getTimeBefore(Date endTime) { // FUTURE_TODO: implement QUARTZ-423 return null; } /** * NOT YET IMPLEMENTED: Returns the final time that the * <code>CronExpression</code> will match. */ public Date getFinalFireTime() { // FUTURE_TODO: implement QUARTZ-423 return null; } protected boolean isLeapYear(int year) { return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); } protected int getLastDayOfMonth(int monthNum, int year) { switch (monthNum) { case 1: return 31; case 2: return (isLeapYear(year)) ? 29 : 28; case 3: return 31; case 4: return 30; case 5: return 31; case 6: return 30; case 7: return 31; case 8: return 31; case 9: return 30; case 10: return 31; case 11: return 30; case 12: return 31; default: throw new IllegalArgumentException("Illegal month number: " + monthNum); } } private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { stream.defaultReadObject(); try { buildExpression(cronExpression); } catch (Exception ignore) { } // never happens } @Override @Deprecated public Object clone() { return new CronExpression(this); } } class ValueSet { public int value; public int pos; }
package com.xgss.xxljob; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; /** * @ClassName XxlJobActuatorManagerInfo * @Author xgss * @Date 2022/12/1 15:38 * @Version 1.0 **/ @Data @NoArgsConstructor @AllArgsConstructor public class XxlJobActuatorManagerInfo { private Integer recordsFiltered; private Integer recordsTotal; private List<XxlJobGroup> data; }
package com.xgss.xxljob; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; /** * @ClassName XxlJobGroup * @Author xgss * @Date 2022/12/1 15:31 * @Version 1.0 **/ @Data @NoArgsConstructor @AllArgsConstructor public class XxlJobGroup { private int id; private String appname; private String title; private int addressType; // 执行器地址类型:0=自动注册、1=手动录入 private String addressList; // 执行器地址列表,多地址逗号分隔(手动录入) private Date updateTime; // registry list private List<String> registryList; // 执行器地址列表(系统注册) public List<String> getRegistryList() { if (addressList!=null && addressList.trim().length()>0) { registryList = new ArrayList<String>(Arrays.asList(addressList.split(","))); } return registryList; } }
package com.xgss.xxljob; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.Date; /** * @ClassName XxlJobInfo * @Author xgss * @Date 2022/12/1 15:14 * @Version 1.0 **/ @Data @NoArgsConstructor @AllArgsConstructor public class XxlJobInfo { private int id; // 主键ID private int jobGroup; // 执行器主键ID private String jobDesc; private String jobCron; //corn表达式 private Date addTime; private Date updateTime; private String author; // 负责人 private String alarmEmail; // 报警邮件 private String scheduleType; // 调度类型 private String scheduleConf; // 调度配置,值含义取决于调度类型 private String misfireStrategy; // 调度过期策略 private String executorRouteStrategy; // 执行器路由策略 private String executorHandler; // 执行器,任务Handler名称 private String executorParam; // 执行器,任务参数 private String executorBlockStrategy; // 阻塞处理策略 private int executorTimeout; // 任务执行超时时间,单位秒 private int executorFailRetryCount; // 失败重试次数 private String glueType; // GLUE类型 #com.xxl.job.core.glue.GlueTypeEnum private String glueSource; // GLUE源代码 private String glueRemark; // GLUE备注 private Date glueUpdatetime; // GLUE更新时间 private String childJobId; // 子任务ID,多个逗号分隔 private int triggerStatus; // 调度状态:0-停止,1-运行 private long triggerLastTime; // 上次调度时间 private long triggerNextTime; // 下次调度时间 } /** * 路由策略说明 * FIRST:第一个 * LAST:最后一个 * ROUND:轮询 * RANDOM:随机 * CONSISTENT_HASH:一致性HASH * LEAST_FREQUENTLY_USED:最不经常使用 * LEAST_RECENTLY_USED:最近最久未使用 * FAILOVER:故障转移 * BUSYOVER:忙碌转移 * SHARDING_BROADCAST:分片广播 */ /** * 阻塞处理策略 * SERIAL_EXECUTION:单机串行 * DISCARD_LATER:丢弃后续调度 * COVER_EARLY:覆盖之前调度 */ //
package com.xgss.xxljob; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @ClassName XxlJobResponseVO * @Author xgss * @Date 2022/12/1 15:08 * @Version 1.0 **/ @Data @NoArgsConstructor @AllArgsConstructor public class XxlJobResponseInfo { private Integer code; private String msg; private String content; }
package com.xgss.xxljob; import com.xgss.xxljob.XxlJobInfo; import java.util.List; /** * @ClassName XxlJobTaskManagerInfoVO * @Author xgss * @Date 2022/12/1 15:20 * @Version 1.0 **/ public class XxlJobTaskManagerInfo { private Integer recordsFiltered; private Integer recordsTotal; private List<XxlJobInfo> data; }