├── README.md ├── easy-job-core ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── rdpaas │ └── task │ ├── annotation │ ├── ContextRefreshedListener.java │ └── Scheduled.java │ ├── common │ ├── Invocation.java │ ├── Node.java │ ├── NodeStatus.java │ ├── NotifyCmd.java │ ├── Task.java │ ├── TaskDetail.java │ └── TaskStatus.java │ ├── config │ └── EasyJobConfig.java │ ├── handles │ ├── NotifyHandler.java │ └── StopTaskHandler.java │ ├── repository │ ├── NodeRepository.java │ └── TaskRepository.java │ ├── scheduler │ ├── DelayItem.java │ ├── RecoverExecutor.java │ └── TaskExecutor.java │ ├── serializer │ ├── JdkSerializationSerializer.java │ └── ObjectSerializer.java │ ├── strategy │ ├── DefaultStrategy.java │ ├── IdHashStrategy.java │ ├── LeastCountStrategy.java │ ├── Strategy.java │ └── WeightStrategy.java │ └── utils │ ├── CronExpression.java │ ├── Delimiters.java │ └── SpringContextUtil.java ├── easy-job-sample ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── rdpaas │ │ │ ├── Application.java │ │ │ └── task │ │ │ └── sample │ │ │ └── SchedulerTest.java │ └── resources │ │ ├── application.yml │ │ └── task_scheduling.sql │ └── test │ └── java │ └── com │ └── rdpaas │ └── task │ ├── TaskTest.java │ └── Test.java └── pom.xml /README.md: -------------------------------------------------------------------------------- 1 | # easy-job 2 | 简单的分布式任务调度,详细代码介绍,请看博客: 3 | https://www.cnblogs.com/rongdi/p/10548613.html 4 | https://www.cnblogs.com/rongdi/p/11940402.html 5 | -------------------------------------------------------------------------------- /easy-job-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.rdpaas 7 | easy-job-core 8 | 0.0.2-SNAPSHOT 9 | jar 10 | 11 | easy-job-core 12 | easy-job-core 13 | 14 | 15 | com.rdpaas 16 | easy-job-parent 17 | 0.0.2-SNAPSHOT 18 | 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-jdbc 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | src/main/java 34 | 35 | **/*.xml 36 | 37 | 38 | 39 | src/main/resources 40 | 41 | **/* 42 | 43 | 44 | 45 | 46 | 47 | ${project.basedir}/src/test/java 48 | 49 | 50 | ${project.basedir}/src/test/resources 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.apache.maven.plugins 59 | maven-compiler-plugin 60 | 61 | ${java.version} 62 | ${java.version} 63 | ${project.build.sourceEncoding} 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-javadoc-plugin 70 | 3.0.0 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/annotation/ContextRefreshedListener.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.annotation; 2 | 3 | import com.rdpaas.task.common.Invocation; 4 | import com.rdpaas.task.config.EasyJobConfig; 5 | import com.rdpaas.task.repository.TaskRepository; 6 | import com.rdpaas.task.scheduler.TaskExecutor; 7 | import com.rdpaas.task.utils.Delimiters; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.ApplicationListener; 12 | import org.springframework.context.event.ContextRefreshedEvent; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.lang.reflect.Method; 16 | import java.util.Date; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Set; 21 | 22 | /** 23 | * spring容器启动完后,加载自定义注解 24 | * @author rongdi 25 | * @date 2019-03-15 21:07 26 | */ 27 | @Component 28 | public class ContextRefreshedListener implements ApplicationListener { 29 | 30 | @Autowired 31 | private TaskExecutor taskExecutor; 32 | 33 | @Autowired 34 | private TaskRepository taskRepository; 35 | 36 | @Autowired 37 | private EasyJobConfig config; 38 | /** 39 | * 用来保存方法名/任务名和任务插入后数据库的ID的映射,用来处理子任务新增用 40 | */ 41 | private Map taskIdMap = new HashMap<>(); 42 | 43 | /** 44 | * 存放数据库所有的任务名称 45 | */ 46 | private List allTaskNames; 47 | 48 | 49 | @Override 50 | public void onApplicationEvent(ContextRefreshedEvent event) { 51 | /** 52 | * 初始化系统启动时间,用于解决系统重启后,还是按照之前时间执行任务 53 | */ 54 | config.setSysStartTime(new Date()); 55 | /** 56 | * 重启重新初始化本节点的任务状态 57 | */ 58 | taskRepository.reInitTasks(); 59 | /** 60 | * 查出数据库所有的任务名称 61 | */ 62 | allTaskNames = taskRepository.listAllTaskNames(); 63 | /** 64 | * 判断根容器为Spring容器,防止出现调用两次的情况(mvc加载也会触发一次) 65 | */ 66 | if(event.getApplicationContext().getParent()==null){ 67 | /** 68 | * 判断调度开关是否打开 69 | * 如果打开了:加载调度注解并将调度添加到调度管理中 70 | */ 71 | ApplicationContext context = event.getApplicationContext(); 72 | Map beans = context.getBeansWithAnnotation(org.springframework.scheduling.annotation.EnableScheduling.class); 73 | if(beans == null) { 74 | return; 75 | } 76 | /** 77 | * 用来存放被调度注解修饰的方法名和Method的映射 78 | */ 79 | Map methodMap = new HashMap<>(); 80 | /** 81 | * 查找所有直接或者间接被Component注解修饰的类,因为不管Service,Controller等都包含了Component,也就是 82 | * 只要是被纳入了spring容器管理的类必然直接或者间接的被Component修饰 83 | */ 84 | Map allBeans = context.getBeansWithAnnotation(org.springframework.stereotype.Component.class); 85 | Set> entrys = allBeans.entrySet(); 86 | /** 87 | * 遍历bean和里面的method找到被Scheduled注解修饰的方法,然后将任务放入任务调度里 88 | */ 89 | for(Map.Entry entry:entrys){ 90 | Object obj = entry.getValue(); 91 | Class clazz = obj.getClass(); 92 | Method[] methods = clazz.getMethods(); 93 | for(Method m:methods) { 94 | if(m.isAnnotationPresent(Scheduled.class)) { 95 | methodMap.put(clazz.getName() + Delimiters.DOT + m.getName(),m); 96 | } 97 | } 98 | } 99 | /** 100 | * 处理Sheduled注解 101 | */ 102 | handleSheduledAnn(methodMap); 103 | /** 104 | * 由于taskIdMap只是启动spring完成后使用一次,这里可以直接清空 105 | */ 106 | taskIdMap.clear(); 107 | } 108 | } 109 | 110 | /** 111 | * 循环处理方法map中的所有Method 112 | * @param methodMap 113 | */ 114 | private void handleSheduledAnn(Map methodMap) { 115 | if(methodMap == null || methodMap.isEmpty()) { 116 | return; 117 | } 118 | Set> entrys = methodMap.entrySet(); 119 | /** 120 | * 遍历bean和里面的method找到被Scheduled注解修饰的方法,然后将任务放入任务调度里 121 | */ 122 | for(Map.Entry entry:entrys){ 123 | Method m = entry.getValue(); 124 | try { 125 | handleSheduledAnn(methodMap,m); 126 | } catch (Exception e) { 127 | e.printStackTrace(); 128 | continue; 129 | } 130 | } 131 | } 132 | 133 | /** 134 | * 递归添加父子任务 135 | * @param methodMap 136 | * @param m 137 | * @throws Exception 138 | */ 139 | private void handleSheduledAnn(Map methodMap,Method m) throws Exception { 140 | Class clazz = m.getDeclaringClass(); 141 | String name = m.getName(); 142 | Scheduled sAnn = m.getAnnotation(Scheduled.class); 143 | String cron = sAnn.cron(); 144 | String parent = sAnn.parent(); 145 | String finalName = clazz.getName() + Delimiters.DOT + name; 146 | /** 147 | * 如果parent为空,说明该方法代表的任务是根任务,则添加到任务调度器中,并且保存在全局map中 148 | * 如果parent不为空,则表示是子任务,子任务需要知道父任务的id 149 | * 先根据parent里面代表的方法全名或者方法名(父任务方法和子任务方法在同一个类直接可以用方法名, 150 | * 不然要带上类的全名)从taskIdMap获取父任务ID 151 | * 如果找不到父任务ID,先根据父方法全名在methodMap找到父任务的method对象,调用本方法递归下去 152 | * 如果找到父任务ID,则添加子任务 153 | */ 154 | if(StringUtils.isEmpty(parent)) { 155 | if(!taskIdMap.containsKey(finalName) && !allTaskNames.contains(finalName)) { 156 | Long taskId = taskExecutor.addTask(finalName, cron, new Invocation(clazz, name, new Class[]{}, new Object[]{})); 157 | taskIdMap.put(finalName, taskId); 158 | } 159 | } else { 160 | String parentMethodName = parent.lastIndexOf(Delimiters.DOT) == -1 ? clazz.getName() + Delimiters.DOT + parent : parent; 161 | Long parentTaskId = taskIdMap.get(parentMethodName); 162 | if(parentTaskId == null) { 163 | Method parentMethod = methodMap.get(parentMethodName); 164 | handleSheduledAnn(methodMap,parentMethod); 165 | /** 166 | * 递归回来一定要更新一下这个父任务ID 167 | */ 168 | parentTaskId = taskIdMap.get(parentMethodName); 169 | } 170 | if(parentTaskId != null && !taskIdMap.containsKey(finalName) && !allTaskNames.contains(finalName)) { 171 | Long taskId = taskExecutor.addChildTask(parentTaskId, finalName, cron, new Invocation(clazz, name, new Class[]{}, new Object[]{})); 172 | taskIdMap.put(finalName, taskId); 173 | } 174 | 175 | } 176 | 177 | 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/annotation/Scheduled.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target({ ElementType.METHOD }) 10 | public @interface Scheduled { 11 | String cron() default ""; 12 | String parent() default ""; 13 | } 14 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/common/Invocation.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.common; 2 | 3 | import com.rdpaas.task.utils.SpringContextUtil; 4 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; 5 | 6 | import java.io.Serializable; 7 | import java.lang.reflect.Method; 8 | 9 | /** 10 | * 任务执行方法,用于序列化保存在数据库 11 | * @author rongdi 12 | * @date 2019-03-12 19:01 13 | */ 14 | public class Invocation implements Serializable { 15 | 16 | private Class targetClass; 17 | 18 | private String methodName; 19 | 20 | private Class[] parameterTypes; 21 | 22 | private Object[] args; 23 | 24 | public Invocation() { 25 | 26 | } 27 | 28 | public Invocation(Class targetClass, String methodName, Class[] parameterTypes, Object... args) { 29 | this.methodName = methodName; 30 | this.parameterTypes = parameterTypes; 31 | this.targetClass = targetClass; 32 | this.args = args; 33 | } 34 | 35 | public Object[] getArgs() { 36 | return args; 37 | } 38 | 39 | public Class getTargetClass() { 40 | return targetClass; 41 | } 42 | 43 | public String getMethodName() { 44 | return methodName; 45 | } 46 | 47 | public Class[] getParameterTypes() { 48 | return parameterTypes; 49 | } 50 | 51 | public Object invoke() throws Exception { 52 | Object target = null; 53 | try { 54 | target = SpringContextUtil.getBean(targetClass); 55 | } catch(NoSuchBeanDefinitionException e) { 56 | target = Class.forName(targetClass.getName()); 57 | } 58 | Method method = target.getClass().getMethod(methodName,parameterTypes); 59 | // 调用服务方法 60 | return method.invoke(targetClass.newInstance(), args); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/common/Node.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.common; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * 执行节点对象 7 | * @author rongdi 8 | * @date 2019-03-14 21:12 9 | */ 10 | public class Node { 11 | 12 | private Long id; 13 | 14 | /** 15 | * 节点ID,必须唯一 16 | */ 17 | private Long nodeId; 18 | 19 | /** 20 | * 节点状态,0表示不可用,1表示可用 21 | */ 22 | private NodeStatus status; 23 | 24 | /** 25 | * 节点序号 26 | */ 27 | private Long rownum; 28 | 29 | /** 30 | * 执行任务次数 31 | */ 32 | private Long counts; 33 | 34 | /** 35 | * 权重,默认都是1 36 | */ 37 | private Integer weight = 1; 38 | 39 | /** 40 | * 通知指令 41 | */ 42 | private NotifyCmd notifyCmd = NotifyCmd.NO_NOTIFY; 43 | 44 | /** 45 | * 通知值 46 | */ 47 | private String notifyValue; 48 | 49 | /** 50 | * 节点创建时间 51 | */ 52 | private Date createTime = new Date(); 53 | 54 | /** 55 | * 更新时间 56 | */ 57 | private Date updateTime = new Date(); 58 | 59 | public Node(Long nodeId) { 60 | this.nodeId = nodeId; 61 | } 62 | 63 | public Node() { 64 | } 65 | 66 | public Long getId() { 67 | return id; 68 | } 69 | 70 | public void setId(Long id) { 71 | this.id = id; 72 | } 73 | 74 | public Long getNodeId() { 75 | return nodeId; 76 | } 77 | 78 | public void setNodeId(Long nodeId) { 79 | this.nodeId = nodeId; 80 | } 81 | 82 | public Long getRownum() { 83 | return rownum; 84 | } 85 | 86 | public void setRownum(Long rownum) { 87 | this.rownum = rownum; 88 | } 89 | 90 | public Long getCounts() { 91 | return counts; 92 | } 93 | 94 | public void setCounts(Long counts) { 95 | this.counts = counts; 96 | } 97 | 98 | public Integer getWeight() { 99 | return weight; 100 | } 101 | 102 | public void setWeight(Integer weight) { 103 | this.weight = weight; 104 | } 105 | 106 | public Date getCreateTime() { 107 | return createTime; 108 | } 109 | 110 | public void setCreateTime(Date createTime) { 111 | this.createTime = createTime; 112 | } 113 | 114 | public Date getUpdateTime() { 115 | return updateTime; 116 | } 117 | 118 | public void setUpdateTime(Date updateTime) { 119 | this.updateTime = updateTime; 120 | } 121 | 122 | public NodeStatus getStatus() { 123 | return status; 124 | } 125 | 126 | public void setStatus(int status) { 127 | this.status = NodeStatus.valueOf(status); 128 | } 129 | 130 | public NotifyCmd getNotifyCmd() { 131 | return notifyCmd; 132 | } 133 | 134 | public void setNotifyCmd(int notifyCmd) { 135 | this.notifyCmd = NotifyCmd.valueOf(notifyCmd); 136 | } 137 | 138 | public String getNotifyValue() { 139 | return notifyValue; 140 | } 141 | 142 | public void setNotifyValue(String notifyValue) { 143 | this.notifyValue = notifyValue; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/common/NodeStatus.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.common; 2 | 3 | /** 4 | * 节点状态枚举类 5 | * @author rongdi 6 | * @date 2019-03-14 21:04 7 | */ 8 | public enum NodeStatus { 9 | 10 | //待执行 11 | DISABLE(0), 12 | //执行中 13 | ENABLE(1); 14 | 15 | int id; 16 | 17 | NodeStatus(int id) { 18 | this.id = id; 19 | } 20 | 21 | public int getId() { 22 | return id; 23 | } 24 | 25 | public static NodeStatus valueOf(int id) { 26 | switch (id) { 27 | case 1: 28 | return ENABLE; 29 | default: 30 | return DISABLE; 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/common/NotifyCmd.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.common; 2 | 3 | /** 4 | * @author rongdi 5 | * @date 2019/11/26 6 | */ 7 | public enum NotifyCmd { 8 | 9 | //没有通知,默认状态 10 | NO_NOTIFY(0), 11 | //开启任务(Task) 12 | START_TASK(1), 13 | //修改任务(Task) 14 | EDIT_TASK(2), 15 | //停止任务(Task) 16 | STOP_TASK(3); 17 | 18 | int id; 19 | 20 | NotifyCmd(int id) { 21 | this.id = id; 22 | } 23 | 24 | public int getId() { 25 | return id; 26 | } 27 | 28 | public static NotifyCmd valueOf(int id) { 29 | switch (id) { 30 | case 1: 31 | return START_TASK; 32 | case 2: 33 | return EDIT_TASK; 34 | case 3: 35 | return STOP_TASK; 36 | default: 37 | return NO_NOTIFY; 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/common/Task.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.common; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * 任务实体 7 | * @author rongdi 8 | * @date 2019-03-12 19:02 9 | */ 10 | public class Task { 11 | 12 | private Long id; 13 | 14 | /** 15 | * 父任务id 16 | */ 17 | private Long pid; 18 | 19 | /** 20 | * 调度名称 21 | */ 22 | private String name; 23 | 24 | /** 25 | * cron表达式 26 | */ 27 | private String cronExpr; 28 | 29 | /** 30 | * 当前执行的节点id 31 | */ 32 | private Long nodeId; 33 | 34 | /** 35 | * 状态,0表示未开始,1表示待执行,2表示执行中,3表示已完成 36 | */ 37 | private TaskStatus status = TaskStatus.NOT_STARTED; 38 | 39 | /** 40 | * 成功次数 41 | */ 42 | private Integer successCount; 43 | 44 | /** 45 | * 失败次数 46 | */ 47 | private Integer failCount; 48 | 49 | /** 50 | * 执行信息 51 | */ 52 | private byte[] invokeInfo; 53 | 54 | /** 55 | * 乐观锁标识 56 | */ 57 | private Integer version; 58 | 59 | /** 60 | * 首次开始时间 61 | */ 62 | private Date firstStartTime; 63 | 64 | /** 65 | * 下次开始时间 66 | */ 67 | private Date nextStartTime; 68 | 69 | /** 70 | * 创建时间 71 | */ 72 | private Date createTime = new Date(); 73 | 74 | /** 75 | * 更新时间 76 | */ 77 | private Date updateTime = new Date(); 78 | 79 | /** 80 | * 任务的执行者 81 | */ 82 | private Invocation invokor; 83 | 84 | public Task() { 85 | } 86 | 87 | public Task(String name, String cronExpr, Invocation invokor) { 88 | this.name = name; 89 | this.cronExpr = cronExpr; 90 | this.invokor = invokor; 91 | } 92 | 93 | public Long getId() { 94 | return id; 95 | } 96 | 97 | public void setId(Long id) { 98 | this.id = id; 99 | } 100 | 101 | public String getName() { 102 | return name; 103 | } 104 | 105 | public void setName(String name) { 106 | this.name = name; 107 | } 108 | 109 | public Long getNodeId() { 110 | return nodeId; 111 | } 112 | 113 | public void setNodeId(Long nodeId) { 114 | this.nodeId = nodeId; 115 | } 116 | 117 | public Date getFirstStartTime() { 118 | return firstStartTime; 119 | } 120 | 121 | public void setFirstStartTime(Date firstStartTime) { 122 | this.firstStartTime = firstStartTime; 123 | } 124 | 125 | public Date getNextStartTime() { 126 | return nextStartTime; 127 | } 128 | 129 | public void setNextStartTime(Date nextStartTime) { 130 | this.nextStartTime = nextStartTime; 131 | } 132 | 133 | public Long getPid() { 134 | return pid; 135 | } 136 | 137 | public void setPid(Long pid) { 138 | this.pid = pid; 139 | } 140 | 141 | public byte[] getInvokeInfo() { 142 | return invokeInfo; 143 | } 144 | 145 | public void setInvokeInfo(byte[] invokeInfo) { 146 | this.invokeInfo = invokeInfo; 147 | } 148 | 149 | public Invocation getInvokor() { 150 | return invokor; 151 | } 152 | 153 | public void setInvokor(Invocation invokor) { 154 | this.invokor = invokor; 155 | } 156 | 157 | public Integer getSuccessCount() { 158 | return successCount; 159 | } 160 | 161 | public void setSuccessCount(Integer successCount) { 162 | this.successCount = successCount; 163 | } 164 | 165 | public Integer getFailCount() { 166 | return failCount; 167 | } 168 | 169 | public void setFailCount(Integer failCount) { 170 | this.failCount = failCount; 171 | } 172 | 173 | public String getCronExpr() { 174 | return cronExpr; 175 | } 176 | 177 | public void setCronExpr(String cronExpr) { 178 | this.cronExpr = cronExpr; 179 | } 180 | 181 | public TaskStatus getStatus() { 182 | return status; 183 | } 184 | 185 | public void setStatus(TaskStatus status) { 186 | this.status = status; 187 | } 188 | 189 | public Integer getVersion() { 190 | return version; 191 | } 192 | 193 | public void setVersion(Integer version) { 194 | this.version = version; 195 | } 196 | 197 | public Date getCreateTime() { 198 | return createTime; 199 | } 200 | 201 | public void setCreateTime(Date createTime) { 202 | this.createTime = createTime; 203 | } 204 | 205 | public Date getUpdateTime() { 206 | return updateTime; 207 | } 208 | 209 | public void setUpdateTime(Date updateTime) { 210 | this.updateTime = updateTime; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/common/TaskDetail.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.common; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * 任务实体详情类 7 | * @author rongdi 8 | * @date 2019-03-12 19:03 9 | */ 10 | public class TaskDetail { 11 | 12 | private Long id; 13 | 14 | /** 15 | * 任务id 16 | */ 17 | private Long taskId; 18 | 19 | /** 20 | * 所属父明细ID 21 | */ 22 | private Long pid; 23 | 24 | /** 25 | * 当前执行的节点id 26 | */ 27 | private Long nodeId; 28 | 29 | /** 30 | * 重试次数 31 | */ 32 | private Integer retryCount; 33 | 34 | /** 35 | * 状态,0表示待执行,1表示执行中,2表示异常中,3表示已完成 36 | * 添加了任务明细说明就开始执行了 37 | */ 38 | private TaskStatus status = TaskStatus.DOING; 39 | 40 | /** 41 | * 开始时间 42 | */ 43 | private Date startTime = new Date(); 44 | 45 | /** 46 | * 结束时间 47 | */ 48 | private Date endTime; 49 | 50 | /** 51 | * 乐观锁标识 52 | */ 53 | private Integer version; 54 | 55 | /** 56 | * 错误信息 57 | */ 58 | private String errorMsg; 59 | 60 | public TaskDetail() { 61 | } 62 | 63 | public TaskDetail(Long taskId) { 64 | this.taskId = taskId; 65 | } 66 | 67 | public Long getId() { 68 | return id; 69 | } 70 | 71 | public void setId(Long id) { 72 | this.id = id; 73 | } 74 | 75 | public Long getTaskId() { 76 | return taskId; 77 | } 78 | 79 | public void setTaskId(Long taskId) { 80 | this.taskId = taskId; 81 | } 82 | 83 | public Long getNodeId() { 84 | return nodeId; 85 | } 86 | 87 | public void setNodeId(Long nodeId) { 88 | this.nodeId = nodeId; 89 | } 90 | 91 | public Integer getRetryCount() { 92 | return retryCount; 93 | } 94 | 95 | public void setRetryCount(Integer retryCount) { 96 | this.retryCount = retryCount; 97 | } 98 | 99 | public TaskStatus getStatus() { 100 | return status; 101 | } 102 | 103 | public void setStatus(TaskStatus status) { 104 | this.status = status; 105 | } 106 | 107 | public Date getStartTime() { 108 | return startTime; 109 | } 110 | 111 | public void setStartTime(Date startTime) { 112 | this.startTime = startTime; 113 | } 114 | 115 | public Date getEndTime() { 116 | return endTime; 117 | } 118 | 119 | public void setEndTime(Date endTime) { 120 | this.endTime = endTime; 121 | } 122 | 123 | public String getErrorMsg() { 124 | return errorMsg; 125 | } 126 | 127 | public void setErrorMsg(String errorMsg) { 128 | this.errorMsg = errorMsg; 129 | } 130 | 131 | public Long getPid() { 132 | return pid; 133 | } 134 | 135 | public void setPid(Long pid) { 136 | this.pid = pid; 137 | } 138 | 139 | public Integer getVersion() { 140 | return version; 141 | } 142 | 143 | public void setVersion(Integer version) { 144 | this.version = version; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/common/TaskStatus.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.common; 2 | 3 | /** 4 | * 任务状态枚举类 5 | * @author rongdi 6 | * @date 2019-03-12 19:04 7 | */ 8 | public enum TaskStatus { 9 | 10 | //未开始 11 | NOT_STARTED(0), 12 | //待执行 13 | PENDING(1), 14 | //执行中 15 | DOING(2), 16 | //异常 17 | ERROR(3), 18 | //已完成 19 | FINISH(4), 20 | //已停止 21 | STOP(5); 22 | 23 | int id; 24 | 25 | TaskStatus(int id) { 26 | this.id = id; 27 | } 28 | 29 | public int getId() { 30 | return id; 31 | } 32 | 33 | public static TaskStatus valueOf(int id) { 34 | switch (id) { 35 | case 1: 36 | return PENDING; 37 | case 2: 38 | return DOING; 39 | case 3: 40 | return ERROR; 41 | case 4: 42 | return FINISH; 43 | case 5: 44 | return STOP; 45 | default: 46 | return NOT_STARTED; 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/config/EasyJobConfig.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.config; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.sql.DataSource; 11 | import java.util.Date; 12 | 13 | @Component 14 | public class EasyJobConfig { 15 | 16 | @Value("${easyjob.node.id:1}") 17 | private Long nodeId; 18 | 19 | /** 20 | * 节点取任务的策略 21 | */ 22 | @Value("${easyjob.node.strategy:default}") 23 | private String nodeStrategy; 24 | 25 | /** 26 | * 节点取任务的周期,单位是毫秒,默认100毫秒 27 | */ 28 | @Value("${easyjob.node.fetchPeriod:100}") 29 | private int fetchPeriod; 30 | 31 | /** 32 | * 节点取任务据当前的时间段,比如每次取还有5分钟开始的任务,这里单位是秒 33 | */ 34 | @Value("${easyjob.node.fetchDuration:300}") 35 | private int fetchDuration; 36 | 37 | /** 38 | * 线程池中队列大小 39 | */ 40 | @Value("${easyjob.pool.queueSize:1000}") 41 | private int queueSize; 42 | 43 | /** 44 | * 线程池中初始线程数量 45 | */ 46 | @Value("${easyjob.pool.coreSize:5}") 47 | private int corePoolSize; 48 | 49 | /** 50 | * 线程池中最大线程数量 51 | */ 52 | @Value("${easyjob.pool.maxSize:10}") 53 | private int maxPoolSize; 54 | 55 | /** 56 | * 节点心跳周期,单位秒 57 | */ 58 | @Value("${easyjob.heartBeat.seconds:20}") 59 | private int heartBeatSeconds; 60 | 61 | /** 62 | * 节点心跳开关,默认开 63 | */ 64 | @Value("${easyjob.heartBeat.enable:true}") 65 | private boolean heartBeatEnable; 66 | 67 | /** 68 | * 恢复线程开关,默认开 69 | */ 70 | @Value("${easyjob.recover.enable:true}") 71 | private boolean recoverEnable; 72 | 73 | /** 74 | * 恢复线程周期,默认60s 75 | */ 76 | @Value("${easyjob.recover.seconds:60}") 77 | private int recoverSeconds; 78 | 79 | @Bean(name = "easyjobDataSource") 80 | @Qualifier("easyjobDataSource") 81 | @ConfigurationProperties(prefix="easyjob.datasource") 82 | public DataSource primaryDataSource() { 83 | return DataSourceBuilder.create().build(); 84 | } 85 | 86 | /** 87 | * 系统启动时间 88 | */ 89 | private Date sysStartTime; 90 | 91 | public Long getNodeId() { 92 | return nodeId; 93 | } 94 | 95 | public void setNodeId(Long nodeId) { 96 | this.nodeId = nodeId; 97 | } 98 | 99 | public String getNodeStrategy() { 100 | return nodeStrategy; 101 | } 102 | 103 | public void setNodeStrategy(String nodeStrategy) { 104 | this.nodeStrategy = nodeStrategy; 105 | } 106 | 107 | public int getFetchPeriod() { 108 | return fetchPeriod; 109 | } 110 | 111 | public void setFetchPeriod(int fetchPeriod) { 112 | this.fetchPeriod = fetchPeriod; 113 | } 114 | 115 | public int getFetchDuration() { 116 | return fetchDuration; 117 | } 118 | 119 | public void setFetchDuration(int fetchDuration) { 120 | this.fetchDuration = fetchDuration; 121 | } 122 | 123 | public int getQueueSize() { 124 | return queueSize; 125 | } 126 | 127 | public void setQueueSize(int queueSize) { 128 | this.queueSize = queueSize; 129 | } 130 | 131 | public int getCorePoolSize() { 132 | return corePoolSize; 133 | } 134 | 135 | public void setCorePoolSize(int corePoolSize) { 136 | this.corePoolSize = corePoolSize; 137 | } 138 | 139 | public int getMaxPoolSize() { 140 | return maxPoolSize; 141 | } 142 | 143 | public void setMaxPoolSize(int maxPoolSize) { 144 | this.maxPoolSize = maxPoolSize; 145 | } 146 | 147 | public int getHeartBeatSeconds() { 148 | return heartBeatSeconds; 149 | } 150 | 151 | public void setHeartBeatSeconds(int heartBeatSeconds) { 152 | this.heartBeatSeconds = heartBeatSeconds; 153 | } 154 | 155 | public boolean isHeartBeatEnable() { 156 | return heartBeatEnable; 157 | } 158 | 159 | public void setHeartBeatEnable(boolean heartBeatEnable) { 160 | this.heartBeatEnable = heartBeatEnable; 161 | } 162 | 163 | public boolean isRecoverEnable() { 164 | return recoverEnable; 165 | } 166 | 167 | public void setRecoverEnable(boolean recoverEnable) { 168 | this.recoverEnable = recoverEnable; 169 | } 170 | 171 | public int getRecoverSeconds() { 172 | return recoverSeconds; 173 | } 174 | 175 | public void setRecoverSeconds(int recoverSeconds) { 176 | this.recoverSeconds = recoverSeconds; 177 | } 178 | 179 | public Date getSysStartTime() { 180 | return sysStartTime; 181 | } 182 | 183 | public void setSysStartTime(Date sysStartTime) { 184 | this.sysStartTime = sysStartTime; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/handles/NotifyHandler.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.handles; 2 | 3 | import com.rdpaas.task.common.NotifyCmd; 4 | import com.rdpaas.task.utils.SpringContextUtil; 5 | 6 | /** 7 | * @author: rongdi 8 | * @date: 9 | */ 10 | public interface NotifyHandler { 11 | 12 | static NotifyHandler chooseHandler(NotifyCmd notifyCmd) { 13 | return SpringContextUtil.getByTypeAndName(NotifyHandler.class,notifyCmd.toString()); 14 | } 15 | 16 | public void update(T t); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/handles/StopTaskHandler.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.handles; 2 | 3 | import com.rdpaas.task.scheduler.TaskExecutor; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * @author: rongdi 9 | * @date: 10 | */ 11 | @Component("STOP_TASK") 12 | public class StopTaskHandler implements NotifyHandler { 13 | 14 | @Autowired 15 | private TaskExecutor taskExecutor; 16 | 17 | @Override 18 | public void update(Long taskId) { 19 | taskExecutor.stop(taskId); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/repository/NodeRepository.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.repository; 2 | 3 | import com.rdpaas.task.common.Node; 4 | import com.rdpaas.task.common.NotifyCmd; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.dao.EmptyResultDataAccessException; 8 | import org.springframework.jdbc.core.BeanPropertyRowMapper; 9 | import org.springframework.jdbc.core.JdbcTemplate; 10 | import org.springframework.jdbc.core.PreparedStatementCreator; 11 | import org.springframework.jdbc.support.GeneratedKeyHolder; 12 | import org.springframework.jdbc.support.KeyHolder; 13 | import org.springframework.stereotype.Component; 14 | 15 | import java.sql.Connection; 16 | import java.sql.PreparedStatement; 17 | import java.sql.SQLException; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * 任务对象数据库操作对象 23 | * @author rongdi 24 | * @date 2019-03-12 19:13 25 | */ 26 | @Component 27 | public class NodeRepository { 28 | 29 | @Autowired 30 | @Qualifier("easyjobJdbcTemplate") 31 | private JdbcTemplate jdbcTemplate; 32 | 33 | public long insert(Node node) { 34 | String sql = "INSERT INTO easy_job_node(node_id,row_num,weight,notify_cmd,create_time,update_time) VALUES (?, ?, ?, ?, ?, ?);"; 35 | KeyHolder kh = new GeneratedKeyHolder(); 36 | jdbcTemplate.update(new PreparedStatementCreator() { 37 | @Override 38 | public PreparedStatement createPreparedStatement(Connection con) 39 | throws SQLException { 40 | //设置返回的主键字段名 41 | PreparedStatement ps = con.prepareStatement(sql,new String[]{"id"}); 42 | ps.setLong(1,node.getNodeId()); 43 | ps.setLong(2,node.getRownum()); 44 | ps.setInt(3,node.getWeight()); 45 | ps.setInt(4,node.getNotifyCmd().getId()); 46 | ps.setTimestamp(5, new java.sql.Timestamp(node.getCreateTime().getTime())); 47 | ps.setTimestamp(6, new java.sql.Timestamp(node.getUpdateTime().getTime())); 48 | return ps; 49 | } 50 | }, kh); 51 | return kh.getKey().longValue(); 52 | } 53 | 54 | /** 55 | * 更新节点心跳时间和序号 56 | * @param nodeId 待更新节点ID 57 | * @return 58 | * @throws Exception 59 | */ 60 | public int updateHeartBeat(Long nodeId) { 61 | StringBuilder sb = new StringBuilder(); 62 | sb.append("update easy_job_node set update_time = now(),row_num = (select tmp.rownum from (") 63 | .append("SELECT (@i:=@i+1) rownum,node_id FROM `easy_job_node`,(SELECT @i:=0) as rownum where status = 1) tmp where tmp.node_id = ?)") 64 | .append("where node_id = ?"); 65 | Object objs[] = {nodeId,nodeId}; 66 | return jdbcTemplate.update(sb.toString(), objs); 67 | } 68 | 69 | /** 70 | * 更新节点的通知信息,实现修改任务,停止任务通知等 71 | * @param cmd 通知指令 72 | * @param notifyValue 通知的值,一般存id 73 | * @return 74 | */ 75 | public int updateNotifyInfo(Long nodeId,NotifyCmd cmd,String notifyValue) { 76 | StringBuilder sb = new StringBuilder(); 77 | sb.append("update easy_job_node set notify_cmd = ?,notify_value = ? where node_id = ?"); 78 | List objList = new ArrayList<>(); 79 | objList.add(cmd.getId()); 80 | objList.add(notifyValue); 81 | 82 | return jdbcTemplate.update(sb.toString(), objList.toArray()); 83 | } 84 | 85 | /** 86 | * 当通知执行完后使用乐观锁重置通知信息 87 | * @param cmd 88 | * @return 89 | */ 90 | public int resetNotifyInfo(Long nodeId,NotifyCmd cmd) { 91 | StringBuilder sb = new StringBuilder(); 92 | sb.append("update easy_job_node set notify_cmd = ?,notify_value = ? "); 93 | sb.append("where notify_cmd = ? and node_id = ?"); 94 | List objList = new ArrayList<>(); 95 | objList.add(NotifyCmd.NO_NOTIFY); 96 | objList.add(""); 97 | objList.add(cmd.getId()); 98 | objList.add(nodeId); 99 | return jdbcTemplate.update(sb.toString(), objList.toArray()); 100 | } 101 | 102 | /** 103 | * 禁用节点 104 | * @param node 105 | * @return 106 | */ 107 | public int disbale(Node node) { 108 | StringBuilder sb = new StringBuilder(); 109 | sb.append("update easy_job_node set status = 0 ") 110 | .append("where node_id = ?"); 111 | Object objs[] = {node.getNodeId()}; 112 | return jdbcTemplate.update(sb.toString(), objs); 113 | } 114 | 115 | public List getEnableNodes(int timeout) { 116 | StringBuilder sb = new StringBuilder(); 117 | sb.append("select id,node_id as nodeId,row_num as rownum,counts,weight,status,notify_cmd as notifyCmd,notify_value as notifyValue,create_time as createTime,update_time as updateTime from easy_job_node n ") 118 | .append("where n.update_time > date_sub(now(), interval ? second) order by node_id"); 119 | Object args[] = {timeout}; 120 | return jdbcTemplate.query(sb.toString(),args,new BeanPropertyRowMapper(Node.class)); 121 | } 122 | 123 | public Node getByNodeId(Long nodeId) { 124 | String sql = "select id,node_id as nodeId,row_num as rownum,counts,weight,status,notify_cmd as notifyCmd,notify_value as notifyValue,create_time as createTime,update_time as updateTime from easy_job_node where node_id = ?"; 125 | Object objs[] = {nodeId}; 126 | try { 127 | return (Node) jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper(Node.class), objs); 128 | } catch (EmptyResultDataAccessException e) { 129 | return null; 130 | } 131 | } 132 | 133 | public long getNextRownum() { 134 | String sql = "select ifnull(max(row_num),0) + 1 as rownum from easy_job_node"; 135 | return jdbcTemplate.queryForObject(sql,Long.class); 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/repository/TaskRepository.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.repository; 2 | 3 | import com.rdpaas.task.common.Invocation; 4 | import com.rdpaas.task.common.Task; 5 | import com.rdpaas.task.common.TaskDetail; 6 | import com.rdpaas.task.common.TaskStatus; 7 | import com.rdpaas.task.config.EasyJobConfig; 8 | import com.rdpaas.task.serializer.JdkSerializationSerializer; 9 | import com.rdpaas.task.serializer.ObjectSerializer; 10 | import com.rdpaas.task.utils.CronExpression; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.beans.factory.annotation.Qualifier; 13 | import org.springframework.jdbc.core.BeanPropertyRowMapper; 14 | import org.springframework.jdbc.core.JdbcTemplate; 15 | import org.springframework.jdbc.core.PreparedStatementCreator; 16 | import org.springframework.jdbc.support.GeneratedKeyHolder; 17 | import org.springframework.jdbc.support.KeyHolder; 18 | import org.springframework.stereotype.Component; 19 | 20 | import java.sql.Connection; 21 | import java.sql.PreparedStatement; 22 | import java.sql.SQLException; 23 | import java.util.ArrayList; 24 | import java.util.Date; 25 | import java.util.List; 26 | 27 | /** 28 | * 任务对象数据库操作对象 29 | * @author rongdi 30 | * @date 2019-03-12 19:13 31 | */ 32 | @Component 33 | public class TaskRepository { 34 | 35 | @Autowired 36 | @Qualifier("easyjobJdbcTemplate") 37 | private JdbcTemplate jdbcTemplate; 38 | 39 | @Autowired 40 | private EasyJobConfig config; 41 | /** 42 | * 序列化工具类 43 | */ 44 | private ObjectSerializer serializer = new JdkSerializationSerializer(); 45 | 46 | /** 47 | * 查询还需要指定时间才开始的主任务列表 48 | * TIMESTAMPDIFF(SECOND,NOW(),next_start_time) < ? 这里用不到索引,会有效率问题 49 | * next_start_time < date_sub(now(), interval ? second) 改成这种方式就好了 50 | * @param duration 51 | * @return 52 | */ 53 | public List listNotStartedTasks(int duration) { 54 | StringBuilder sb = new StringBuilder(); 55 | sb.append("SELECT id,node_id AS nodeId,pid,`name`,cron_expr AS cronExpr,STATUS,fail_count AS failCount,success_count AS successCount,VERSION,first_start_time AS firstStartTime,next_start_time AS nextStartTime,update_time AS updateTime,create_time AS createTime FROM easy_job_task ") 56 | .append("WHERE pid IS NULL AND next_start_time < date_sub(now(), interval ? second) AND STATUS = 0 ") 57 | .append("ORDER BY next_start_time"); 58 | Object[] args = {duration}; 59 | return jdbcTemplate.query(sb.toString(),args,new BeanPropertyRowMapper(Task.class)); 60 | } 61 | 62 | /** 63 | * 查找所有的任务名称 64 | * @return 65 | */ 66 | public List listAllTaskNames() { 67 | StringBuilder sb = new StringBuilder(); 68 | sb.append("SELECT `name` FROM easy_job_task "); 69 | Object[] args = {}; 70 | return jdbcTemplate.queryForList(sb.toString(),args,String.class); 71 | } 72 | 73 | /** 74 | * 列出指定任务的任务详情 75 | * @param taskId 任务id 76 | * @return 77 | */ 78 | public List listDetails(Long taskId) { 79 | String sql = "select id,node_id as nodeId,task_id as taskId,retry_count as retryCount,start_time as startTime,end_time as endTime,type,`status`,error_msg as errorMsg from easy_job_task_detail where task_id = ?"; 80 | Object[] args = {taskId}; 81 | return jdbcTemplate.query(sql,args,new BeanPropertyRowMapper(TaskDetail.class)); 82 | } 83 | 84 | /** 85 | * 获取指定任务的子任务明细列表 86 | * @param id 87 | * @return 88 | */ 89 | public List getDetailChilds(Long id) { 90 | String sql = "select id,pid,task_id as taskId,node_id as nodeId,retry_count as retryCount,version,status,start_time as startTime,end_time as endTime from easy_job_task_detail where pid = ?"; 91 | Object[] args = {id}; 92 | List tasks = jdbcTemplate.query(sql,args,new BeanPropertyRowMapper(TaskDetail.class)); 93 | if(tasks == null) { 94 | return null; 95 | } 96 | return tasks; 97 | } 98 | 99 | /** 100 | * 列出需要恢复的任务,需要恢复的任务是指所属执行节点已经挂了并且该任务还属于执行中的任务 101 | * timestampdiff(SECOND,n.update_time,now()) > ? 这种用不到索引 102 | * n.update_time < date_sub(now(), interval ? second) 换成这种 103 | * @param timeout 超时时间 104 | * @return 105 | */ 106 | public List listRecoverTasks(int timeout) { 107 | StringBuilder sb = new StringBuilder(); 108 | sb.append("select t.id,t.node_id AS nodeId,t.pid,t.`name`,t.cron_expr AS cronExpr,t.status,t.fail_count AS failCount,t.success_count AS successCount,t.version,t.first_start_time AS firstStartTime,t.next_start_time AS nextStartTime,t.update_time AS updateTime,t.create_time AS createTime from easy_job_task t left join easy_job_node n on t.node_id = n.id ") 109 | .append("where (t.status = 2 or t.status = 1) and n.update_time < date_sub(now(), interval ? second)"); 110 | Object[] args = {timeout}; 111 | return jdbcTemplate.query(sb.toString(),args,new BeanPropertyRowMapper(Task.class)); 112 | } 113 | 114 | /** 115 | * 根据指定id获取具体任务对象 116 | * @param id 117 | * @return 118 | */ 119 | public Task get(Long id) { 120 | String sql = "select id,node_id as nodeId,pid,`name`,cron_expr as cronExpr,status,fail_count as failCount,success_count as successCount,version,first_start_time as firstStartTime,next_start_time as nextStartTime,update_time as updateTime,create_time as createTime,invoke_info as invokeInfo from easy_job_task where id = ?"; 121 | Object[] args = {id}; 122 | Task task = (Task)jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper(Task.class),args); 123 | if(task != null) { 124 | task.setInvokor((Invocation) serializer.deserialize(task.getInvokeInfo())); 125 | } 126 | return task; 127 | } 128 | 129 | /** 130 | * 根据指定id获取具体任务明细对象 131 | * @param id 132 | * @return 133 | */ 134 | public TaskDetail getDetail(Long id) { 135 | String sql = "select id,node_id as nodeId,task_id as taskId,retry_count as retryCount,start_time as startTime,end_time as endTime,type,`status`,error_msg as errorMsg from easy_job_task_detail where id = ?"; 136 | Object[] args = {id}; 137 | return (TaskDetail)jdbcTemplate.queryForObject(sql,new BeanPropertyRowMapper(TaskDetail.class),args); 138 | } 139 | 140 | /** 141 | * 获取指定任务的子任务列表 142 | * @param id 143 | * @return 144 | */ 145 | public List getChilds(Long id) { 146 | String sql = "select id,node_id as nodeId,pid,`name`,cron_expr as cronExpr,status,fail_count as failCount,success_count as successCount,version,first_start_time as firstStartTime,next_start_time as nextStartTime,update_time as updateTime,create_time as createTime from easy_job_task where pid = ?"; 147 | Object[] args = {id}; 148 | List tasks = jdbcTemplate.query(sql,args,new BeanPropertyRowMapper(Task.class)); 149 | if(tasks == null) { 150 | return null; 151 | } 152 | for(Task task:tasks) { 153 | task.setInvokor((Invocation) serializer.deserialize(task.getInvokeInfo())); 154 | } 155 | return tasks; 156 | } 157 | 158 | /** 159 | * 查找在当前明细之后是否还有相同状态的同属一一个主任务对象的明细 160 | * @return 161 | */ 162 | public Long findNextId(Long taskId,Long id,TaskStatus status) { 163 | String sql = "SELECT id FROM `easy_job_task_detail` WHERE task_id = ? AND id > ? and status = ? LIMIT 1"; 164 | Object[] args = {taskId,id,status.getId()}; 165 | return jdbcTemplate.queryForObject(sql,args,Long.class); 166 | } 167 | 168 | /** 169 | * 插入任务 170 | * @param task 待插入任务 171 | * @return 172 | * @throws Exception 173 | */ 174 | public long insert(Task task) throws Exception { 175 | CronExpression cronExpession = new CronExpression(task.getCronExpr()); 176 | Date nextStartDate = cronExpession.getNextValidTimeAfter(new Date()); 177 | task.setFirstStartTime(nextStartDate); 178 | task.setNextStartTime(nextStartDate); 179 | String sql = "INSERT INTO easy_job_task(`name`,pid,cron_expr,status,create_time,update_time,first_start_time,next_start_time,invoke_info) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);"; 180 | KeyHolder kh = new GeneratedKeyHolder(); 181 | jdbcTemplate.update(new PreparedStatementCreator() { 182 | @Override 183 | public PreparedStatement createPreparedStatement(Connection con) 184 | throws SQLException { 185 | //设置返回的主键字段名 186 | PreparedStatement ps = con.prepareStatement(sql,new String[]{"id"}); 187 | ps.setString(1,task.getName()); 188 | ps.setObject(2,task.getPid()); 189 | ps.setString(3,task.getCronExpr()); 190 | ps.setInt(4,task.getStatus().getId()); 191 | ps.setTimestamp(5,new java.sql.Timestamp(task.getCreateTime().getTime())); 192 | ps.setTimestamp(6,new java.sql.Timestamp(task.getUpdateTime().getTime())); 193 | ps.setTimestamp(7,task.getFirstStartTime() == null?null:new java.sql.Timestamp(task.getFirstStartTime().getTime())); 194 | ps.setTimestamp(8,task.getNextStartTime() == null?null:new java.sql.Timestamp(task.getNextStartTime().getTime())); 195 | ps.setBytes(9, serializer.serialize(task.getInvokor())); 196 | return ps; 197 | } 198 | }, kh); 199 | return kh.getKey().longValue(); 200 | } 201 | 202 | /** 203 | * 插入任务详情 204 | * @param taskDetail 待插入任务详情 205 | * @return 206 | */ 207 | public long insert(TaskDetail taskDetail) { 208 | String sql = "INSERT INTO easy_job_task_detail(`task_id`,pid,start_time,status,node_id) VALUES (?, ?, ?, ?,?);"; 209 | KeyHolder kh = new GeneratedKeyHolder(); 210 | jdbcTemplate.update(new PreparedStatementCreator() { 211 | @Override 212 | public PreparedStatement createPreparedStatement(Connection con) 213 | throws SQLException { 214 | //设置返回的主键字段名 215 | PreparedStatement ps = con.prepareStatement(sql,new String[]{"id"}); 216 | ps.setLong(1,taskDetail.getTaskId()); 217 | ps.setObject(2,taskDetail.getPid()); 218 | ps.setTimestamp(3, new java.sql.Timestamp(taskDetail.getStartTime().getTime())); 219 | ps.setInt(4,taskDetail.getStatus().getId()); 220 | ps.setLong(5,taskDetail.getNodeId()); 221 | return ps; 222 | } 223 | }, kh); 224 | 225 | return kh.getKey().longValue(); 226 | } 227 | 228 | /** 229 | * 开始一个任务 230 | * @param task 待开始的任务 231 | * @return 232 | * @throws Exception 233 | */ 234 | public TaskDetail start(Task task) throws Exception { 235 | TaskDetail taskDetail = new TaskDetail(task.getId()); 236 | taskDetail.setNodeId(task.getNodeId()); 237 | long id = insert(taskDetail); 238 | taskDetail.setId(id); 239 | return taskDetail; 240 | } 241 | 242 | /** 243 | * 开始一个子任务 244 | * @param task 待开始的任务 245 | * @return 246 | * @throws Exception 247 | */ 248 | public TaskDetail startChild(Task task,TaskDetail detail) throws Exception { 249 | TaskDetail taskDetail = new TaskDetail(task.getId()); 250 | taskDetail.setNodeId(task.getNodeId()); 251 | taskDetail.setPid(detail.getId()); 252 | long id = insert(taskDetail); 253 | taskDetail.setId(id); 254 | return taskDetail; 255 | } 256 | 257 | /** 258 | * 完成任务 259 | * @param task 待开始的任务 260 | * @param detail 本次执行的具体任务详情 261 | * @throws Exception 262 | */ 263 | public void finish(Task task,TaskDetail detail) throws Exception { 264 | CronExpression cronExpession = new CronExpression(task.getCronExpr()); 265 | Date nextStartDate = cronExpession.getNextValidTimeAfter(task.getNextStartTime()); 266 | /** 267 | * 如果没有下次执行时间了,该任务就完成了,反之变成未开始 268 | */ 269 | if(nextStartDate == null) { 270 | task.setStatus(TaskStatus.FINISH); 271 | } else { 272 | task.setStatus(TaskStatus.NOT_STARTED); 273 | } 274 | /** 275 | * 增加任务成功次数 276 | */ 277 | task.setSuccessCount(task.getSuccessCount() + 1); 278 | task.setNextStartTime(nextStartDate); 279 | /** 280 | * 使用乐观锁检测是否可以更新成功,成功则更新详情 281 | */ 282 | int n = updateWithVersion(task); 283 | if(n > 0) { 284 | detail.setEndTime(new Date()); 285 | detail.setStatus(TaskStatus.FINISH); 286 | update(detail); 287 | addCounts(detail.getNodeId()); 288 | } 289 | } 290 | 291 | /** 292 | * 记录任务失败信息 293 | * @param task 待失败任务 294 | * @param detail 任务详情 295 | * @param errorMsg 出错信息 296 | * @throws Exception 297 | */ 298 | public void fail(Task task,TaskDetail detail,String errorMsg) throws Exception { 299 | if(detail == null) return; 300 | //如果没有下次执行时间了,该任务就完成了,反之变成待执行 301 | task.setStatus(TaskStatus.ERROR); 302 | task.setFailCount(task.getFailCount() + 1); 303 | int n = updateWithVersion(task); 304 | if(n > 0) { 305 | detail.setEndTime(new Date()); 306 | detail.setStatus(TaskStatus.ERROR); 307 | detail.setErrorMsg(errorMsg); 308 | update(detail); 309 | addCounts(detail.getNodeId()); 310 | } 311 | } 312 | 313 | /** 314 | * 重启服务后,重新把本节点的任务初始化为初始状态 315 | * @return 316 | * @throws Exception 317 | */ 318 | public int reInitTasks() { 319 | StringBuilder sb = new StringBuilder(); 320 | List objs = new ArrayList<>(); 321 | sb.append("update easy_job_task set status = ? "); 322 | sb.append("where node_id = ?"); 323 | objs.add(TaskStatus.NOT_STARTED.getId()); 324 | objs.add(config.getNodeId()); 325 | return jdbcTemplate.update(sb.toString(), objs.toArray()); 326 | } 327 | 328 | /** 329 | * 使用乐观锁更新任务 330 | * @param task 待更新任务 331 | * @return 332 | * @throws Exception 333 | */ 334 | public int updateWithVersion(Task task) throws Exception { 335 | StringBuilder sb = new StringBuilder(); 336 | List objs = new ArrayList<>(); 337 | sb.append("update easy_job_task set version = version + 1,next_start_time = ?"); 338 | objs.add(task.getNextStartTime()); 339 | if(task.getStatus() != null) { 340 | sb.append(",status = ? "); 341 | objs.add(task.getStatus().getId()); 342 | } 343 | if(task.getUpdateTime() != null) { 344 | sb.append(",update_time = ? "); 345 | objs.add(task.getUpdateTime()); 346 | } 347 | if(task.getSuccessCount() != null) { 348 | sb.append(",success_count = ? "); 349 | objs.add(task.getSuccessCount()); 350 | } 351 | if(task.getFailCount() != null) { 352 | sb.append(",fail_count = ? "); 353 | objs.add(task.getFailCount()); 354 | } 355 | if(task.getNodeId() != null) { 356 | sb.append(",node_id = ? "); 357 | objs.add(task.getNodeId()); 358 | } 359 | if(task.getCronExpr() != null) { 360 | sb.append(",cron_expr = ? "); 361 | objs.add(task.getCronExpr()); 362 | } 363 | sb.append("where version = ? and id = ?"); 364 | objs.add(task.getVersion()); 365 | objs.add(task.getId()); 366 | return jdbcTemplate.update(sb.toString(), objs.toArray()); 367 | } 368 | 369 | /** 370 | * 不使用乐观锁更新任务,比如拿到任务后只是临时更新一下状态 371 | * @param task 待更新任务 372 | * @return 373 | * @throws Exception 374 | */ 375 | public int update(Task task) { 376 | StringBuilder sb = new StringBuilder(); 377 | List objs = new ArrayList<>(); 378 | sb.append("update easy_job_task set next_start_time = ?"); 379 | objs.add(task.getNextStartTime()); 380 | if(task.getStatus() != null) { 381 | sb.append(",status = ? "); 382 | objs.add(task.getStatus().getId()); 383 | } 384 | if(task.getUpdateTime() != null) { 385 | sb.append(",update_time = ? "); 386 | objs.add(task.getUpdateTime()); 387 | } 388 | if(task.getSuccessCount() != null) { 389 | sb.append(",success_count = ? "); 390 | objs.add(task.getSuccessCount()); 391 | } 392 | if(task.getFailCount() != null) { 393 | sb.append(",fail_count = ? "); 394 | objs.add(task.getFailCount()); 395 | } 396 | if(task.getNodeId() != null) { 397 | sb.append(",node_id = ? "); 398 | objs.add(task.getNodeId()); 399 | } 400 | if(task.getCronExpr() != null) { 401 | sb.append(",cron_expr = ? "); 402 | objs.add(task.getCronExpr()); 403 | } 404 | sb.append("where id = ?"); 405 | objs.add(task.getId()); 406 | return jdbcTemplate.update(sb.toString(), objs.toArray()); 407 | } 408 | 409 | /** 410 | * 更新任务详情,这里没有使用乐观锁,是因为同一个任务详情同一个时间点只会在一个节点执行, 411 | * 因为需要根据乐观锁先更新了任务的状态的节点才能执行详情操作,这个方法主要给任务调度用 412 | * @param taskDetail 413 | * @return 414 | * @throws Exception 415 | */ 416 | public int update(TaskDetail taskDetail) throws Exception { 417 | 418 | StringBuilder sb = new StringBuilder(); 419 | List objs = new ArrayList<>(); 420 | sb.append("update easy_job_task_detail set status = ?"); 421 | objs.add(taskDetail.getStatus().getId()); 422 | if(taskDetail.getRetryCount() != null) { 423 | sb.append(",retry_count = ? "); 424 | objs.add(taskDetail.getRetryCount()); 425 | } 426 | if(taskDetail.getEndTime() != null) { 427 | sb.append(",end_time = ? "); 428 | objs.add(taskDetail.getEndTime()); 429 | } 430 | if(taskDetail.getStatus() != null) { 431 | sb.append(",status = ? "); 432 | objs.add(taskDetail.getStatus().getId()); 433 | } 434 | if(taskDetail.getErrorMsg() != null) { 435 | sb.append(",error_msg = ? "); 436 | objs.add(taskDetail.getErrorMsg()); 437 | } 438 | if(taskDetail.getNodeId() != null) { 439 | sb.append(",node_id = ? "); 440 | objs.add(taskDetail.getNodeId()); 441 | } 442 | sb.append("where id = ?"); 443 | objs.add(taskDetail.getId()); 444 | return jdbcTemplate.update(sb.toString(), objs.toArray()); 445 | } 446 | 447 | /** 448 | * 更新任务详情,这里使用乐观锁,是因为多个节点的恢复线程可能会产生竞争 449 | * @param taskDetail 450 | * @return 451 | * @throws Exception 452 | */ 453 | public int updateWithVersion(TaskDetail taskDetail) throws Exception { 454 | 455 | StringBuilder sb = new StringBuilder(); 456 | List objs = new ArrayList<>(); 457 | sb.append("update easy_job_task_detail set version = version + 1"); 458 | objs.add(taskDetail.getStatus().getId()); 459 | if(taskDetail.getRetryCount() != null) { 460 | sb.append(",retry_count = ? "); 461 | objs.add(taskDetail.getRetryCount()); 462 | } 463 | if(taskDetail.getEndTime() != null) { 464 | sb.append(",end_time = ? "); 465 | objs.add(taskDetail.getEndTime()); 466 | } 467 | if(taskDetail.getStatus() != null) { 468 | sb.append(",status = ? "); 469 | objs.add(taskDetail.getStatus().getId()); 470 | } 471 | if(taskDetail.getErrorMsg() != null) { 472 | sb.append(",error_msg = ? "); 473 | objs.add(taskDetail.getErrorMsg()); 474 | } 475 | if(taskDetail.getNodeId() != null) { 476 | sb.append(",node_id = ? "); 477 | objs.add(taskDetail.getNodeId()); 478 | } 479 | sb.append("where id = ? and version = ? "); 480 | objs.add(taskDetail.getId()); 481 | objs.add(taskDetail.getVersion()); 482 | return jdbcTemplate.update(sb.toString(), objs.toArray()); 483 | } 484 | 485 | /** 486 | * 给节点增加处理次数 487 | * @param nodeId 488 | * @return 489 | */ 490 | public int addCounts(Long nodeId) { 491 | StringBuilder sb = new StringBuilder(); 492 | sb.append("update easy_job_node set counts = counts + 1 ") 493 | .append("where node_id = ?"); 494 | Object objs[] = {nodeId}; 495 | return jdbcTemplate.update(sb.toString(), objs); 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/scheduler/DelayItem.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.scheduler; 2 | 3 | import java.util.concurrent.Delayed; 4 | import java.util.concurrent.TimeUnit; 5 | 6 | /** 7 | * 延时队列中的元素 8 | * @author rongdi 9 | * @date 2019-03-13 21:05 10 | */ 11 | public class DelayItem implements Delayed { 12 | 13 | private final long delay; 14 | private final long expire; 15 | private final T t; 16 | 17 | private final long now; 18 | 19 | public DelayItem(long delay, T t) { 20 | this.delay = delay; 21 | this.t = t; 22 | //到期时间 = 当前时间+延迟时间 23 | expire = System.currentTimeMillis() + delay; 24 | now = System.currentTimeMillis(); 25 | } 26 | 27 | 28 | @Override 29 | public String toString() { 30 | final StringBuilder sb = new StringBuilder("DelayedElement{"); 31 | sb.append("delay=").append(delay); 32 | sb.append(", expire=").append(expire); 33 | sb.append(", now=").append(now); 34 | sb.append('}'); 35 | return sb.toString(); 36 | } 37 | 38 | /** 39 | * 需要实现的接口,获得延迟时间 用过期时间-当前时间 40 | * @param unit 41 | * @return 42 | */ 43 | public long getDelay(TimeUnit unit) { 44 | return unit.convert(this.expire - System.currentTimeMillis() , TimeUnit.MILLISECONDS); 45 | } 46 | 47 | /** 48 | * 用于延迟队列内部比较排序 当前时间的延迟时间 - 比较对象的延迟时间 49 | * @param o 50 | * @return 51 | */ 52 | public int compareTo(Delayed o) { 53 | return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS)); 54 | } 55 | 56 | public T getItem() { 57 | return t; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/scheduler/RecoverExecutor.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.scheduler; 2 | 3 | import com.rdpaas.task.common.Node; 4 | import com.rdpaas.task.common.NotifyCmd; 5 | import com.rdpaas.task.common.Task; 6 | import com.rdpaas.task.common.TaskStatus; 7 | import com.rdpaas.task.config.EasyJobConfig; 8 | import com.rdpaas.task.handles.NotifyHandler; 9 | import com.rdpaas.task.repository.NodeRepository; 10 | import com.rdpaas.task.repository.TaskRepository; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Component; 16 | 17 | import javax.annotation.PostConstruct; 18 | import java.util.Date; 19 | import java.util.List; 20 | import java.util.concurrent.DelayQueue; 21 | import java.util.concurrent.ExecutorService; 22 | import java.util.concurrent.Executors; 23 | 24 | /** 25 | * 恢复调度器,恢复那些属于失联的节点的未完成的任务,异常也是一种任务状态的终点, 26 | * 由于不能确定异常是否只是偶发的,这里暂时不做恢复。这里可以这样恢复是因为几点 27 | * 1.子任务异常不影响主任务状态,只要主任务自己没有异常就不会是异常状态 28 | * 2.子任务未完成,主任务就算完成也是执行中的状态 29 | * 3.这里指的任务状态是task表的状态并不是task_detail的状态 30 | * @author rongdi 31 | * @date 2019-03-16 11:16 32 | */ 33 | @Component 34 | public class RecoverExecutor { 35 | 36 | private static final Logger logger = LoggerFactory.getLogger(RecoverExecutor.class); 37 | 38 | @Autowired 39 | private EasyJobConfig config; 40 | 41 | @Autowired 42 | private TaskRepository taskRepository; 43 | 44 | @Autowired 45 | private NodeRepository nodeRepository; 46 | 47 | /** 48 | * 创建节点心跳延时队列 49 | */ 50 | private DelayQueue> heartBeatQueue = new DelayQueue<>(); 51 | 52 | /** 53 | * 可以明确知道最多只会运行2个线程,直接使用系统自带工具 54 | */ 55 | private ExecutorService bossPool = Executors.newFixedThreadPool(2); 56 | 57 | @PostConstruct 58 | public void init() { 59 | /** 60 | * 如果恢复线程开关是开着,并且心跳开关也是开着 61 | */ 62 | if(config.isRecoverEnable() && config.isHeartBeatEnable()) { 63 | /** 64 | * 初始化一个节点到心跳队列,延时为0,用来注册节点 65 | */ 66 | heartBeatQueue.offer(new DelayItem<>(0,new Node(config.getNodeId()))); 67 | /** 68 | * 执行心跳线程 69 | */ 70 | bossPool.execute(new HeartBeat()); 71 | /** 72 | * 执行异常恢复线程 73 | */ 74 | bossPool.execute(new Recover()); 75 | } 76 | } 77 | 78 | class Recover implements Runnable { 79 | @Override 80 | public void run() { 81 | for (;;) { 82 | try { 83 | /** 84 | * 太累了,先睡会 85 | */ 86 | Thread.sleep(config.getRecoverSeconds() * 1000); 87 | /** 88 | * 查找需要恢复的任务,这里界定需要恢复的任务是任务还没完成,并且所属执行节点超过3个 89 | * 心跳周期没有更新心跳时间。由于这些任务由于当时执行节点没有来得及执行完就挂了,所以 90 | * 只需要把状态再改回待执行,并且下次执行时间改成当前时间,让任务再次被调度一次 91 | */ 92 | List tasks = taskRepository.listRecoverTasks(config.getHeartBeatSeconds() * 3); 93 | if(tasks == null || tasks.isEmpty()) { 94 | return; 95 | } 96 | /** 97 | * 先获取可用的节点列表 98 | */ 99 | List nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2); 100 | if(nodes == null || nodes.isEmpty()) { 101 | return; 102 | } 103 | long maxNodeId = nodes.get(nodes.size() - 1).getNodeId(); 104 | for (Task task : tasks) { 105 | /** 106 | * 每个节点有一个恢复线程,为了避免不必要的竞争,从可用节点找到一个最靠近任务所属节点的节点 107 | */ 108 | long currNodeId = chooseNodeId(nodes,maxNodeId,task.getNodeId()); 109 | long myNodeId = config.getNodeId(); 110 | /** 111 | * 如果不该当前节点处理直接跳过 112 | */ 113 | if(currNodeId != myNodeId) { 114 | continue; 115 | } 116 | /** 117 | * 直接将任务状态改成待执行,并且节点改成当前节点,有人可能会怀疑这里的安全性,可能会随着该事务的原主人节点的下一个 118 | * 正好在这时候挂了,chooseNodeId得到的下一个节点就变了,其它节点获取到的下一个节点就变了,也会进入这里。不过就算这里 119 | * 产生了竞争。如果我只是给事务换个主人。真正补偿由补偿线程完成,那边使用乐观锁去抢占事务,就会变得很安全。 120 | */ 121 | task.setStatus(TaskStatus.PENDING); 122 | task.setNextStartTime(new Date()); 123 | task.setNodeId(config.getNodeId()); 124 | taskRepository.updateWithVersion(task); 125 | } 126 | 127 | } catch (Exception e) { 128 | logger.error("Get next task failed,cause by:{}", e); 129 | } 130 | } 131 | } 132 | 133 | } 134 | 135 | class HeartBeat implements Runnable { 136 | @Override 137 | public void run() { 138 | for(;;) { 139 | try { 140 | /** 141 | * 时间到了就可以从延时队列拿出节点对象,然后更新时间和序号, 142 | * 最后再新建一个超时时间为心跳时间的节点对象放入延时队列,形成循环的心跳 143 | */ 144 | DelayItem item = heartBeatQueue.take(); 145 | if(item != null && item.getItem() != null) { 146 | Node node = item.getItem(); 147 | handHeartBeat(node); 148 | } 149 | heartBeatQueue.offer(new DelayItem<>(config.getHeartBeatSeconds() * 1000,new Node(config.getNodeId()))); 150 | } catch (Exception e) { 151 | logger.error("task heart beat error,cause by:{} ",e); 152 | } 153 | } 154 | } 155 | } 156 | 157 | /** 158 | * 处理节点心跳 159 | * @param node 160 | */ 161 | private void handHeartBeat(Node node) { 162 | if(node == null) { 163 | return; 164 | } 165 | /** 166 | * 先看看数据库是否存在这个节点 167 | * 如果不存在:先查找下一个序号,然后设置到node对象中,最后插入 168 | * 如果存在:直接根据nodeId更新当前节点的序号和时间 169 | */ 170 | Node currNode= nodeRepository.getByNodeId(node.getNodeId()); 171 | if(currNode == null) { 172 | node.setRownum(nodeRepository.getNextRownum()); 173 | nodeRepository.insert(node); 174 | } else { 175 | nodeRepository.updateHeartBeat(node.getNodeId()); 176 | NotifyCmd cmd = currNode.getNotifyCmd(); 177 | String notifyValue = currNode.getNotifyValue(); 178 | if(cmd != null && cmd != NotifyCmd.NO_NOTIFY) { 179 | /** 180 | * 借助心跳做一下通知的事情,比如及时停止正在执行的任务 181 | * 根据指令名称查找Handler 182 | */ 183 | NotifyHandler handler = NotifyHandler.chooseHandler(currNode.getNotifyCmd()); 184 | if(handler == null || StringUtils.isEmpty(notifyValue)) { 185 | return; 186 | } 187 | /** 188 | * 先重置通知再说,以免每次心跳无限执行通知下面更新逻辑 189 | */ 190 | nodeRepository.resetNotifyInfo(currNode.getNodeId(),cmd); 191 | /** 192 | * 执行操作 193 | */ 194 | handler.update(Long.valueOf(notifyValue)); 195 | } 196 | 197 | } 198 | 199 | 200 | } 201 | 202 | /** 203 | * 选择下一个节点 204 | * @param nodes 205 | * @param maxNodeId 206 | * @param nodeId 207 | * @return 208 | */ 209 | private long chooseNodeId(List nodes,long maxNodeId,long nodeId) { 210 | if(nodes.size() == 0 || nodeId >= maxNodeId) { 211 | return nodes.get(0).getNodeId(); 212 | } 213 | return nodes.stream().filter(node -> node.getNodeId() > nodeId).findFirst().get().getNodeId(); 214 | } 215 | 216 | 217 | } 218 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/scheduler/TaskExecutor.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.scheduler; 2 | 3 | import com.rdpaas.task.common.Invocation; 4 | import com.rdpaas.task.common.Node; 5 | import com.rdpaas.task.common.NotifyCmd; 6 | import com.rdpaas.task.common.Task; 7 | import com.rdpaas.task.common.TaskDetail; 8 | import com.rdpaas.task.common.TaskStatus; 9 | import com.rdpaas.task.config.EasyJobConfig; 10 | import com.rdpaas.task.repository.NodeRepository; 11 | import com.rdpaas.task.repository.TaskRepository; 12 | import com.rdpaas.task.strategy.Strategy; 13 | import com.rdpaas.task.utils.CronExpression; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.stereotype.Component; 18 | 19 | import javax.annotation.PostConstruct; 20 | import java.util.Date; 21 | import java.util.HashMap; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.concurrent.ArrayBlockingQueue; 25 | import java.util.concurrent.Callable; 26 | import java.util.concurrent.DelayQueue; 27 | import java.util.concurrent.ExecutorService; 28 | import java.util.concurrent.Executors; 29 | import java.util.concurrent.Future; 30 | import java.util.concurrent.ThreadPoolExecutor; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | /** 34 | * 任务调度器 35 | * @author rongdi 36 | * @date 2019-03-13 21:15 37 | */ 38 | @Component 39 | public class TaskExecutor { 40 | 41 | private static final Logger logger = LoggerFactory.getLogger(TaskExecutor.class); 42 | 43 | @Autowired 44 | private TaskRepository taskRepository; 45 | 46 | @Autowired 47 | private NodeRepository nodeRepository; 48 | 49 | @Autowired 50 | private EasyJobConfig config; 51 | 52 | /** 53 | * 创建任务到期延时队列 54 | */ 55 | private DelayQueue> taskQueue = new DelayQueue<>(); 56 | 57 | /** 58 | * 可以明确知道最多只会运行2个线程,直接使用系统自带工具就可以了 59 | */ 60 | private ExecutorService bossPool = Executors.newFixedThreadPool(2); 61 | 62 | /** 63 | * 正在执行的任务的Future 64 | */ 65 | private Map doingFutures = new HashMap<>(); 66 | 67 | /** 68 | * 声明工作线程池 69 | */ 70 | private ThreadPoolExecutor workerPool; 71 | 72 | /** 73 | * 获取任务的策略 74 | */ 75 | private Strategy strategy; 76 | 77 | 78 | @PostConstruct 79 | public void init() { 80 | /** 81 | * 根据配置选择一个节点获取任务的策略 82 | */ 83 | strategy = Strategy.choose(config.getNodeStrategy()); 84 | /** 85 | * 自定义线程池,初始线程数量corePoolSize,线程池等待队列大小queueSize,当初始线程都有任务,并且等待队列满后 86 | * 线程数量会自动扩充最大线程数maxSize,当新扩充的线程空闲60s后自动回收.自定义线程池是因为Executors那几个线程工具 87 | * 各有各的弊端,不适合生产使用 88 | */ 89 | workerPool = new ThreadPoolExecutor(config.getCorePoolSize(), config.getMaxPoolSize(), 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(config.getQueueSize())); 90 | /** 91 | * 执行待处理任务加载线程 92 | */ 93 | bossPool.execute(new Loader()); 94 | /** 95 | * 执行任务调度线程 96 | */ 97 | bossPool.execute(new Boss()); 98 | 99 | } 100 | 101 | class Loader implements Runnable { 102 | 103 | @Override 104 | public void run() { 105 | for(;;) { 106 | try { 107 | /** 108 | * 先休息一下 109 | */ 110 | Thread.sleep(config.getFetchPeriod()); 111 | /** 112 | * 先获取可用的节点列表 113 | */ 114 | List nodes = nodeRepository.getEnableNodes(config.getHeartBeatSeconds() * 2); 115 | if(nodes == null || nodes.isEmpty()) { 116 | continue; 117 | } 118 | /** 119 | * 查找还有指定时间(单位秒)才开始的主任务列表 120 | */ 121 | List tasks = taskRepository.listNotStartedTasks(config.getFetchDuration()); 122 | if(tasks == null || tasks.isEmpty()) { 123 | continue; 124 | } 125 | for(Task task:tasks) { 126 | 127 | boolean accept = strategy.accept(nodes, task, config.getNodeId()); 128 | /** 129 | * 不该自己拿就不要抢 130 | */ 131 | if(!accept) { 132 | continue; 133 | } 134 | /** 135 | * 先设置成待执行 136 | */ 137 | task.setStatus(TaskStatus.PENDING); 138 | task.setNodeId(config.getNodeId()); 139 | /** 140 | * 使用乐观锁尝试更新状态,如果更新成功,其他节点就不会更新成功。如果其它节点也正在查询未完成的 141 | * 任务列表和当前这段时间有节点已经更新了这个任务,version必然和查出来时候的version不一样了,这里更新 142 | * 必然会返回0了 143 | */ 144 | int n = taskRepository.updateWithVersion(task); 145 | Date nextStartTime = task.getNextStartTime(); 146 | if(n == 0 || nextStartTime == null) { 147 | continue; 148 | } 149 | 150 | /** 151 | * 如果任务的下次启动时间还在系统启动时间之前,说明时间已过期需要重新更新 152 | */ 153 | if(nextStartTime != null && nextStartTime.before(config.getSysStartTime())) { 154 | /** 155 | * 如果服务停止重新启动后由于之前的任务的nextStartTime时间还是之前的就可能存在,再次启动后仍然按照之前时间执行的情况 156 | */ 157 | CronExpression cronExpession = new CronExpression(task.getCronExpr()); 158 | Date nextStartDate = cronExpession.getNextValidTimeAfter(config.getSysStartTime()); 159 | task.setNextStartTime(nextStartDate); 160 | task.setStatus(TaskStatus.NOT_STARTED); 161 | taskRepository.update(task); 162 | continue; 163 | } 164 | 165 | /** 166 | * 封装成延时对象放入延时队列,这里再查一次是因为上面乐观锁已经更新了版本,会导致后面结束任务更新不成功 167 | */ 168 | task = taskRepository.get(task.getId()); 169 | DelayItem delayItem = new DelayItem(nextStartTime.getTime() - new Date().getTime(), task); 170 | taskQueue.offer(delayItem); 171 | 172 | } 173 | 174 | } catch(Exception e) { 175 | logger.error("fetch task list failed,cause by:{}", e); 176 | } 177 | } 178 | } 179 | 180 | } 181 | 182 | class Boss implements Runnable { 183 | @Override 184 | public void run() { 185 | for (;;) { 186 | try { 187 | /** 188 | * 时间到了就可以从延时队列拿出任务对象,然后交给worker线程池去执行 189 | */ 190 | DelayItem item = taskQueue.take(); 191 | if(item != null && item.getItem() != null) { 192 | Task task = item.getItem(); 193 | /** 194 | * 真正开始执行了设置成执行中 195 | */ 196 | task.setStatus(TaskStatus.DOING); 197 | /** 198 | * loader线程中已经使用乐观锁控制了,这里没必要了 199 | */ 200 | taskRepository.update(task); 201 | /** 202 | * 提交到线程池 203 | */ 204 | Future future = workerPool.submit(new Worker(task)); 205 | /** 206 | * 暂存在doingFutures 207 | */ 208 | doingFutures.put(task.getId(),future); 209 | } 210 | 211 | } catch (Exception e) { 212 | logger.error("fetch task failed,cause by:{}", e); 213 | } 214 | } 215 | } 216 | 217 | } 218 | 219 | class Worker implements Callable { 220 | 221 | private Task task; 222 | 223 | public Worker(Task task) { 224 | this.task = task; 225 | } 226 | 227 | @Override 228 | public String call() { 229 | logger.info("Begin to execute task:{}",task.getId()); 230 | TaskDetail detail = null; 231 | try { 232 | //开始任务 233 | detail = taskRepository.start(task); 234 | if(detail == null) return null; 235 | //执行任务 236 | task.getInvokor().invoke(); 237 | //完成任务 238 | finish(task,detail); 239 | logger.info("finished execute task:{}",task.getId()); 240 | /** 241 | * 执行完后删了 242 | */ 243 | doingFutures.remove(task.getId()); 244 | } catch (Exception e) { 245 | logger.error("execute task:{} error,cause by:{}",task.getId(), e); 246 | try { 247 | taskRepository.fail(task,detail,e.getCause().getMessage()); 248 | } catch(Exception e1) { 249 | logger.error("fail task:{} error,cause by:{}",task.getId(), e); 250 | } 251 | } 252 | return null; 253 | } 254 | 255 | } 256 | 257 | /** 258 | * 完成子任务,如果父任务失败了,子任务不会执行 259 | * @param task 260 | * @param detail 261 | * @throws Exception 262 | */ 263 | private void finish(Task task,TaskDetail detail) throws Exception { 264 | 265 | //查看是否有子类任务 266 | List childTasks = taskRepository.getChilds(task.getId()); 267 | if(childTasks == null || childTasks.isEmpty()) { 268 | //当没有子任务时完成父任务 269 | taskRepository.finish(task,detail); 270 | return; 271 | } else { 272 | for (Task childTask : childTasks) { 273 | //开始任务 274 | TaskDetail childDetail = null; 275 | try { 276 | //将子任务状态改成执行中 277 | childTask.setStatus(TaskStatus.DOING); 278 | childTask.setNodeId(config.getNodeId()); 279 | //开始子任务 280 | childDetail = taskRepository.startChild(childTask,detail); 281 | //使用乐观锁更新下状态,不然这里可能和恢复线程产生并发问题 282 | int n = taskRepository.updateWithVersion(childTask); 283 | if (n > 0) { 284 | //再从数据库取一下,避免上面update修改后version不同步 285 | childTask = taskRepository.get(childTask.getId()); 286 | //执行子任务 287 | childTask.getInvokor().invoke(); 288 | //完成子任务 289 | finish(childTask, childDetail); 290 | } 291 | } catch (Exception e) { 292 | logger.error("execute child task error,cause by:{}", e); 293 | try { 294 | taskRepository.fail(childTask, childDetail, e.getCause().getMessage()); 295 | } catch (Exception e1) { 296 | logger.error("fail child task error,cause by:{}", e); 297 | } 298 | } 299 | } 300 | /** 301 | * 当有子任务时完成子任务后再完成父任务 302 | */ 303 | taskRepository.finish(task,detail); 304 | 305 | } 306 | 307 | } 308 | 309 | /** 310 | * 添加任务 311 | * @param name 312 | * @param cronExp 313 | * @param invockor 314 | * @return 315 | * @throws Exception 316 | */ 317 | public long addTask(String name, String cronExp, Invocation invockor) throws Exception { 318 | Task task = new Task(name,cronExp,invockor); 319 | return taskRepository.insert(task); 320 | } 321 | 322 | /** 323 | * 添加子任务 324 | * @param pid 325 | * @param name 326 | * @param cronExp 327 | * @param invockor 328 | * @return 329 | * @throws Exception 330 | */ 331 | public long addChildTask(Long pid,String name, String cronExp, Invocation invockor) throws Exception { 332 | Task task = new Task(name,cronExp,invockor); 333 | task.setPid(pid); 334 | return taskRepository.insert(task); 335 | } 336 | 337 | /** 338 | * 立即执行任务,就是设置一下延时为0加入任务队列就好了,这个可以外部直接调用 339 | * @param taskId 340 | * @return 341 | */ 342 | public boolean startNow(Long taskId) { 343 | Task task = taskRepository.get(taskId); 344 | task.setStatus(TaskStatus.DOING); 345 | taskRepository.update(task); 346 | DelayItem delayItem = new DelayItem(0L, task); 347 | return taskQueue.offer(delayItem); 348 | } 349 | 350 | /** 351 | * 立即停止正在执行的任务,留给外部调用的方法 352 | * @param taskId 353 | * @return 354 | */ 355 | public boolean stopNow(Long taskId) { 356 | Task task = taskRepository.get(taskId); 357 | if(task == null) { 358 | return false; 359 | } 360 | /** 361 | * 该任务不是正在执行,直接修改task状态为已完成即可 362 | */ 363 | if(task.getStatus() != TaskStatus.DOING) { 364 | task.setStatus(TaskStatus.STOP); 365 | taskRepository.update(task); 366 | return true; 367 | } 368 | /** 369 | * 该任务正在执行,使用节点配合心跳发布停用通知 370 | */ 371 | int n = nodeRepository.updateNotifyInfo(NotifyCmd.STOP_TASK,String.valueOf(taskId)); 372 | return n > 0; 373 | } 374 | 375 | /** 376 | * 立即停止正在执行的任务,这个不需要自己调用,是给心跳线程调用 377 | * @param taskId 378 | * @return 379 | */ 380 | public boolean stop(Long taskId) { 381 | Task task = taskRepository.get(taskId); 382 | /** 383 | * 不是自己节点的任务,本节点不能执行停用 384 | */ 385 | if(task == null || !config.getNodeId().equals(task.getNodeId())) { 386 | return false; 387 | } 388 | /** 389 | * 拿到正在执行任务的future,然后强制停用,并删除doingFutures的任务 390 | */ 391 | Future future = doingFutures.get(taskId); 392 | boolean flag = future.cancel(true); 393 | if(flag) { 394 | doingFutures.remove(taskId); 395 | /** 396 | * 修改状态为已停用 397 | */ 398 | task.setStatus(TaskStatus.STOP); 399 | taskRepository.update(task); 400 | } 401 | /** 402 | * 重置通知信息,避免重复执行停用通知 403 | */ 404 | nodeRepository.resetNotifyInfo(NotifyCmd.STOP_TASK); 405 | return flag; 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/serializer/JdkSerializationSerializer.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.serializer; 2 | 3 | import java.io.*; 4 | 5 | /** 6 | * jdk序列化实现类 7 | * @author rongdi 8 | * @date 2019-03-12 19:10 9 | */ 10 | public class JdkSerializationSerializer implements ObjectSerializer { 11 | 12 | @Override 13 | public byte[] serialize(T object) { 14 | if (object == null) { 15 | return null; 16 | } else { 17 | ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); 18 | 19 | try { 20 | ObjectOutputStream ex = new ObjectOutputStream(baos); 21 | ex.writeObject(object); 22 | ex.flush(); 23 | } catch (IOException var3) { 24 | throw new IllegalArgumentException("Failed to serialize object of type: " + object.getClass(), var3); 25 | } 26 | 27 | return baos.toByteArray(); 28 | } 29 | } 30 | 31 | @Override 32 | public T deserialize(byte[] bytes) { 33 | if (bytes == null) { 34 | return null; 35 | } else { 36 | try { 37 | ObjectInputStream ex = new ObjectInputStream(new ByteArrayInputStream(bytes)); 38 | return (T) ex.readObject(); 39 | } catch (IOException var2) { 40 | throw new IllegalArgumentException("Failed to deserialize object", var2); 41 | } catch (ClassNotFoundException var3) { 42 | throw new IllegalStateException("Failed to deserialize object type", var3); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/serializer/ObjectSerializer.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.serializer; 2 | 3 | /** 4 | * jdk序列化抽象接口 5 | * @author rongdi 6 | * @date 2019-03-12 19:09 7 | */ 8 | public interface ObjectSerializer { 9 | 10 | byte[] serialize(T t); 11 | 12 | T deserialize(byte[] bytes); 13 | } 14 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/strategy/DefaultStrategy.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.strategy; 2 | 3 | import java.util.List; 4 | 5 | import com.rdpaas.task.common.Node; 6 | import com.rdpaas.task.common.Task; 7 | 8 | /** 9 | * 默认的来者不惧的策略,只要能抢到就要,其实这种方式等同于随机分配任务,因为谁可以抢到不一定 10 | * @author rongdi 11 | * @date 2019-03-16 21:34 12 | */ 13 | public class DefaultStrategy implements Strategy { 14 | 15 | @Override 16 | public boolean accept(List nodes, Task task, Long myNodeId) { 17 | return true; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/strategy/IdHashStrategy.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.strategy; 2 | 3 | import java.util.List; 4 | 5 | import com.rdpaas.task.common.Node; 6 | import com.rdpaas.task.common.Task; 7 | 8 | /** 9 | * 按照任务ID hash方式针对有效节点个数取余,然后余数+1后和各个节点的顺序号匹配, 10 | * 这种方式效果其实等同于轮询,因为任务id是自增的 11 | * @author rongdi 12 | * @date 2019-03-16 13 | */ 14 | public class IdHashStrategy implements Strategy { 15 | 16 | /** 17 | * 这里的nodes集合必然不会为空,外面调度那判断了,而且是按照nodeId的升序排列的 18 | */ 19 | @Override 20 | public boolean accept(List nodes, Task task, Long myNodeId) { 21 | int size = nodes.size(); 22 | long taskId = task.getId(); 23 | /** 24 | * 找到自己的节点 25 | */ 26 | Node myNode = nodes.stream().filter(node -> node.getNodeId() == myNodeId).findFirst().get(); 27 | return myNode == null ? false : (taskId % size) + 1 == myNode.getRownum(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/strategy/LeastCountStrategy.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.strategy; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import com.rdpaas.task.common.Node; 6 | import com.rdpaas.task.common.Task; 7 | 8 | /** 9 | * 最少处理任务次数策略,也就是每次任务来了,看看自己是不是处理任务次数最少的,是就可以消费这个任务 10 | * @author rongdi 11 | * @date 2019-03-16 21:56 12 | */ 13 | public class LeastCountStrategy implements Strategy { 14 | 15 | @Override 16 | public boolean accept(List nodes, Task task, Long myNodeId) { 17 | 18 | /** 19 | * 获取次数最少的那个节点,这里可以类比成先按counts升序排列然后取第一个元素 20 | * 然后是自己就返回true 21 | */ 22 | Optional min = nodes.stream().min((o1, o2) -> o1.getCounts().compareTo(o2.getCounts())); 23 | 24 | return min.isPresent()? min.get().getNodeId() == myNodeId : false; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/strategy/Strategy.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.strategy; 2 | 3 | import java.util.List; 4 | 5 | import com.rdpaas.task.common.Node; 6 | import com.rdpaas.task.common.Task; 7 | 8 | /** 9 | * 抽象的策略接口 10 | * @author rongdi 11 | * @date 2019-03-16 12:36 12 | */ 13 | public interface Strategy { 14 | 15 | /** 16 | * 默认策略 17 | */ 18 | String DEFAULT = "default"; 19 | 20 | /** 21 | * 按任务ID hash取余再和自己节点序号匹配 22 | */ 23 | String ID_HASH = "id_hash"; 24 | 25 | /** 26 | * 最少执行次数 27 | */ 28 | String LEAST_COUNT = "least_count"; 29 | 30 | /** 31 | * 按节点权重 32 | */ 33 | String WEIGHT = "weight"; 34 | 35 | 36 | public static Strategy choose(String key) { 37 | switch(key) { 38 | case ID_HASH: 39 | return new IdHashStrategy(); 40 | case LEAST_COUNT: 41 | return new LeastCountStrategy(); 42 | case WEIGHT: 43 | return new WeightStrategy(); 44 | default: 45 | return new DefaultStrategy(); 46 | } 47 | } 48 | 49 | public boolean accept(List nodes,Task task,Long myNodeId); 50 | 51 | } 52 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/strategy/WeightStrategy.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.strategy; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | import com.mysql.fabric.xmlrpc.base.Array; 8 | import com.rdpaas.task.common.Node; 9 | import com.rdpaas.task.common.Task; 10 | 11 | /** 12 | * 按权重的分配策略,方案如下,假如 13 | * 节点序号 1 ,2 ,3 ,4 14 | * 节点权重 2 ,3 ,3 ,2 15 | * 则取余后 0,1 | 2,3,4 | 5,6,7 | 8,9 16 | * 序号1可以消费按照权重的和取余后小于2的 17 | * 序号2可以消费按照权重的和取余后大于等于2小于2+3的 18 | * 序号3可以消费按照权重的和取余后大于等于2+3小于2+3+3的 19 | * 序号3可以消费按照权重的和取余后大于等于2+3+3小于2+3+3+2的 20 | * 总结:本节点可以消费的按照权重的和取余后大于等于前面节点的权重和小于包括自己的权重和的这个范围 21 | * 不知道有没有大神有更好的算法思路 22 | * @author rongdi 23 | * @date 2019-03-16 23:16 24 | */ 25 | public class WeightStrategy implements Strategy { 26 | 27 | @Override 28 | public boolean accept(List nodes, Task task, Long myNodeId) { 29 | Node myNode = nodes.stream().filter(node -> node.getNodeId() == myNodeId).findFirst().get(); 30 | if(myNode == null) { 31 | return false; 32 | } 33 | /** 34 | * 计算本节点序号前面的节点的权重和 35 | */ 36 | int preWeightSum = nodes.stream().filter(node -> node.getRownum() < myNode.getRownum()).collect(Collectors.summingInt(Node::getWeight)); 37 | /** 38 | * 计算全部权重的和 39 | */ 40 | int weightSum = nodes.stream().collect(Collectors.summingInt(Node::getWeight)); 41 | /** 42 | * 计算对权重和取余的余数 43 | */ 44 | int remainder = (int)(task.getId() % weightSum); 45 | return remainder >= preWeightSum && remainder < preWeightSum + myNode.getWeight(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/utils/CronExpression.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.utils; 2 | 3 | import java.io.IOException; 4 | import java.io.ObjectInputStream; 5 | import java.io.Serializable; 6 | import java.text.ParseException; 7 | import java.util.ArrayList; 8 | import java.util.Calendar; 9 | import java.util.Date; 10 | import java.util.HashMap; 11 | import java.util.Iterator; 12 | import java.util.Locale; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import java.util.SortedSet; 16 | import java.util.StringTokenizer; 17 | import java.util.TimeZone; 18 | import java.util.TreeSet; 19 | 20 | /** 21 | * cron表达式工具,这里直接是复制的哪个框架里面的代码 22 | * @date 2019-03-12 19:06 23 | */ 24 | public class CronExpression implements Serializable, Cloneable { 25 | private static final long serialVersionUID = 12423409423L; 26 | protected static final int SECOND = 0; 27 | protected static final int MINUTE = 1; 28 | protected static final int HOUR = 2; 29 | protected static final int DAY_OF_MONTH = 3; 30 | protected static final int MONTH = 4; 31 | protected static final int DAY_OF_WEEK = 5; 32 | protected static final int YEAR = 6; 33 | protected static final int ALL_SPEC_INT = 99; 34 | protected static final int NO_SPEC_INT = 98; 35 | protected static final Integer ALL_SPEC = new Integer(99); 36 | protected static final Integer NO_SPEC = new Integer(98); 37 | protected static Map monthMap = new HashMap(20); 38 | protected static Map dayMap = new HashMap(60); 39 | protected String cronExpression = null; 40 | private TimeZone timeZone = null; 41 | protected transient TreeSet seconds; 42 | protected transient TreeSet minutes; 43 | protected transient TreeSet hours; 44 | protected transient TreeSet daysOfMonth; 45 | protected transient TreeSet months; 46 | protected transient TreeSet daysOfWeek; 47 | protected transient TreeSet years; 48 | protected transient boolean lastdayOfWeek = false; 49 | protected transient int nthdayOfWeek = 0; 50 | protected transient boolean lastdayOfMonth = false; 51 | protected transient boolean nearestWeekday = false; 52 | protected transient boolean expressionParsed = false; 53 | 54 | public CronExpression(String cronExpression) throws ParseException { 55 | if(cronExpression == null) { 56 | throw new IllegalArgumentException("cronExpression cannot be null"); 57 | } else { 58 | this.cronExpression = cronExpression.toUpperCase(Locale.US); 59 | this.buildExpression(this.cronExpression); 60 | } 61 | } 62 | 63 | public boolean isSatisfiedBy(Date date) { 64 | Calendar testDateCal = Calendar.getInstance(this.getTimeZone()); 65 | testDateCal.setTime(date); 66 | testDateCal.set(14, 0); 67 | Date originalDate = testDateCal.getTime(); 68 | testDateCal.add(13, -1); 69 | Date timeAfter = this.getTimeAfter(testDateCal.getTime()); 70 | return timeAfter != null && timeAfter.equals(originalDate); 71 | } 72 | 73 | public Date getNextValidTimeAfter(Date date) { 74 | return this.getTimeAfter(date); 75 | } 76 | 77 | public Date getNextInvalidTimeAfter(Date date) { 78 | long difference = 1000L; 79 | Calendar adjustCal = Calendar.getInstance(this.getTimeZone()); 80 | adjustCal.setTime(date); 81 | adjustCal.set(14, 0); 82 | Date lastDate = adjustCal.getTime(); 83 | Date newDate = null; 84 | 85 | while(difference == 1000L) { 86 | newDate = this.getTimeAfter(lastDate); 87 | difference = newDate.getTime() - lastDate.getTime(); 88 | if(difference == 1000L) { 89 | lastDate = newDate; 90 | } 91 | } 92 | 93 | return new Date(lastDate.getTime() + 1000L); 94 | } 95 | 96 | public TimeZone getTimeZone() { 97 | if(this.timeZone == null) { 98 | this.timeZone = TimeZone.getDefault(); 99 | } 100 | 101 | return this.timeZone; 102 | } 103 | 104 | public void setTimeZone(TimeZone timeZone) { 105 | this.timeZone = timeZone; 106 | } 107 | 108 | public String toString() { 109 | return this.cronExpression; 110 | } 111 | 112 | public static boolean isValidExpression(String cronExpression) { 113 | try { 114 | new CronExpression(cronExpression); 115 | return true; 116 | } catch (ParseException var2) { 117 | return false; 118 | } 119 | } 120 | 121 | protected void buildExpression(String expression) throws ParseException { 122 | this.expressionParsed = true; 123 | 124 | try { 125 | if(this.seconds == null) { 126 | this.seconds = new TreeSet(); 127 | } 128 | 129 | if(this.minutes == null) { 130 | this.minutes = new TreeSet(); 131 | } 132 | 133 | if(this.hours == null) { 134 | this.hours = new TreeSet(); 135 | } 136 | 137 | if(this.daysOfMonth == null) { 138 | this.daysOfMonth = new TreeSet(); 139 | } 140 | 141 | if(this.months == null) { 142 | this.months = new TreeSet(); 143 | } 144 | 145 | if(this.daysOfWeek == null) { 146 | this.daysOfWeek = new TreeSet(); 147 | } 148 | 149 | if(this.years == null) { 150 | this.years = new TreeSet(); 151 | } 152 | 153 | int e = 0; 154 | 155 | for(StringTokenizer exprsTok = new StringTokenizer(expression, " \t", false); exprsTok.hasMoreTokens() && e <= 6; ++e) { 156 | String dow = exprsTok.nextToken().trim(); 157 | if(e == 3 && dow.indexOf(76) != -1 && dow.length() > 1 && dow.indexOf(",") >= 0) { 158 | throw new ParseException("Support for specifying \'L\' and \'LW\' with other days of the month is not implemented", -1); 159 | } 160 | 161 | if(e == 5 && dow.indexOf(76) != -1 && dow.length() > 1 && dow.indexOf(",") >= 0) { 162 | throw new ParseException("Support for specifying \'L\' with other days of the week is not implemented", -1); 163 | } 164 | 165 | StringTokenizer dom = new StringTokenizer(dow, ","); 166 | 167 | while(dom.hasMoreTokens()) { 168 | String dayOfMSpec = dom.nextToken(); 169 | this.storeExpressionVals(0, dayOfMSpec, e); 170 | } 171 | } 172 | 173 | if(e <= 5) { 174 | throw new ParseException("Unexpected end of expression.", expression.length()); 175 | } else { 176 | if(e <= 6) { 177 | this.storeExpressionVals(0, "*", 6); 178 | } 179 | 180 | TreeSet var10 = this.getSet(5); 181 | TreeSet var11 = this.getSet(3); 182 | boolean var12 = !var11.contains(NO_SPEC); 183 | boolean dayOfWSpec = !var10.contains(NO_SPEC); 184 | if((!var12 || dayOfWSpec) && (!dayOfWSpec || var12)) { 185 | throw new ParseException("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.", 0); 186 | } 187 | } 188 | } catch (ParseException var8) { 189 | throw var8; 190 | } catch (Exception var9) { 191 | throw new ParseException("Illegal cron expression format (" + var9.toString() + ")", 0); 192 | } 193 | } 194 | 195 | protected int storeExpressionVals(int pos, String s, int type) throws ParseException { 196 | byte incr = 0; 197 | int i = this.skipWhiteSpace(pos, s); 198 | if(i >= s.length()) { 199 | return i; 200 | } else { 201 | char c = s.charAt(i); 202 | if(c >= 65 && c <= 90 && !s.equals("L") && !s.equals("LW")) { 203 | String var13 = s.substring(i, i + 3); 204 | boolean var14 = true; 205 | int eval = -1; 206 | int var15; 207 | if(type == 4) { 208 | var15 = this.getMonthNumber(var13) + 1; 209 | if(var15 <= 0) { 210 | throw new ParseException("Invalid Month value: \'" + var13 + "\'", i); 211 | } 212 | 213 | if(s.length() > i + 3) { 214 | c = s.charAt(i + 3); 215 | if(c == 45) { 216 | i += 4; 217 | var13 = s.substring(i, i + 3); 218 | eval = this.getMonthNumber(var13) + 1; 219 | if(eval <= 0) { 220 | throw new ParseException("Invalid Month value: \'" + var13 + "\'", i); 221 | } 222 | } 223 | } 224 | } else { 225 | if(type != 5) { 226 | throw new ParseException("Illegal characters for this position: \'" + var13 + "\'", i); 227 | } 228 | 229 | var15 = this.getDayOfWeekNumber(var13); 230 | if(var15 < 0) { 231 | throw new ParseException("Invalid Day-of-Week value: \'" + var13 + "\'", i); 232 | } 233 | 234 | if(s.length() > i + 3) { 235 | c = s.charAt(i + 3); 236 | if(c == 45) { 237 | i += 4; 238 | var13 = s.substring(i, i + 3); 239 | eval = this.getDayOfWeekNumber(var13); 240 | if(eval < 0) { 241 | throw new ParseException("Invalid Day-of-Week value: \'" + var13 + "\'", i); 242 | } 243 | } else if(c == 35) { 244 | try { 245 | i += 4; 246 | this.nthdayOfWeek = Integer.parseInt(s.substring(i)); 247 | if(this.nthdayOfWeek < 1 || this.nthdayOfWeek > 5) { 248 | throw new Exception(); 249 | } 250 | } catch (Exception var11) { 251 | throw new ParseException("A numeric value between 1 and 5 must follow the \'#\' option", i); 252 | } 253 | } else if(c == 76) { 254 | this.lastdayOfWeek = true; 255 | ++i; 256 | } 257 | } 258 | } 259 | 260 | if(eval != -1) { 261 | incr = 1; 262 | } 263 | 264 | this.addToSet(var15, eval, incr, type); 265 | return i + 3; 266 | } else { 267 | int val; 268 | if(c == 63) { 269 | ++i; 270 | if(i + 1 < s.length() && s.charAt(i) != 32 && s.charAt(i + 1) != 9) { 271 | throw new ParseException("Illegal character after \'?\': " + s.charAt(i), i); 272 | } else if(type != 5 && type != 3) { 273 | throw new ParseException("\'?\' can only be specfied for Day-of-Month or Day-of-Week.", i); 274 | } else { 275 | if(type == 5 && !this.lastdayOfMonth) { 276 | val = ((Integer)this.daysOfMonth.last()).intValue(); 277 | if(val == 98) { 278 | throw new ParseException("\'?\' can only be specfied for Day-of-Month -OR- Day-of-Week.", i); 279 | } 280 | } 281 | 282 | this.addToSet(98, -1, 0, type); 283 | return i; 284 | } 285 | } else if(c != 42 && c != 47) { 286 | if(c == 76) { 287 | ++i; 288 | if(type == 3) { 289 | this.lastdayOfMonth = true; 290 | } 291 | 292 | if(type == 5) { 293 | this.addToSet(7, 7, 0, type); 294 | } 295 | 296 | if(type == 3 && s.length() > i) { 297 | c = s.charAt(i); 298 | if(c == 87) { 299 | this.nearestWeekday = true; 300 | ++i; 301 | } 302 | } 303 | 304 | return i; 305 | } else if(c >= 48 && c <= 57) { 306 | val = Integer.parseInt(String.valueOf(c)); 307 | ++i; 308 | if(i >= s.length()) { 309 | this.addToSet(val, -1, -1, type); 310 | return i; 311 | } else { 312 | c = s.charAt(i); 313 | if(c >= 48 && c <= 57) { 314 | ValueSet vs = this.getValue(val, s, i); 315 | val = vs.value; 316 | i = vs.pos; 317 | } 318 | 319 | i = this.checkNext(i, s, val, type); 320 | return i; 321 | } 322 | } else { 323 | throw new ParseException("Unexpected character: " + c, i); 324 | } 325 | } else if(c == 42 && i + 1 >= s.length()) { 326 | this.addToSet(99, -1, incr, type); 327 | return i + 1; 328 | } else if(c != 47 || i + 1 < s.length() && s.charAt(i + 1) != 32 && s.charAt(i + 1) != 9) { 329 | if(c == 42) { 330 | ++i; 331 | } 332 | 333 | c = s.charAt(i); 334 | int var12; 335 | if(c != 47) { 336 | var12 = 1; 337 | } else { 338 | ++i; 339 | if(i >= s.length()) { 340 | throw new ParseException("Unexpected end of string.", i); 341 | } 342 | 343 | var12 = this.getNumericValue(s, i); 344 | ++i; 345 | if(var12 > 10) { 346 | ++i; 347 | } 348 | 349 | if(var12 > 59 && (type == 0 || type == 1)) { 350 | throw new ParseException("Increment > 60 : " + var12, i); 351 | } 352 | 353 | if(var12 > 23 && type == 2) { 354 | throw new ParseException("Increment > 24 : " + var12, i); 355 | } 356 | 357 | if(var12 > 31 && type == 3) { 358 | throw new ParseException("Increment > 31 : " + var12, i); 359 | } 360 | 361 | if(var12 > 7 && type == 5) { 362 | throw new ParseException("Increment > 7 : " + var12, i); 363 | } 364 | 365 | if(var12 > 12 && type == 4) { 366 | throw new ParseException("Increment > 12 : " + var12, i); 367 | } 368 | } 369 | 370 | this.addToSet(99, -1, var12, type); 371 | return i; 372 | } else { 373 | throw new ParseException("\'/\' must be followed by an integer.", i); 374 | } 375 | } 376 | } 377 | } 378 | 379 | protected int checkNext(int pos, String s, int val, int type) throws ParseException { 380 | byte end = -1; 381 | if(pos >= s.length()) { 382 | this.addToSet(val, end, -1, type); 383 | return pos; 384 | } else { 385 | char c = s.charAt(pos); 386 | int i; 387 | TreeSet v2; 388 | if(c == 76) { 389 | if(type == 5) { 390 | this.lastdayOfWeek = true; 391 | v2 = this.getSet(type); 392 | v2.add(new Integer(val)); 393 | i = pos + 1; 394 | return i; 395 | } else { 396 | throw new ParseException("\'L\' option is not valid here. (pos=" + pos + ")", pos); 397 | } 398 | } else if(c == 87) { 399 | if(type == 3) { 400 | this.nearestWeekday = true; 401 | v2 = this.getSet(type); 402 | v2.add(new Integer(val)); 403 | i = pos + 1; 404 | return i; 405 | } else { 406 | throw new ParseException("\'W\' option is not valid here. (pos=" + pos + ")", pos); 407 | } 408 | } else if(c != 35) { 409 | ValueSet vs; 410 | int v3; 411 | int var14; 412 | if(c == 45) { 413 | i = pos + 1; 414 | c = s.charAt(i); 415 | var14 = Integer.parseInt(String.valueOf(c)); 416 | int var13 = var14; 417 | ++i; 418 | if(i >= s.length()) { 419 | this.addToSet(val, var14, 1, type); 420 | return i; 421 | } else { 422 | c = s.charAt(i); 423 | if(c >= 48 && c <= 57) { 424 | vs = this.getValue(var14, s, i); 425 | v3 = vs.value; 426 | var13 = v3; 427 | i = vs.pos; 428 | } 429 | 430 | if(i < s.length() && s.charAt(i) == 47) { 431 | ++i; 432 | c = s.charAt(i); 433 | int var15 = Integer.parseInt(String.valueOf(c)); 434 | ++i; 435 | if(i >= s.length()) { 436 | this.addToSet(val, var13, var15, type); 437 | return i; 438 | } else { 439 | c = s.charAt(i); 440 | if(c >= 48 && c <= 57) { 441 | ValueSet var16 = this.getValue(var15, s, i); 442 | int v31 = var16.value; 443 | this.addToSet(val, var13, v31, type); 444 | i = var16.pos; 445 | return i; 446 | } else { 447 | this.addToSet(val, var13, var15, type); 448 | return i; 449 | } 450 | } 451 | } else { 452 | this.addToSet(val, var13, 1, type); 453 | return i; 454 | } 455 | } 456 | } else if(c == 47) { 457 | i = pos + 1; 458 | c = s.charAt(i); 459 | var14 = Integer.parseInt(String.valueOf(c)); 460 | ++i; 461 | if(i >= s.length()) { 462 | this.addToSet(val, end, var14, type); 463 | return i; 464 | } else { 465 | c = s.charAt(i); 466 | if(c >= 48 && c <= 57) { 467 | vs = this.getValue(var14, s, i); 468 | v3 = vs.value; 469 | this.addToSet(val, end, v3, type); 470 | i = vs.pos; 471 | return i; 472 | } else { 473 | throw new ParseException("Unexpected character \'" + c + "\' after \'/\'", i); 474 | } 475 | } 476 | } else { 477 | this.addToSet(val, end, 0, type); 478 | i = pos + 1; 479 | return i; 480 | } 481 | } else if(type != 5) { 482 | throw new ParseException("\'#\' option is not valid here. (pos=" + pos + ")", pos); 483 | } else { 484 | i = pos + 1; 485 | 486 | try { 487 | this.nthdayOfWeek = Integer.parseInt(s.substring(i)); 488 | if(this.nthdayOfWeek < 1 || this.nthdayOfWeek > 5) { 489 | throw new Exception(); 490 | } 491 | } catch (Exception var12) { 492 | throw new ParseException("A numeric value between 1 and 5 must follow the \'#\' option", i); 493 | } 494 | 495 | v2 = this.getSet(type); 496 | v2.add(new Integer(val)); 497 | ++i; 498 | return i; 499 | } 500 | } 501 | } 502 | 503 | public String getCronExpression() { 504 | return this.cronExpression; 505 | } 506 | 507 | public String getExpressionSummary() { 508 | StringBuffer buf = new StringBuffer(); 509 | buf.append("seconds: "); 510 | buf.append(this.getExpressionSetSummary((Set)this.seconds)); 511 | buf.append("\n"); 512 | buf.append("minutes: "); 513 | buf.append(this.getExpressionSetSummary((Set)this.minutes)); 514 | buf.append("\n"); 515 | buf.append("hours: "); 516 | buf.append(this.getExpressionSetSummary((Set)this.hours)); 517 | buf.append("\n"); 518 | buf.append("daysOfMonth: "); 519 | buf.append(this.getExpressionSetSummary((Set)this.daysOfMonth)); 520 | buf.append("\n"); 521 | buf.append("months: "); 522 | buf.append(this.getExpressionSetSummary((Set)this.months)); 523 | buf.append("\n"); 524 | buf.append("daysOfWeek: "); 525 | buf.append(this.getExpressionSetSummary((Set)this.daysOfWeek)); 526 | buf.append("\n"); 527 | buf.append("lastdayOfWeek: "); 528 | buf.append(this.lastdayOfWeek); 529 | buf.append("\n"); 530 | buf.append("nearestWeekday: "); 531 | buf.append(this.nearestWeekday); 532 | buf.append("\n"); 533 | buf.append("NthDayOfWeek: "); 534 | buf.append(this.nthdayOfWeek); 535 | buf.append("\n"); 536 | buf.append("lastdayOfMonth: "); 537 | buf.append(this.lastdayOfMonth); 538 | buf.append("\n"); 539 | buf.append("years: "); 540 | buf.append(this.getExpressionSetSummary((Set)this.years)); 541 | buf.append("\n"); 542 | return buf.toString(); 543 | } 544 | 545 | protected String getExpressionSetSummary(Set set) { 546 | if(set.contains(NO_SPEC)) { 547 | return "?"; 548 | } else if(set.contains(ALL_SPEC)) { 549 | return "*"; 550 | } else { 551 | StringBuffer buf = new StringBuffer(); 552 | Iterator itr = set.iterator(); 553 | 554 | for(boolean first = true; itr.hasNext(); first = false) { 555 | Integer iVal = (Integer)itr.next(); 556 | String val = iVal.toString(); 557 | if(!first) { 558 | buf.append(","); 559 | } 560 | 561 | buf.append(val); 562 | } 563 | 564 | return buf.toString(); 565 | } 566 | } 567 | 568 | protected String getExpressionSetSummary(ArrayList list) { 569 | if(list.contains(NO_SPEC)) { 570 | return "?"; 571 | } else if(list.contains(ALL_SPEC)) { 572 | return "*"; 573 | } else { 574 | StringBuffer buf = new StringBuffer(); 575 | Iterator itr = list.iterator(); 576 | 577 | for(boolean first = true; itr.hasNext(); first = false) { 578 | Integer iVal = (Integer)itr.next(); 579 | String val = iVal.toString(); 580 | if(!first) { 581 | buf.append(","); 582 | } 583 | 584 | buf.append(val); 585 | } 586 | 587 | return buf.toString(); 588 | } 589 | } 590 | 591 | protected int skipWhiteSpace(int i, String s) { 592 | while(i < s.length() && (s.charAt(i) == 32 || s.charAt(i) == 9)) { 593 | ++i; 594 | } 595 | 596 | return i; 597 | } 598 | 599 | protected int findNextWhiteSpace(int i, String s) { 600 | while(i < s.length() && (s.charAt(i) != 32 || s.charAt(i) != 9)) { 601 | ++i; 602 | } 603 | 604 | return i; 605 | } 606 | 607 | protected void addToSet(int val, int end, int incr, int type) throws ParseException { 608 | TreeSet set = this.getSet(type); 609 | if(type != 0 && type != 1) { 610 | if(type == 2) { 611 | if((val < 0 || val > 23 || end > 23) && val != 99) { 612 | throw new ParseException("Hour values must be between 0 and 23", -1); 613 | } 614 | } else if(type == 3) { 615 | if((val < 1 || val > 31 || end > 31) && val != 99 && val != 98) { 616 | throw new ParseException("Day of month values must be between 1 and 31", -1); 617 | } 618 | } else if(type == 4) { 619 | if((val < 1 || val > 12 || end > 12) && val != 99) { 620 | throw new ParseException("Month values must be between 1 and 12", -1); 621 | } 622 | } else if(type == 5 && (val == 0 || val > 7 || end > 7) && val != 99 && val != 98) { 623 | throw new ParseException("Day-of-Week values must be between 1 and 7", -1); 624 | } 625 | } else if((val < 0 || val > 59 || end > 59) && val != 99) { 626 | throw new ParseException("Minute and Second values must be between 0 and 59", -1); 627 | } 628 | 629 | if((incr == 0 || incr == -1) && val != 99) { 630 | if(val != -1) { 631 | set.add(new Integer(val)); 632 | } else { 633 | set.add(NO_SPEC); 634 | } 635 | 636 | } else { 637 | int startAt = val; 638 | int stopAt = end; 639 | if(val == 99 && incr <= 0) { 640 | incr = 1; 641 | set.add(ALL_SPEC); 642 | } 643 | 644 | if(type != 0 && type != 1) { 645 | if(type == 2) { 646 | if(end == -1) { 647 | stopAt = 23; 648 | } 649 | 650 | if(val == -1 || val == 99) { 651 | startAt = 0; 652 | } 653 | } else if(type == 3) { 654 | if(end == -1) { 655 | stopAt = 31; 656 | } 657 | 658 | if(val == -1 || val == 99) { 659 | startAt = 1; 660 | } 661 | } else if(type == 4) { 662 | if(end == -1) { 663 | stopAt = 12; 664 | } 665 | 666 | if(val == -1 || val == 99) { 667 | startAt = 1; 668 | } 669 | } else if(type == 5) { 670 | if(end == -1) { 671 | stopAt = 7; 672 | } 673 | 674 | if(val == -1 || val == 99) { 675 | startAt = 1; 676 | } 677 | } else if(type == 6) { 678 | if(end == -1) { 679 | stopAt = 2099; 680 | } 681 | 682 | if(val == -1 || val == 99) { 683 | startAt = 1970; 684 | } 685 | } 686 | } else { 687 | if(end == -1) { 688 | stopAt = 59; 689 | } 690 | 691 | if(val == -1 || val == 99) { 692 | startAt = 0; 693 | } 694 | } 695 | 696 | byte max = -1; 697 | if(stopAt < startAt) { 698 | switch(type) { 699 | case 0: 700 | max = 60; 701 | break; 702 | case 1: 703 | max = 60; 704 | break; 705 | case 2: 706 | max = 24; 707 | break; 708 | case 3: 709 | max = 31; 710 | break; 711 | case 4: 712 | max = 12; 713 | break; 714 | case 5: 715 | max = 7; 716 | break; 717 | case 6: 718 | throw new IllegalArgumentException("Start year must be less than stop year"); 719 | default: 720 | throw new IllegalArgumentException("Unexpected type encountered"); 721 | } 722 | 723 | stopAt += max; 724 | } 725 | 726 | for(int i = startAt; i <= stopAt; i += incr) { 727 | if(max == -1) { 728 | set.add(new Integer(i)); 729 | } else { 730 | int i2 = i % max; 731 | if(i2 == 0 && (type == 4 || type == 5 || type == 3)) { 732 | i2 = max; 733 | } 734 | 735 | set.add(new Integer(i2)); 736 | } 737 | } 738 | 739 | } 740 | } 741 | 742 | protected TreeSet getSet(int type) { 743 | switch(type) { 744 | case 0: 745 | return this.seconds; 746 | case 1: 747 | return this.minutes; 748 | case 2: 749 | return this.hours; 750 | case 3: 751 | return this.daysOfMonth; 752 | case 4: 753 | return this.months; 754 | case 5: 755 | return this.daysOfWeek; 756 | case 6: 757 | return this.years; 758 | default: 759 | return null; 760 | } 761 | } 762 | 763 | protected ValueSet getValue(int v, String s, int i) { 764 | char c = s.charAt(i); 765 | 766 | String s1; 767 | for(s1 = String.valueOf(v); c >= 48 && c <= 57; c = s.charAt(i)) { 768 | s1 = s1 + c; 769 | ++i; 770 | if(i >= s.length()) { 771 | break; 772 | } 773 | } 774 | 775 | ValueSet val = new ValueSet(); 776 | val.pos = i < s.length()?i:i + 1; 777 | val.value = Integer.parseInt(s1); 778 | return val; 779 | } 780 | 781 | protected int getNumericValue(String s, int i) { 782 | int endOfVal = this.findNextWhiteSpace(i, s); 783 | String val = s.substring(i, endOfVal); 784 | return Integer.parseInt(val); 785 | } 786 | 787 | protected int getMonthNumber(String s) { 788 | Integer integer = (Integer)monthMap.get(s); 789 | return integer == null?-1:integer.intValue(); 790 | } 791 | 792 | protected int getDayOfWeekNumber(String s) { 793 | Integer integer = (Integer)dayMap.get(s); 794 | return integer == null?-1:integer.intValue(); 795 | } 796 | 797 | protected Date getTimeAfter(Date afterTime) { 798 | Calendar cl = Calendar.getInstance(this.getTimeZone()); 799 | afterTime = new Date(afterTime.getTime() + 1000L); 800 | cl.setTime(afterTime); 801 | cl.set(14, 0); 802 | boolean gotOne = false; 803 | 804 | while(true) { 805 | while(true) { 806 | while(!gotOne) { 807 | if(cl.get(1) > 2999) { 808 | return null; 809 | } 810 | 811 | SortedSet st = null; 812 | boolean t = false; 813 | int sec = cl.get(13); 814 | int min = cl.get(12); 815 | st = this.seconds.tailSet(new Integer(sec)); 816 | if(st != null && st.size() != 0) { 817 | sec = ((Integer)st.first()).intValue(); 818 | } else { 819 | sec = ((Integer)this.seconds.first()).intValue(); 820 | ++min; 821 | cl.set(12, min); 822 | } 823 | 824 | cl.set(13, sec); 825 | min = cl.get(12); 826 | int hr = cl.get(11); 827 | int var19 = -1; 828 | st = this.minutes.tailSet(new Integer(min)); 829 | if(st != null && st.size() != 0) { 830 | var19 = min; 831 | min = ((Integer)st.first()).intValue(); 832 | } else { 833 | min = ((Integer)this.minutes.first()).intValue(); 834 | ++hr; 835 | } 836 | 837 | if(min == var19) { 838 | cl.set(12, min); 839 | hr = cl.get(11); 840 | int day = cl.get(5); 841 | var19 = -1; 842 | st = this.hours.tailSet(new Integer(hr)); 843 | if(st != null && st.size() != 0) { 844 | var19 = hr; 845 | hr = ((Integer)st.first()).intValue(); 846 | } else { 847 | hr = ((Integer)this.hours.first()).intValue(); 848 | ++day; 849 | } 850 | 851 | if(hr == var19) { 852 | cl.set(11, hr); 853 | day = cl.get(5); 854 | int mon = cl.get(2) + 1; 855 | var19 = -1; 856 | int tmon = mon; 857 | boolean dayOfMSpec = !this.daysOfMonth.contains(NO_SPEC); 858 | boolean dayOfWSpec = !this.daysOfWeek.contains(NO_SPEC); 859 | int year; 860 | int dow; 861 | int daysToAdd; 862 | if(dayOfMSpec && !dayOfWSpec) { 863 | st = this.daysOfMonth.tailSet(new Integer(day)); 864 | Calendar var20; 865 | Date var22; 866 | if(this.lastdayOfMonth) { 867 | if(!this.nearestWeekday) { 868 | var19 = day; 869 | day = this.getLastDayOfMonth(mon, cl.get(1)); 870 | } else { 871 | var19 = day; 872 | day = this.getLastDayOfMonth(mon, cl.get(1)); 873 | var20 = Calendar.getInstance(this.getTimeZone()); 874 | var20.set(13, 0); 875 | var20.set(12, 0); 876 | var20.set(11, 0); 877 | var20.set(5, day); 878 | var20.set(2, mon - 1); 879 | var20.set(1, cl.get(1)); 880 | dow = this.getLastDayOfMonth(mon, cl.get(1)); 881 | daysToAdd = var20.get(7); 882 | if(daysToAdd == 7 && day == 1) { 883 | day += 2; 884 | } else if(daysToAdd == 7) { 885 | --day; 886 | } else if(daysToAdd == 1 && day == dow) { 887 | day -= 2; 888 | } else if(daysToAdd == 1) { 889 | ++day; 890 | } 891 | 892 | var20.set(13, sec); 893 | var20.set(12, min); 894 | var20.set(11, hr); 895 | var20.set(5, day); 896 | var20.set(2, mon - 1); 897 | var22 = var20.getTime(); 898 | if(var22.before(afterTime)) { 899 | day = 1; 900 | ++mon; 901 | } 902 | } 903 | } else if(this.nearestWeekday) { 904 | var19 = day; 905 | day = ((Integer)this.daysOfMonth.first()).intValue(); 906 | var20 = Calendar.getInstance(this.getTimeZone()); 907 | var20.set(13, 0); 908 | var20.set(12, 0); 909 | var20.set(11, 0); 910 | var20.set(5, day); 911 | var20.set(2, mon - 1); 912 | var20.set(1, cl.get(1)); 913 | dow = this.getLastDayOfMonth(mon, cl.get(1)); 914 | daysToAdd = var20.get(7); 915 | if(daysToAdd == 7 && day == 1) { 916 | day += 2; 917 | } else if(daysToAdd == 7) { 918 | --day; 919 | } else if(daysToAdd == 1 && day == dow) { 920 | day -= 2; 921 | } else if(daysToAdd == 1) { 922 | ++day; 923 | } 924 | 925 | var20.set(13, sec); 926 | var20.set(12, min); 927 | var20.set(11, hr); 928 | var20.set(5, day); 929 | var20.set(2, mon - 1); 930 | var22 = var20.getTime(); 931 | if(var22.before(afterTime)) { 932 | day = ((Integer)this.daysOfMonth.first()).intValue(); 933 | ++mon; 934 | } 935 | } else if(st != null && st.size() != 0) { 936 | var19 = day; 937 | day = ((Integer)st.first()).intValue(); 938 | year = this.getLastDayOfMonth(mon, cl.get(1)); 939 | if(day > year) { 940 | day = ((Integer)this.daysOfMonth.first()).intValue(); 941 | ++mon; 942 | } 943 | } else { 944 | day = ((Integer)this.daysOfMonth.first()).intValue(); 945 | ++mon; 946 | } 947 | 948 | if(day != var19 || mon != tmon) { 949 | cl.set(13, 0); 950 | cl.set(12, 0); 951 | cl.set(11, 0); 952 | cl.set(5, day); 953 | cl.set(2, mon - 1); 954 | continue; 955 | } 956 | } else { 957 | if(!dayOfWSpec || dayOfMSpec) { 958 | throw new UnsupportedOperationException("Support for specifying both a day-of-week AND a day-of-month parameter is not implemented."); 959 | } 960 | 961 | int lDay; 962 | if(this.lastdayOfWeek) { 963 | year = ((Integer)this.daysOfWeek.first()).intValue(); 964 | dow = cl.get(7); 965 | daysToAdd = 0; 966 | if(dow < year) { 967 | daysToAdd = year - dow; 968 | } 969 | 970 | if(dow > year) { 971 | daysToAdd = year + (7 - dow); 972 | } 973 | 974 | lDay = this.getLastDayOfMonth(mon, cl.get(1)); 975 | if(day + daysToAdd > lDay) { 976 | cl.set(13, 0); 977 | cl.set(12, 0); 978 | cl.set(11, 0); 979 | cl.set(5, 1); 980 | cl.set(2, mon); 981 | continue; 982 | } 983 | 984 | while(day + daysToAdd + 7 <= lDay) { 985 | daysToAdd += 7; 986 | } 987 | 988 | day += daysToAdd; 989 | if(daysToAdd > 0) { 990 | cl.set(13, 0); 991 | cl.set(12, 0); 992 | cl.set(11, 0); 993 | cl.set(5, day); 994 | cl.set(2, mon - 1); 995 | continue; 996 | } 997 | } else if(this.nthdayOfWeek != 0) { 998 | year = ((Integer)this.daysOfWeek.first()).intValue(); 999 | dow = cl.get(7); 1000 | daysToAdd = 0; 1001 | if(dow < year) { 1002 | daysToAdd = year - dow; 1003 | } else if(dow > year) { 1004 | daysToAdd = year + (7 - dow); 1005 | } 1006 | 1007 | boolean var21 = false; 1008 | if(daysToAdd > 0) { 1009 | var21 = true; 1010 | } 1011 | 1012 | day += daysToAdd; 1013 | int weekOfMonth = day / 7; 1014 | if(day % 7 > 0) { 1015 | ++weekOfMonth; 1016 | } 1017 | 1018 | daysToAdd = (this.nthdayOfWeek - weekOfMonth) * 7; 1019 | day += daysToAdd; 1020 | if(daysToAdd < 0 || day > this.getLastDayOfMonth(mon, cl.get(1))) { 1021 | cl.set(13, 0); 1022 | cl.set(12, 0); 1023 | cl.set(11, 0); 1024 | cl.set(5, 1); 1025 | cl.set(2, mon); 1026 | continue; 1027 | } 1028 | 1029 | if(daysToAdd > 0 || var21) { 1030 | cl.set(13, 0); 1031 | cl.set(12, 0); 1032 | cl.set(11, 0); 1033 | cl.set(5, day); 1034 | cl.set(2, mon - 1); 1035 | continue; 1036 | } 1037 | } else { 1038 | year = cl.get(7); 1039 | dow = ((Integer)this.daysOfWeek.first()).intValue(); 1040 | st = this.daysOfWeek.tailSet(new Integer(year)); 1041 | if(st != null && st.size() > 0) { 1042 | dow = ((Integer)st.first()).intValue(); 1043 | } 1044 | 1045 | daysToAdd = 0; 1046 | if(year < dow) { 1047 | daysToAdd = dow - year; 1048 | } 1049 | 1050 | if(year > dow) { 1051 | daysToAdd = dow + (7 - year); 1052 | } 1053 | 1054 | lDay = this.getLastDayOfMonth(mon, cl.get(1)); 1055 | if(day + daysToAdd > lDay) { 1056 | cl.set(13, 0); 1057 | cl.set(12, 0); 1058 | cl.set(11, 0); 1059 | cl.set(5, 1); 1060 | cl.set(2, mon); 1061 | continue; 1062 | } 1063 | 1064 | if(daysToAdd > 0) { 1065 | cl.set(13, 0); 1066 | cl.set(12, 0); 1067 | cl.set(11, 0); 1068 | cl.set(5, day + daysToAdd); 1069 | cl.set(2, mon - 1); 1070 | continue; 1071 | } 1072 | } 1073 | } 1074 | 1075 | cl.set(5, day); 1076 | mon = cl.get(2) + 1; 1077 | year = cl.get(1); 1078 | var19 = -1; 1079 | if(year > 2099) { 1080 | return null; 1081 | } 1082 | 1083 | st = this.months.tailSet(new Integer(mon)); 1084 | if(st != null && st.size() != 0) { 1085 | var19 = mon; 1086 | mon = ((Integer)st.first()).intValue(); 1087 | } else { 1088 | mon = ((Integer)this.months.first()).intValue(); 1089 | ++year; 1090 | } 1091 | 1092 | if(mon != var19) { 1093 | cl.set(13, 0); 1094 | cl.set(12, 0); 1095 | cl.set(11, 0); 1096 | cl.set(5, 1); 1097 | cl.set(2, mon - 1); 1098 | cl.set(1, year); 1099 | } else { 1100 | cl.set(2, mon - 1); 1101 | year = cl.get(1); 1102 | t = true; 1103 | st = this.years.tailSet(new Integer(year)); 1104 | if(st == null || st.size() == 0) { 1105 | return null; 1106 | } 1107 | 1108 | var19 = year; 1109 | year = ((Integer)st.first()).intValue(); 1110 | if(year != var19) { 1111 | cl.set(13, 0); 1112 | cl.set(12, 0); 1113 | cl.set(11, 0); 1114 | cl.set(5, 1); 1115 | cl.set(2, 0); 1116 | cl.set(1, year); 1117 | } else { 1118 | cl.set(1, year); 1119 | gotOne = true; 1120 | } 1121 | } 1122 | } else { 1123 | cl.set(13, 0); 1124 | cl.set(12, 0); 1125 | cl.set(5, day); 1126 | this.setCalendarHour(cl, hr); 1127 | } 1128 | } else { 1129 | cl.set(13, 0); 1130 | cl.set(12, min); 1131 | this.setCalendarHour(cl, hr); 1132 | } 1133 | } 1134 | 1135 | return cl.getTime(); 1136 | } 1137 | } 1138 | } 1139 | 1140 | protected void setCalendarHour(Calendar cal, int hour) { 1141 | cal.set(11, hour); 1142 | if(cal.get(11) != hour && hour != 24) { 1143 | cal.set(11, hour + 1); 1144 | } 1145 | 1146 | } 1147 | 1148 | protected Date getTimeBefore(Date endTime) { 1149 | return null; 1150 | } 1151 | 1152 | public Date getFinalFireTime() { 1153 | return null; 1154 | } 1155 | 1156 | protected boolean isLeapYear(int year) { 1157 | return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; 1158 | } 1159 | 1160 | protected int getLastDayOfMonth(int monthNum, int year) { 1161 | switch(monthNum) { 1162 | case 1: 1163 | return 31; 1164 | case 2: 1165 | return this.isLeapYear(year)?29:28; 1166 | case 3: 1167 | return 31; 1168 | case 4: 1169 | return 30; 1170 | case 5: 1171 | return 31; 1172 | case 6: 1173 | return 30; 1174 | case 7: 1175 | return 31; 1176 | case 8: 1177 | return 31; 1178 | case 9: 1179 | return 30; 1180 | case 10: 1181 | return 31; 1182 | case 11: 1183 | return 30; 1184 | case 12: 1185 | return 31; 1186 | default: 1187 | throw new IllegalArgumentException("Illegal month number: " + monthNum); 1188 | } 1189 | } 1190 | 1191 | private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { 1192 | stream.defaultReadObject(); 1193 | 1194 | try { 1195 | this.buildExpression(this.cronExpression); 1196 | } catch (Exception var3) { 1197 | ; 1198 | } 1199 | 1200 | } 1201 | 1202 | public Object clone() { 1203 | CronExpression copy = null; 1204 | 1205 | try { 1206 | copy = new CronExpression(this.getCronExpression()); 1207 | copy.setTimeZone(this.getTimeZone()); 1208 | return copy; 1209 | } catch (ParseException var3) { 1210 | throw new IncompatibleClassChangeError("Not Cloneable."); 1211 | } 1212 | } 1213 | 1214 | static { 1215 | monthMap.put("JAN", new Integer(0)); 1216 | monthMap.put("FEB", new Integer(1)); 1217 | monthMap.put("MAR", new Integer(2)); 1218 | monthMap.put("APR", new Integer(3)); 1219 | monthMap.put("MAY", new Integer(4)); 1220 | monthMap.put("JUN", new Integer(5)); 1221 | monthMap.put("JUL", new Integer(6)); 1222 | monthMap.put("AUG", new Integer(7)); 1223 | monthMap.put("SEP", new Integer(8)); 1224 | monthMap.put("OCT", new Integer(9)); 1225 | monthMap.put("NOV", new Integer(10)); 1226 | monthMap.put("DEC", new Integer(11)); 1227 | dayMap.put("SUN", new Integer(1)); 1228 | dayMap.put("MON", new Integer(2)); 1229 | dayMap.put("TUE", new Integer(3)); 1230 | dayMap.put("WED", new Integer(4)); 1231 | dayMap.put("THU", new Integer(5)); 1232 | dayMap.put("FRI", new Integer(6)); 1233 | dayMap.put("SAT", new Integer(7)); 1234 | } 1235 | 1236 | static class ValueSet { 1237 | public int value; 1238 | public int pos; 1239 | 1240 | ValueSet() { 1241 | } 1242 | } 1243 | } -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/utils/Delimiters.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.utils; 2 | 3 | /** 4 | * 分隔符 5 | * @author rongdi 6 | * @date 2019-03-15 20:09 7 | */ 8 | public class Delimiters { 9 | 10 | public final static String COMMA = ","; 11 | 12 | public final static String DOT = "."; 13 | 14 | public final static String LINE = "-"; 15 | 16 | public final static String COLON = ":"; 17 | 18 | public final static String BLANK = "\\s+"; 19 | 20 | public final static String COMMENT = "//"; 21 | 22 | public final static String NEW_LINE = "\n"; 23 | 24 | public final static String SLASH = "/"; 25 | 26 | public final static String ANGLE_BRACKETS = "<.*?>"; 27 | 28 | public final static String LEFT_BRACKET = "("; 29 | 30 | public final static String RIGHT_BRACKET = ")"; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /easy-job-core/src/main/java/com/rdpaas/task/utils/SpringContextUtil.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.utils; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Map; 9 | 10 | /** 11 | * spring上下文工具 12 | * @author rongdi 13 | * @date 2019-03-12 19:05 14 | */ 15 | @Component 16 | public class SpringContextUtil implements ApplicationContextAware { 17 | 18 | private static ApplicationContext context; 19 | 20 | @Override 21 | public void setApplicationContext(ApplicationContext contex) 22 | throws BeansException { 23 | SpringContextUtil.context = contex; 24 | } 25 | 26 | public static ApplicationContext getApplicationContext() { 27 | return context; 28 | } 29 | 30 | public static Object getBean(String beanName) { 31 | return context.getBean(beanName); 32 | } 33 | 34 | public static Object getBean(Class clazz) { 35 | return context.getBean(clazz); 36 | } 37 | 38 | public static T getByTypeAndName(Class clazz,String name) { 39 | Map clazzMap = context.getBeansOfType(clazz); 40 | return clazzMap.get(name); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /easy-job-sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.rdpaas 7 | easy-job-sample 8 | 0.0.2-SNAPSHOT 9 | jar 10 | 11 | easy-job-sample 12 | easy-job-sample 13 | 14 | 15 | com.rdpaas 16 | easy-job-parent 17 | 0.0.2-SNAPSHOT 18 | 19 | 20 | 21 | 22 | 23 | ${project.groupId} 24 | easy-job-core 25 | ${project.version} 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-jdbc 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | src/main/java 39 | 40 | **/*.xml 41 | 42 | 43 | 44 | src/main/resources 45 | 46 | **/* 47 | 48 | 49 | 50 | 51 | 52 | ${project.basedir}/src/test/java 53 | 54 | 55 | ${project.basedir}/src/test/resources 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-maven-plugin 65 | 66 | true 67 | 68 | 69 | 70 | 71 | org.apache.maven.plugins 72 | maven-compiler-plugin 73 | 74 | ${java.version} 75 | ${java.version} 76 | ${project.build.sourceEncoding} 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-javadoc-plugin 83 | 3.0.0 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /easy-job-sample/src/main/java/com/rdpaas/Application.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas; 2 | 3 | import org.springframework.beans.factory.annotation.Qualifier; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.jdbc.core.JdbcTemplate; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | import javax.sql.DataSource; 11 | 12 | 13 | /** 14 | * springboot启动类 15 | * @author rongdi 16 | * @date 2019-03-17 16:04 17 | */ 18 | @EnableScheduling 19 | @SpringBootApplication 20 | public class Application { 21 | public static void main(String[] args) { 22 | SpringApplication.run(Application.class, args); 23 | } 24 | 25 | @Bean(name = "easyjobJdbcTemplate") 26 | public JdbcTemplate taskJdbcTemplate( 27 | @Qualifier("easyjobDataSource") DataSource dataSource) { 28 | return new JdbcTemplate(dataSource); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /easy-job-sample/src/main/java/com/rdpaas/task/sample/SchedulerTest.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task.sample; 2 | 3 | import com.rdpaas.task.annotation.Scheduled; 4 | import org.springframework.stereotype.Component; 5 | import java.text.SimpleDateFormat; 6 | import java.util.Date; 7 | 8 | /** 9 | * 测试调度功能 10 | * @author rongdi 11 | * @date 2019-03-17 16:54 12 | */ 13 | @Component 14 | public class SchedulerTest { 15 | 16 | @Scheduled(cron = "0/10 * * * * ?") 17 | public void test1() throws InterruptedException { 18 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 19 | Thread.sleep(2000); 20 | System.out.println("当前时间1:"+sdf.format(new Date())); 21 | } 22 | 23 | @Scheduled(cron = "0/20 * * * * ?",parent = "test1") 24 | public void test2() throws InterruptedException { 25 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 26 | Thread.sleep(2000); 27 | System.out.println("当前时间2:"+sdf.format(new Date())); 28 | } 29 | 30 | @Scheduled(cron = "0/10 * * * * ?",parent = "test2") 31 | public void test3() throws InterruptedException { 32 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 33 | Thread.sleep(2000); 34 | System.out.println("当前时间3:"+sdf.format(new Date())); 35 | } 36 | 37 | @Scheduled(cron = "0/10 * * * * ?",parent = "test3") 38 | public void test4() throws InterruptedException { 39 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 40 | Thread.sleep(2000); 41 | System.out.println("当前时间4:"+sdf.format(new Date())); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /easy-job-sample/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | easyjob: 2 | datasource: 3 | name: test 4 | url: jdbc:mysql://localhost:3306/easy_job?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true 5 | username: root 6 | password: 123456 7 | driver-class-name: com.mysql.jdbc.Driver 8 | node: 9 | id: 1 #节点id,不能重复 10 | strategy: default #节点取任务策略,default,id_hash,least_count,weight,除了默认策略其它依赖于心跳开关 11 | fetchPeriod: 100 #节点取任务周期,单位毫秒 12 | fetchDuration: 300 #节点每次取还有多久执行的任务,单位秒 13 | pool: 14 | queueSize: 1000 #节点执行任务线程池的队列容量 15 | coreSize: 5 #节点执行任务线程池初始线程数 16 | maxSize: 10 #节点执行任务线程池最大线程数 17 | heartBeat: 18 | enable: true #是否开启心跳,只有开启心跳上面除了默认策略的其他策略有效,下面的异常恢复线程才有效 19 | seconds: 10 #节点心跳周期,单位秒,每个多少秒向数据库上报一下自己还活着 20 | recover: 21 | enable: true #节点异常状态恢复线程是否启用,依赖于心跳开关 22 | seconds: 30 #节点异常状态恢复线程每隔多少秒去处理失联节点的未完成任务,依赖于心跳开关 23 | -------------------------------------------------------------------------------- /easy-job-sample/src/main/resources/task_scheduling.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE easy_job; 2 | 3 | CREATE TABLE `easy_job_node` ( 4 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 5 | `node_id` bigint(20) NOT NULL COMMENT '节点ID,必须唯一', 6 | `row_num` bigint(20) NOT NULL DEFAULT '0' COMMENT '节点序号', 7 | `counts` bigint(255) NOT NULL DEFAULT '0' COMMENT '执行次数', 8 | `weight` int(11) NOT NULL DEFAULT '1' COMMENT '权重', 9 | `status` int(11) NOT NULL DEFAULT '1' COMMENT '节点状态,1表示可用,0表示不可用', 10 | `create_time` datetime NOT NULL COMMENT '创建时间', 11 | `update_time` datetime NOT NULL COMMENT '更新时间,用于心跳更新', 12 | PRIMARY KEY (`id`), 13 | UNIQUE KEY `idx_job_node_id` (`node_id`) USING BTREE 14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 15 | 16 | CREATE TABLE `easy_job_task` ( 17 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 18 | `pid` bigint(20) DEFAULT NULL COMMENT '任务父id,用于实现依赖任务,限制性父任务再执行子任务', 19 | `name` varchar(255) DEFAULT NULL COMMENT '调度名称', 20 | `cron_expr` varchar(255) DEFAULT NULL COMMENT 'cron表达式', 21 | `status` int(255) NOT NULL DEFAULT '0' COMMENT '状态,0表示未开始,1表示待执行,2表示执行中,3表示异常中,4表示已完成,5表示已停止', 22 | `fail_count` int(255) NOT NULL DEFAULT '0' COMMENT '失败执行次数', 23 | `success_count` int(255) NOT NULL DEFAULT '0' COMMENT '成功执行次数', 24 | `invoke_info` varbinary(10000) DEFAULT NULL COMMENT '序列化的执行类方法信息', 25 | `version` int(255) NOT NULL DEFAULT '0' COMMENT '乐观锁标识', 26 | `node_id` bigint(20) DEFAULT NULL COMMENT '当前执行节点id', 27 | `first_start_time` datetime DEFAULT NULL COMMENT '首次开始执行时间', 28 | `next_start_time` datetime DEFAULT NULL COMMENT '下次开始执行时间', 29 | `update_time` datetime DEFAULT NULL COMMENT '更新时间', 30 | `create_time` datetime DEFAULT NULL COMMENT '创建时间', 31 | PRIMARY KEY (`id`), 32 | KEY `idx_tsk_next_stime` (`next_start_time`) USING BTREE 33 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 34 | 35 | CREATE TABLE `easy_job_task_detail` ( 36 | `id` bigint(20) NOT NULL AUTO_INCREMENT, 37 | `pid` bigint(20) DEFAULT NULL COMMENT '所属明细父ID', 38 | `task_id` bigint(20) NOT NULL COMMENT '所属任务ID', 39 | `node_id` bigint(20) NOT NULL COMMENT '执行节点id', 40 | `retry_count` int(8) NOT NULL DEFAULT '0' COMMENT '重试次数', 41 | `version` int(8) DEFAULT NULL COMMENT '乐观锁标识', 42 | `start_time` datetime DEFAULT NULL COMMENT '开始时间', 43 | `end_time` datetime DEFAULT NULL COMMENT '结束时间', 44 | `status` int(8) NOT NULL DEFAULT '0' COMMENT '状态,0表示未开始,1表示待执行,2表示执行中,3表示异常中,4表示已完成,5表示已停止', 45 | `error_msg` varchar(2000) DEFAULT NULL COMMENT '失败原因', 46 | PRIMARY KEY (`id`), 47 | KEY `idx_tskd_task_id` (`task_id`) USING BTREE 48 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 49 | 50 | ALTER TABLE `easy_job`.`easy_job_node` 51 | ADD COLUMN `notify_cmd` int(8) DEFAULT 0 COMMENT '通知指令' AFTER `status`, 52 | ADD COLUMN `notify_value` varchar(255) NULL COMMENT '通知值,一般记录id啥的' AFTER `notify_cmd`; -------------------------------------------------------------------------------- /easy-job-sample/src/test/java/com/rdpaas/task/TaskTest.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task; 2 | 3 | import com.rdpaas.Application; 4 | import com.rdpaas.task.common.Invocation; 5 | import com.rdpaas.task.scheduler.TaskExecutor; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | 12 | @RunWith(SpringRunner.class) 13 | @SpringBootTest(classes = Application.class) 14 | public class TaskTest { 15 | 16 | @Autowired 17 | private TaskExecutor executor; 18 | 19 | @Test 20 | public void testTask() throws Exception { 21 | executor.addTask("test1","0/10 * 9-19 * * ?",new Invocation(com.rdpaas.task.Test.class,"test1",new Class[]{},new Object[]{})); 22 | Thread.sleep(1000); 23 | executor.addTask("test2","0/20 * 9-19 * * ?",new Invocation(com.rdpaas.task.Test.class,"test2",new Class[]{},new Object[]{})); 24 | Thread.sleep(1000); 25 | executor.addChildTask(1L,"test3","0/10 * 9-19 * * ?",new Invocation(com.rdpaas.task.Test.class,"test3",new Class[]{},new Object[]{})); 26 | Thread.sleep(1000); 27 | executor.addChildTask(3L,"test4","0/10 * 9-19 * * ?",new Invocation(com.rdpaas.task.Test.class,"test4",new Class[]{},new Object[]{})); 28 | System.in.read(); 29 | } 30 | 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /easy-job-sample/src/test/java/com/rdpaas/task/Test.java: -------------------------------------------------------------------------------- 1 | package com.rdpaas.task; 2 | 3 | import com.rdpaas.task.annotation.Scheduled; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.text.SimpleDateFormat; 7 | import java.util.Date; 8 | 9 | @Component 10 | public class Test { 11 | 12 | @Scheduled(cron = "11") 13 | public void test1() throws InterruptedException { 14 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 15 | Thread.sleep(1000); 16 | System.out.println("当前时间1:"+sdf.format(new Date())); 17 | } 18 | 19 | public void test2() throws InterruptedException { 20 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 21 | Thread.sleep(1000); 22 | System.out.println("当前时间2:"+sdf.format(new Date())); 23 | } 24 | 25 | public void test3() throws InterruptedException { 26 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 27 | Thread.sleep(1000); 28 | System.out.println("当前时间3:"+sdf.format(new Date())); 29 | } 30 | 31 | public void test4() throws InterruptedException { 32 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 33 | Thread.sleep(1000); 34 | System.out.println("当前时间4:"+sdf.format(new Date())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.rdpaas 7 | easy-job-parent 8 | 0.0.2-SNAPSHOT 9 | pom 10 | 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 1.5.10.RELEASE 16 | 17 | 18 | 19 | 20 | easy-job-core 21 | easy-job-sample 22 | 23 | 24 | 25 | UTF-8 26 | UTF-8 27 | 1.8 28 | 1.1.9 29 | 3.4 30 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-jetty 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-test 44 | test 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-web 51 | 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-aop 57 | 58 | 59 | 60 | 61 | mysql 62 | mysql-connector-java 63 | 64 | 65 | 66 | 67 | com.alibaba 68 | druid-spring-boot-starter 69 | ${druid.version} 70 | 71 | 72 | 73 | org.apache.commons 74 | commons-lang3 75 | ${commons-lang3.version} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | src/main/java 84 | 85 | **/*.xml 86 | 87 | 88 | 89 | src/main/resources 90 | 91 | **/* 92 | 93 | 94 | 95 | 96 | 97 | ${project.basedir}/src/test/java 98 | 99 | 100 | ${project.basedir}/src/test/resources 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-compiler-plugin 109 | 3.1 110 | 111 | 1.8 112 | 1.8 113 | ${project.build.sourceEncoding} 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | --------------------------------------------------------------------------------