commit 87c937a385424b5c4fc8a2b3c04b4248b7b7c065 Author: sikadi <13315935+sikadi_love@user.noreply.gitee.com> Date: Sun Nov 19 20:37:07 2023 +0800 初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b4f1fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +Dockerfile###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### JRebel ### +rebel.xml +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d1c90ad --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM anolis-registry.cn-zhangjiakou.cr.aliyuncs.com/openanolis/openjdk:17-8.6 + + +# 暴露端口号 +EXPOSE 9203/tcp + + +# 挂载目录位置 +VOLUME /home/logs/fate-job + +#构造 复制外部文件到docker 内部 +COPY target/fate-modules-job.jar /home/app.jar + +# 工作目录 exec -it 进来就是默认这个目录1gitn1 +WORKDIR /home + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > .etc.timezone + +# 启动java程序 +CMD ["java","-Dfile.encoding=UTF-8","-jar","/home/app.jar"] diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a897c06 --- /dev/null +++ b/pom.xml @@ -0,0 +1,101 @@ + + + + com.fate + fate-modules + 3.6.3 + + 4.0.0 + + fate-modules-job + + + fate-modules-job定时任务 + + 3.6.3 + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + io.springfox + springfox-swagger-ui + ${swagger.fox.version} + + + + + org.quartz-scheduler + quartz + + + com.mchange + c3p0 + + + + + + + com.mysql + mysql-connector-j + 8.0.33 + + + + + com.fate + fate-common-log + + + + + com.fate + fate-common-swagger + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + diff --git a/src/main/java/com/fate/job/FateJobApplication.java b/src/main/java/com/fate/job/FateJobApplication.java new file mode 100644 index 0000000..3ee72e4 --- /dev/null +++ b/src/main/java/com/fate/job/FateJobApplication.java @@ -0,0 +1,22 @@ +package com.fate.job; + +import com.fate.common.security.annotation.EnableCustomConfig; +import com.fate.common.security.annotation.EnableMyFeignClients; +import com.fate.common.swagger.annotation.EnableCustomSwagger2; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 定时任务 + * + * @author fate + */ +@EnableCustomConfig +@EnableCustomSwagger2 +@EnableMyFeignClients +@SpringBootApplication +public class FateJobApplication { + public static void main (String[] args) { + SpringApplication.run(FateJobApplication.class, args); + } +} diff --git a/src/main/java/com/fate/job/config/ScheduleConfig.java b/src/main/java/com/fate/job/config/ScheduleConfig.java new file mode 100644 index 0000000..1c69b91 --- /dev/null +++ b/src/main/java/com/fate/job/config/ScheduleConfig.java @@ -0,0 +1,57 @@ +//package com.fate.job.config; +// +//import java.util.Properties; +//import javax.sql.DataSource; +//import org.springframework.context.annotation.Bean; +//import org.springframework.context.annotation.Configuration; +//import org.springframework.scheduling.quartz.SchedulerFactoryBean; +// +///** +// * 定时任务配置(单机部署建议删除此类和qrtz数据库表,默认走内存会最高效) +// * +// * @author fate +// */ +//@Configuration +//public class ScheduleConfig +//{ +// @Bean +// public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) +// { +// SchedulerFactoryBean factory = new SchedulerFactoryBean(); +// factory.setDataSource(dataSource); +// +// // quartz参数 +// Properties prop = new Properties(); +// prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); +// prop.put("org.quartz.scheduler.instanceId", "AUTO"); +// // 线程池配置 +// prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); +// prop.put("org.quartz.threadPool.threadCount", "20"); +// prop.put("org.quartz.threadPool.threadPriority", "5"); +// // JobStore配置 +// prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore"); +// // 集群配置 +// prop.put("org.quartz.jobStore.isClustered", "true"); +// prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); +// prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); +// prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); +// +// // sqlserver 启用 +// // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); +// prop.put("org.quartz.jobStore.misfireThreshold", "12000"); +// prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); +// factory.setQuartzProperties(prop); +// +// factory.setSchedulerName("RuoyiScheduler"); +// // 延时启动 +// factory.setStartupDelay(1); +// factory.setApplicationContextSchedulerContextKey("applicationContextKey"); +// // 可选,QuartzScheduler +// // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 +// factory.setOverwriteExistingJobs(true); +// // 设置自动启动,默认为true +// factory.setAutoStartup(true); +// +// return factory; +// } +//} diff --git a/src/main/java/com/fate/job/controller/SysJobController.java b/src/main/java/com/fate/job/controller/SysJobController.java new file mode 100644 index 0000000..b7e6e60 --- /dev/null +++ b/src/main/java/com/fate/job/controller/SysJobController.java @@ -0,0 +1,149 @@ +package com.fate.job.controller; + +import com.fate.common.core.constant.Constants; +import com.fate.common.core.exception.job.TaskException; +import com.fate.common.core.utils.StringUtils; +import com.fate.common.core.utils.poi.ExcelUtil; +import com.fate.common.core.web.controller.BaseController; +import com.fate.common.core.domain.Result; +import com.fate.common.core.web.page.TableDataInfo; +import com.fate.common.log.annotation.Log; +import com.fate.common.log.enums.BusinessType; +import com.fate.common.security.annotation.RequiresPermissions; +import com.fate.common.security.utils.SecurityUtils; +import com.fate.job.domain.SysJob; +import com.fate.job.service.ISysJobService; +import com.fate.job.util.CronUtils; +import com.fate.job.util.ScheduleUtils; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 调度任务信息操作处理 + * + * @author fate + */ +@RestController +@RequestMapping("/job") +public class SysJobController extends BaseController { + @Autowired + private ISysJobService jobService; + + /** + * 查询定时任务列表 + */ + @RequiresPermissions("monitor:job:list") + @GetMapping("/list") + public Result> list (SysJob sysJob) { + startPage(); + List list = jobService.selectJobList(sysJob); + return getDataTable(list); + } + + /** + * 导出定时任务列表 + */ + @RequiresPermissions("monitor:job:export") + @Log(title = "定时任务", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export (HttpServletResponse response, SysJob sysJob) { + List list = jobService.selectJobList(sysJob); + ExcelUtil util = new ExcelUtil(SysJob.class); + util.exportExcel(response, list, "定时任务"); + } + + /** + * 获取定时任务详细信息 + */ + @RequiresPermissions("monitor:job:query") + @GetMapping(value = "/{jobId}") + public Result getInfo (@PathVariable("jobId") Long jobId) { + return success(jobService.selectJobById(jobId)); + } + + /** + * 新增定时任务 + */ + @RequiresPermissions("monitor:job:add") + @Log(title = "定时任务", businessType = BusinessType.INSERT) + @PostMapping + public Result add (@RequestBody SysJob job) throws SchedulerException, TaskException { + if (!CronUtils.isValid(job.getCronExpression())) { + return error("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) { + return error("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setCreateBy(SecurityUtils.getUsername()); + return toAjax(jobService.insertJob(job)); + } + + /** + * 修改定时任务 + */ + @RequiresPermissions("monitor:job:edit") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping + public Result edit (@RequestBody SysJob job) throws SchedulerException, TaskException { + if (!CronUtils.isValid(job.getCronExpression())) { + return error("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确"); + } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), Constants.LOOKUP_RMI)) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.LOOKUP_LDAP, Constants.LOOKUP_LDAPS})) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{Constants.HTTP, Constants.HTTPS})) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用"); + } else if (StringUtils.containsAnyIgnoreCase(job.getInvokeTarget(), Constants.JOB_ERROR_STR)) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规"); + } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) { + return error("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内"); + } + job.setUpdateBy(SecurityUtils.getUsername()); + return toAjax(jobService.updateJob(job)); + } + + /** + * 定时任务状态修改 + */ + @RequiresPermissions("monitor:job:changeStatus") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/changeStatus") + public Result changeStatus (@RequestBody SysJob job) throws SchedulerException { + SysJob newJob = jobService.selectJobById(job.getJobId()); + newJob.setStatus(job.getStatus()); + return toAjax(jobService.changeStatus(newJob)); + } + + /** + * 定时任务立即执行一次 + */ + @RequiresPermissions("monitor:job:changeStatus") + @Log(title = "定时任务", businessType = BusinessType.UPDATE) + @PutMapping("/run") + public Result run (@RequestBody SysJob job) throws SchedulerException { + boolean result = jobService.run(job); + return result ? success() : error("任务不存在或已过期!"); + } + + /** + * 删除定时任务 + */ + @RequiresPermissions("monitor:job:remove") + @Log(title = "定时任务", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobIds}") + public Result remove (@PathVariable Long[] jobIds) throws SchedulerException, TaskException { + jobService.deleteJobByIds(jobIds); + return success(); + } +} diff --git a/src/main/java/com/fate/job/controller/SysJobLogController.java b/src/main/java/com/fate/job/controller/SysJobLogController.java new file mode 100644 index 0000000..25ece4c --- /dev/null +++ b/src/main/java/com/fate/job/controller/SysJobLogController.java @@ -0,0 +1,81 @@ +package com.fate.job.controller; + +import com.fate.common.core.utils.poi.ExcelUtil; +import com.fate.common.core.web.controller.BaseController; +import com.fate.common.core.domain.Result; +import com.fate.common.core.web.page.TableDataInfo; +import com.fate.common.log.annotation.Log; +import com.fate.common.log.enums.BusinessType; +import com.fate.common.security.annotation.RequiresPermissions; +import com.fate.job.domain.SysJobLog; +import com.fate.job.service.ISysJobLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * 调度日志操作处理 + * + * @author fate + */ +@RestController +@RequestMapping("/job/log") +public class SysJobLogController extends BaseController { + @Autowired + private ISysJobLogService jobLogService; + + /** + * 查询定时任务调度日志列表 + */ + @RequiresPermissions("monitor:job:list") + @GetMapping("/list") + public Result> list (SysJobLog sysJobLog) { + startPage(); + List list = jobLogService.selectJobLogList(sysJobLog); + return getDataTable(list); + } + + /** + * 导出定时任务调度日志列表 + */ + @RequiresPermissions("monitor:job:export") + @Log(title = "任务调度日志", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export (HttpServletResponse response, SysJobLog sysJobLog) { + List list = jobLogService.selectJobLogList(sysJobLog); + ExcelUtil util = new ExcelUtil(SysJobLog.class); + util.exportExcel(response, list, "调度日志"); + } + + /** + * 根据调度编号获取详细信息 + */ + @RequiresPermissions("monitor:job:query") + @GetMapping(value = "/{jobLogId}") + public Result getInfo (@PathVariable Long jobLogId) { + return success(jobLogService.selectJobLogById(jobLogId)); + } + + /** + * 删除定时任务调度日志 + */ + @RequiresPermissions("monitor:job:remove") + @Log(title = "定时任务调度日志", businessType = BusinessType.DELETE) + @DeleteMapping("/{jobLogIds}") + public Result remove (@PathVariable Long[] jobLogIds) { + return toAjax(jobLogService.deleteJobLogByIds(jobLogIds)); + } + + /** + * 清空定时任务调度日志 + */ + @RequiresPermissions("monitor:job:remove") + @Log(title = "调度日志", businessType = BusinessType.CLEAN) + @DeleteMapping("/clean") + public Result clean () { + jobLogService.cleanJobLog(); + return success(); + } +} diff --git a/src/main/java/com/fate/job/domain/SysJob.java b/src/main/java/com/fate/job/domain/SysJob.java new file mode 100644 index 0000000..a1dea36 --- /dev/null +++ b/src/main/java/com/fate/job/domain/SysJob.java @@ -0,0 +1,179 @@ +package com.fate.job.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fate.common.core.annotation.Excel; +import com.fate.common.core.annotation.Excel.ColumnType; +import com.fate.common.core.constant.ScheduleConstants; +import com.fate.common.core.utils.StringUtils; +import com.fate.common.core.web.domain.BaseEntity; +import com.fate.job.util.CronUtils; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.Date; + +/** + * 定时任务调度表 sys_job + * + * @author fate + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SysJob extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * 任务ID + */ + @Excel(name = "任务序号", cellType = ColumnType.NUMERIC) + private Long jobId; + + /** + * 任务名称 + */ + @Excel(name = "任务名称") + private String jobName; + + /** + * 任务组名 + */ + @Excel(name = "任务组名") + private String jobGroup; + + /** + * 调用目标字符串 + */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** + * cron执行表达式 + */ + @Excel(name = "执行表达式 ") + private String cronExpression; + + /** + * cron计划策略 + */ + @Excel(name = "计划策略 ", readConverterExp = "0=默认,1=立即触发执行,2=触发一次执行,3=不触发立即执行") + private String misfirePolicy = ScheduleConstants.MISFIRE_DEFAULT; + + /** + * 是否并发执行(0允许 1禁止) + */ + @Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止") + private String concurrent; + + /** + * 任务状态(0正常 1暂停) + */ + @Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停") + private String status; + + public Long getJobId () { + return jobId; + } + + public void setJobId (Long jobId) { + this.jobId = jobId; + } + + @NotBlank(message = "任务名称不能为空") + @Size(min = 0, max = 64, message = "任务名称不能超过64个字符") + public String getJobName () { + return jobName; + } + + public void setJobName (String jobName) { + this.jobName = jobName; + } + + public String getJobGroup () { + return jobGroup; + } + + public void setJobGroup (String jobGroup) { + this.jobGroup = jobGroup; + } + + @NotBlank(message = "调用目标字符串不能为空") + @Size(min = 0, max = 500, message = "调用目标字符串长度不能超过500个字符") + public String getInvokeTarget () { + return invokeTarget; + } + + public void setInvokeTarget (String invokeTarget) { + this.invokeTarget = invokeTarget; + } + + @NotBlank(message = "Cron执行表达式不能为空") + @Size(min = 0, max = 255, message = "Cron执行表达式不能超过255个字符") + public String getCronExpression () { + return cronExpression; + } + + public void setCronExpression (String cronExpression) { + this.cronExpression = cronExpression; + } + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + public Date getNextValidTime () { + if (StringUtils.isNotEmpty(cronExpression)) { + return CronUtils.getNextExecution(cronExpression); + } + return null; + } + + public String getMisfirePolicy () { + return misfirePolicy; + } + + public void setMisfirePolicy (String misfirePolicy) { + this.misfirePolicy = misfirePolicy; + } + + public String getConcurrent () { + return concurrent; + } + + public void setConcurrent (String concurrent) { + this.concurrent = concurrent; + } + + public String getStatus () { + return status; + } + + public void setStatus (String status) { + this.status = status; + } + + @Override + public String toString () { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("jobId", getJobId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("cronExpression", getCronExpression()) + .append("nextValidTime", getNextValidTime()) + .append("misfirePolicy", getMisfirePolicy()) + .append("concurrent", getConcurrent()) + .append("status", getStatus()) + .append("createBy", getCreateBy()) + .append("createTime", getCreateTime()) + .append("updateBy", getUpdateBy()) + .append("updateTime", getUpdateTime()) + .append("remark", getRemark()) + .toString(); + } +} diff --git a/src/main/java/com/fate/job/domain/SysJobLog.java b/src/main/java/com/fate/job/domain/SysJobLog.java new file mode 100644 index 0000000..8a87e93 --- /dev/null +++ b/src/main/java/com/fate/job/domain/SysJobLog.java @@ -0,0 +1,165 @@ +package com.fate.job.domain; + +import com.fate.common.core.annotation.Excel; +import com.fate.common.core.web.domain.BaseEntity; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Date; + +/** + * 定时任务调度日志表 sys_job_log + * + * @author fate + */ +@Data +@SuperBuilder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class SysJobLog extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** + * ID + */ + @Excel(name = "日志序号") + private Long jobLogId; + + /** + * 任务名称 + */ + @Excel(name = "任务名称") + private String jobName; + + /** + * 任务组名 + */ + @Excel(name = "任务组名") + private String jobGroup; + + /** + * 调用目标字符串 + */ + @Excel(name = "调用目标字符串") + private String invokeTarget; + + /** + * 日志信息 + */ + @Excel(name = "日志信息") + private String jobMessage; + + /** + * 执行状态(0正常 1失败) + */ + @Excel(name = "执行状态", readConverterExp = "0=正常,1=失败") + private String status; + + /** + * 异常信息 + */ + @Excel(name = "异常信息") + private String exceptionInfo; + + /** + * 开始时间 + */ + private Date startTime; + + /** + * 停止时间 + */ + private Date stopTime; + + public Long getJobLogId () { + return jobLogId; + } + + public void setJobLogId (Long jobLogId) { + this.jobLogId = jobLogId; + } + + public String getJobName () { + return jobName; + } + + public void setJobName (String jobName) { + this.jobName = jobName; + } + + public String getJobGroup () { + return jobGroup; + } + + public void setJobGroup (String jobGroup) { + this.jobGroup = jobGroup; + } + + public String getInvokeTarget () { + return invokeTarget; + } + + public void setInvokeTarget (String invokeTarget) { + this.invokeTarget = invokeTarget; + } + + public String getJobMessage () { + return jobMessage; + } + + public void setJobMessage (String jobMessage) { + this.jobMessage = jobMessage; + } + + public String getStatus () { + return status; + } + + public void setStatus (String status) { + this.status = status; + } + + public String getExceptionInfo () { + return exceptionInfo; + } + + public void setExceptionInfo (String exceptionInfo) { + this.exceptionInfo = exceptionInfo; + } + + public Date getStartTime () { + return startTime; + } + + public void setStartTime (Date startTime) { + this.startTime = startTime; + } + + public Date getStopTime () { + return stopTime; + } + + public void setStopTime (Date stopTime) { + this.stopTime = stopTime; + } + + @Override + public String toString () { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("jobLogId", getJobLogId()) + .append("jobName", getJobName()) + .append("jobGroup", getJobGroup()) + .append("jobMessage", getJobMessage()) + .append("status", getStatus()) + .append("exceptionInfo", getExceptionInfo()) + .append("startTime", getStartTime()) + .append("stopTime", getStopTime()) + .toString(); + } +} diff --git a/src/main/java/com/fate/job/mapper/SysJobLogMapper.java b/src/main/java/com/fate/job/mapper/SysJobLogMapper.java new file mode 100644 index 0000000..13703af --- /dev/null +++ b/src/main/java/com/fate/job/mapper/SysJobLogMapper.java @@ -0,0 +1,70 @@ +package com.fate.job.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.fate.job.domain.SysJobLog; + +import java.util.List; + +/** + * 调度任务日志信息 数据层 + * + * @author fate + */ +public interface SysJobLogMapper extends BaseMapper { + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * + * @return 调度任务日志集合 + */ + public List selectJobLogList (SysJobLog jobLog); + + /** + * 查询所有调度任务日志 + * + * @return 调度任务日志列表 + */ + public List selectJobLogAll (); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById (Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + * + * @return 结果 + */ + public int insertJobLog (SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * + * @return 结果 + */ + public int deleteJobLogByIds (Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * + * @return 结果 + */ + public int deleteJobLogById (Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog (); +} diff --git a/src/main/java/com/fate/job/mapper/SysJobMapper.java b/src/main/java/com/fate/job/mapper/SysJobMapper.java new file mode 100644 index 0000000..5ac7e9b --- /dev/null +++ b/src/main/java/com/fate/job/mapper/SysJobMapper.java @@ -0,0 +1,74 @@ +package com.fate.job.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.fate.job.domain.SysJob; + +import java.util.List; + +/** + * 调度任务信息 数据层 + * + * @author fate + */ +public interface SysJobMapper extends BaseMapper { + /** + * 查询调度任务日志集合 + * + * @param job 调度信息 + * + * @return 操作日志集合 + */ + public List selectJobList (SysJob job); + + /** + * 查询所有调度任务 + * + * @return 调度任务列表 + */ + public List selectJobAll (); + + /** + * 通过调度ID查询调度任务信息 + * + * @param jobId 调度ID + * + * @return 角色对象信息 + */ + public SysJob selectJobById (Long jobId); + + /** + * 通过调度ID删除调度任务信息 + * + * @param jobId 调度ID + * + * @return 结果 + */ + public int deleteJobById (Long jobId); + + /** + * 批量删除调度任务信息 + * + * @param ids 需要删除的数据ID + * + * @return 结果 + */ + public int deleteJobByIds (Long[] ids); + + /** + * 修改调度任务信息 + * + * @param job 调度任务信息 + * + * @return 结果 + */ + public int updateJob (SysJob job); + + /** + * 新增调度任务信息 + * + * @param job 调度任务信息 + * + * @return 结果 + */ + public int insertJob (SysJob job); +} diff --git a/src/main/java/com/fate/job/service/ISysJobLogService.java b/src/main/java/com/fate/job/service/ISysJobLogService.java new file mode 100644 index 0000000..8a238d8 --- /dev/null +++ b/src/main/java/com/fate/job/service/ISysJobLogService.java @@ -0,0 +1,60 @@ +package com.fate.job.service; + +import com.fate.job.domain.SysJobLog; + +import java.util.List; + +/** + * 定时任务调度日志信息信息 服务层 + * + * @author fate + */ +public interface ISysJobLogService { + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * + * @return 调度任务日志集合 + */ + public List selectJobLogList (SysJobLog jobLog); + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * + * @return 调度任务日志对象信息 + */ + public SysJobLog selectJobLogById (Long jobLogId); + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + public void addJobLog (SysJobLog jobLog); + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的日志ID + * + * @return 结果 + */ + public int deleteJobLogByIds (Long[] logIds); + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + * + * @return 结果 + */ + public int deleteJobLogById (Long jobId); + + /** + * 清空任务日志 + */ + public void cleanJobLog (); +} diff --git a/src/main/java/com/fate/job/service/ISysJobService.java b/src/main/java/com/fate/job/service/ISysJobService.java new file mode 100644 index 0000000..edc0dbb --- /dev/null +++ b/src/main/java/com/fate/job/service/ISysJobService.java @@ -0,0 +1,113 @@ +package com.fate.job.service; + +import com.fate.common.core.exception.job.TaskException; +import com.fate.job.domain.SysJob; +import org.quartz.SchedulerException; + +import java.util.List; + +/** + * 定时任务调度信息信息 服务层 + * + * @author fate + */ +public interface ISysJobService { + /** + * 获取quartz调度器的计划任务 + * + * @param job 调度信息 + * + * @return 调度任务集合 + */ + public List selectJobList (SysJob job); + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * + * @return 调度任务对象信息 + */ + public SysJob selectJobById (Long jobId); + + /** + * 暂停任务 + * + * @param job 调度信息 + * + * @return 结果 + */ + public int pauseJob (SysJob job) throws SchedulerException; + + /** + * 恢复任务 + * + * @param job 调度信息 + * + * @return 结果 + */ + public int resumeJob (SysJob job) throws SchedulerException; + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + * + * @return 结果 + */ + public int deleteJob (SysJob job) throws SchedulerException; + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * + * @return 结果 + */ + public void deleteJobByIds (Long[] jobIds) throws SchedulerException; + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + * + * @return 结果 + */ + public int changeStatus (SysJob job) throws SchedulerException; + + /** + * 立即运行任务 + * + * @param job 调度信息 + * + * @return 结果 + */ + public boolean run (SysJob job) throws SchedulerException; + + /** + * 新增任务 + * + * @param job 调度信息 + * + * @return 结果 + */ + public int insertJob (SysJob job) throws SchedulerException, TaskException; + + /** + * 更新任务 + * + * @param job 调度信息 + * + * @return 结果 + */ + public int updateJob (SysJob job) throws SchedulerException, TaskException; + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * + * @return 结果 + */ + public boolean checkCronExpressionIsValid (String cronExpression); +} diff --git a/src/main/java/com/fate/job/service/SysJobLogServiceImpl.java b/src/main/java/com/fate/job/service/SysJobLogServiceImpl.java new file mode 100644 index 0000000..db61df8 --- /dev/null +++ b/src/main/java/com/fate/job/service/SysJobLogServiceImpl.java @@ -0,0 +1,83 @@ +package com.fate.job.service; + +import com.fate.job.domain.SysJobLog; +import com.fate.job.mapper.SysJobLogMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 定时任务调度日志信息 服务层 + * + * @author fate + */ +@Service +public class SysJobLogServiceImpl implements ISysJobLogService { + @Autowired + private SysJobLogMapper jobLogMapper; + + /** + * 获取quartz调度器日志的计划任务 + * + * @param jobLog 调度日志信息 + * + * @return 调度任务日志集合 + */ + @Override + public List selectJobLogList (SysJobLog jobLog) { + return jobLogMapper.selectJobLogList(jobLog); + } + + /** + * 通过调度任务日志ID查询调度信息 + * + * @param jobLogId 调度任务日志ID + * + * @return 调度任务日志对象信息 + */ + @Override + public SysJobLog selectJobLogById (Long jobLogId) { + return jobLogMapper.selectJobLogById(jobLogId); + } + + /** + * 新增任务日志 + * + * @param jobLog 调度日志信息 + */ + @Override + public void addJobLog (SysJobLog jobLog) { + jobLogMapper.insertJobLog(jobLog); + } + + /** + * 批量删除调度日志信息 + * + * @param logIds 需要删除的数据ID + * + * @return 结果 + */ + @Override + public int deleteJobLogByIds (Long[] logIds) { + return jobLogMapper.deleteJobLogByIds(logIds); + } + + /** + * 删除任务日志 + * + * @param jobId 调度日志ID + */ + @Override + public int deleteJobLogById (Long jobId) { + return jobLogMapper.deleteJobLogById(jobId); + } + + /** + * 清空任务日志 + */ + @Override + public void cleanJobLog () { + jobLogMapper.cleanJobLog(); + } +} diff --git a/src/main/java/com/fate/job/service/SysJobServiceImpl.java b/src/main/java/com/fate/job/service/SysJobServiceImpl.java new file mode 100644 index 0000000..c307aba --- /dev/null +++ b/src/main/java/com/fate/job/service/SysJobServiceImpl.java @@ -0,0 +1,239 @@ +package com.fate.job.service; + +import com.fate.common.core.constant.ScheduleConstants; +import com.fate.common.core.exception.job.TaskException; +import com.fate.job.domain.SysJob; +import com.fate.job.mapper.SysJobMapper; +import com.fate.job.util.CronUtils; +import com.fate.job.util.ScheduleUtils; +import org.quartz.JobDataMap; +import org.quartz.JobKey; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.PostConstruct; +import java.util.List; + +/** + * 定时任务调度信息 服务层 + * + * @author fate + */ +@Service +public class SysJobServiceImpl implements ISysJobService { + @Autowired + private Scheduler scheduler; + + @Autowired + private SysJobMapper jobMapper; + + /** + * 项目启动时,初始化定时器 主要是防止手动修改数据库导致未同步到定时任务处理(注:不能手动修改数据库ID和任务组名,否则会导致脏数据) + */ + @PostConstruct + public void init () throws SchedulerException, TaskException { + scheduler.clear(); + List jobList = jobMapper.selectJobAll(); + for (SysJob job : jobList) { + ScheduleUtils.createScheduleJob(scheduler, job); + } + } + + /** + * 获取quartz调度器的计划任务列表 + * + * @param job 调度信息 + * + * @return + */ + @Override + public List selectJobList (SysJob job) { + return jobMapper.selectJobList(job); + } + + /** + * 通过调度任务ID查询调度信息 + * + * @param jobId 调度任务ID + * + * @return 调度任务对象信息 + */ + @Override + public SysJob selectJobById (Long jobId) { + return jobMapper.selectJobById(jobId); + } + + /** + * 暂停任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int pauseJob (SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 恢复任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int resumeJob (SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + job.setStatus(ScheduleConstants.Status.NORMAL.getValue()); + int rows = jobMapper.updateJob(job); + if (rows > 0) { + scheduler.resumeJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 删除任务后,所对应的trigger也将被删除 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int deleteJob (SysJob job) throws SchedulerException { + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + int rows = jobMapper.deleteJobById(jobId); + if (rows > 0) { + scheduler.deleteJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + return rows; + } + + /** + * 批量删除调度信息 + * + * @param jobIds 需要删除的任务ID + * + * @return 结果 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJobByIds (Long[] jobIds) throws SchedulerException { + for (Long jobId : jobIds) { + SysJob job = jobMapper.selectJobById(jobId); + deleteJob(job); + } + } + + /** + * 任务调度状态修改 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int changeStatus (SysJob job) throws SchedulerException { + int rows = 0; + String status = job.getStatus(); + if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) { + rows = resumeJob(job); + } else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) { + rows = pauseJob(job); + } + return rows; + } + + /** + * 立即运行任务 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean run (SysJob job) throws SchedulerException { + boolean result = false; + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + SysJob properties = selectJobById(job.getJobId()); + // 参数 + JobDataMap dataMap = new JobDataMap(); + dataMap.put(ScheduleConstants.TASK_PROPERTIES, properties); + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) { + result = true; + scheduler.triggerJob(jobKey, dataMap); + } + return result; + } + + /** + * 新增任务 + * + * @param job 调度信息 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int insertJob (SysJob job) throws SchedulerException, TaskException { + job.setStatus(ScheduleConstants.Status.PAUSE.getValue()); + int rows = jobMapper.insertJob(job); + if (rows > 0) { + ScheduleUtils.createScheduleJob(scheduler, job); + } + return rows; + } + + /** + * 更新任务的时间表达式 + * + * @param job 调度信息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public int updateJob (SysJob job) throws SchedulerException, TaskException { + SysJob properties = selectJobById(job.getJobId()); + int rows = jobMapper.updateJob(job); + if (rows > 0) { + updateSchedulerJob(job, properties.getJobGroup()); + } + return rows; + } + + /** + * 更新任务 + * + * @param job 任务对象 + * @param jobGroup 任务组名 + */ + public void updateSchedulerJob (SysJob job, String jobGroup) throws SchedulerException, TaskException { + Long jobId = job.getJobId(); + // 判断是否存在 + JobKey jobKey = ScheduleUtils.getJobKey(jobId, jobGroup); + if (scheduler.checkExists(jobKey)) { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(jobKey); + } + ScheduleUtils.createScheduleJob(scheduler, job); + } + + /** + * 校验cron表达式是否有效 + * + * @param cronExpression 表达式 + * + * @return 结果 + */ + @Override + public boolean checkCronExpressionIsValid (String cronExpression) { + return CronUtils.isValid(cronExpression); + } +} diff --git a/src/main/java/com/fate/job/task/MyTask.java b/src/main/java/com/fate/job/task/MyTask.java new file mode 100644 index 0000000..03fd215 --- /dev/null +++ b/src/main/java/com/fate/job/task/MyTask.java @@ -0,0 +1,24 @@ +package com.fate.job.task; + +import com.fate.common.core.utils.StringUtils; +import org.springframework.stereotype.Component; + +/** + * 定时任务调度测试 + * + * @author fate + */ +@Component("myTask") +public class MyTask { + public void ryMultipleParams (String s, Boolean b, Long l, Double d, Integer i) { + System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i)); + } + + public void ryParams (String params) { + System.out.println("执行有参方法:" + params); + } + + public void ryNoParams () { + System.out.println("执行无参方法"); + } +} diff --git a/src/main/java/com/fate/job/util/AbstractQuartzJob.java b/src/main/java/com/fate/job/util/AbstractQuartzJob.java new file mode 100644 index 0000000..9e569a1 --- /dev/null +++ b/src/main/java/com/fate/job/util/AbstractQuartzJob.java @@ -0,0 +1,97 @@ +package com.fate.job.util; + +import com.fate.common.core.constant.ScheduleConstants; +import com.fate.common.core.utils.ExceptionUtil; +import com.fate.common.core.utils.SpringUtils; +import com.fate.common.core.utils.StringUtils; +import com.fate.common.core.utils.bean.BeanUtils; +import com.fate.job.domain.SysJob; +import com.fate.job.domain.SysJobLog; +import com.fate.job.service.ISysJobLogService; +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +/** + * 抽象quartz调用 + * + * @author fate + */ +public abstract class AbstractQuartzJob implements Job { + private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class); + + /** + * 线程本地变量 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + @Override + public void execute (JobExecutionContext context) throws JobExecutionException { + SysJob sysJob = new SysJob(); + BeanUtils.copyBeanProp(sysJob, context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES)); + try { + before(context, sysJob); + if (sysJob != null) { + doExecute(context, sysJob); + } + after(context, sysJob, null); + } catch (Exception e) { + log.error("任务执行异常 - :", e); + after(context, sysJob, e); + } + } + + /** + * 执行前 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void before (JobExecutionContext context, SysJob sysJob) { + threadLocal.set(new Date()); + } + + /** + * 执行后 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + */ + protected void after (JobExecutionContext context, SysJob sysJob, Exception e) { + Date startTime = threadLocal.get(); + threadLocal.remove(); + + final SysJobLog sysJobLog = new SysJobLog(); + sysJobLog.setJobName(sysJob.getJobName()); + sysJobLog.setJobGroup(sysJob.getJobGroup()); + sysJobLog.setInvokeTarget(sysJob.getInvokeTarget()); + sysJobLog.setStartTime(startTime); + sysJobLog.setStopTime(new Date()); + long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime(); + sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒"); + if (e != null) { + sysJobLog.setStatus("1"); + String errorMsg = StringUtils.substring(ExceptionUtil.getExceptionMessage(e), 0, 2000); + sysJobLog.setExceptionInfo(errorMsg); + } else { + sysJobLog.setStatus("0"); + } + + // 写入数据库当中 + SpringUtils.getBean(ISysJobLogService.class).addJobLog(sysJobLog); + } + + /** + * 执行方法,由子类重载 + * + * @param context 工作执行上下文对象 + * @param sysJob 系统计划任务 + * + * @throws Exception 执行过程中的异常 + */ + protected abstract void doExecute (JobExecutionContext context, SysJob sysJob) throws Exception; +} diff --git a/src/main/java/com/fate/job/util/CronUtils.java b/src/main/java/com/fate/job/util/CronUtils.java new file mode 100644 index 0000000..ec31bc4 --- /dev/null +++ b/src/main/java/com/fate/job/util/CronUtils.java @@ -0,0 +1,56 @@ +package com.fate.job.util; + +import org.quartz.CronExpression; + +import java.text.ParseException; +import java.util.Date; + +/** + * cron表达式工具类 + * + * @author fate + */ +public class CronUtils { + /** + * 返回一个布尔值代表一个给定的Cron表达式的有效性 + * + * @param cronExpression Cron表达式 + * + * @return boolean 表达式是否有效 + */ + public static boolean isValid (String cronExpression) { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 返回一个字符串值,表示该消息无效Cron表达式给出有效性 + * + * @param cronExpression Cron表达式 + * + * @return String 无效时返回表达式错误描述,如果有效返回null + */ + public static String getInvalidMessage (String cronExpression) { + try { + new CronExpression(cronExpression); + return null; + } catch (ParseException pe) { + return pe.getMessage(); + } + } + + /** + * 返回下一个执行时间根据给定的Cron表达式 + * + * @param cronExpression Cron表达式 + * + * @return Date 下次Cron表达式执行时间 + */ + public static Date getNextExecution (String cronExpression) { + try { + CronExpression cron = new CronExpression(cronExpression); + return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis())); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } +} diff --git a/src/main/java/com/fate/job/util/JobInvokeUtil.java b/src/main/java/com/fate/job/util/JobInvokeUtil.java new file mode 100644 index 0000000..832b18e --- /dev/null +++ b/src/main/java/com/fate/job/util/JobInvokeUtil.java @@ -0,0 +1,165 @@ +package com.fate.job.util; + +import com.fate.common.core.utils.SpringUtils; +import com.fate.common.core.utils.StringUtils; +import com.fate.job.domain.SysJob; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; + +/** + * 任务执行工具 + * + * @author fate + */ +public class JobInvokeUtil { + /** + * 执行方法 + * + * @param sysJob 系统任务 + */ + public static void invokeMethod (SysJob sysJob) throws Exception { + String invokeTarget = sysJob.getInvokeTarget(); + String beanName = getBeanName(invokeTarget); + String methodName = getMethodName(invokeTarget); + List methodParams = getMethodParams(invokeTarget); + + if (!isValidClassName(beanName)) { + Object bean = SpringUtils.getBean(beanName); + invokeMethod(bean, methodName, methodParams); + } else { + Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance(); + invokeMethod(bean, methodName, methodParams); + } + } + + /** + * 调用任务方法 + * + * @param bean 目标对象 + * @param methodName 方法名称 + * @param methodParams 方法参数 + */ + private static void invokeMethod (Object bean, String methodName, List methodParams) + throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, + InvocationTargetException { + if (StringUtils.isNotNull(methodParams) && methodParams.size() > 0) { + Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams)); + method.invoke(bean, getMethodParamsValue(methodParams)); + } else { + Method method = bean.getClass().getMethod(methodName); + method.invoke(bean); + } + } + + /** + * 校验是否为为class包名 + * + * @param invokeTarget 名称 + * + * @return true是 false否 + */ + public static boolean isValidClassName (String invokeTarget) { + return StringUtils.countMatches(invokeTarget, ".") > 1; + } + + /** + * 获取bean名称 + * + * @param invokeTarget 目标字符串 + * + * @return bean名称 + */ + public static String getBeanName (String invokeTarget) { + String beanName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringBeforeLast(beanName, "."); + } + + /** + * 获取bean方法 + * + * @param invokeTarget 目标字符串 + * + * @return method方法 + */ + public static String getMethodName (String invokeTarget) { + String methodName = StringUtils.substringBefore(invokeTarget, "("); + return StringUtils.substringAfterLast(methodName, "."); + } + + /** + * 获取method方法参数相关列表 + * + * @param invokeTarget 目标字符串 + * + * @return method方法相关参数列表 + */ + public static List getMethodParams (String invokeTarget) { + String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")"); + if (StringUtils.isEmpty(methodStr)) { + return null; + } + String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)"); + List classs = new LinkedList<>(); + for (int i = 0 ; i < methodParams.length ; i++) { + String str = StringUtils.trimToEmpty(methodParams[i]); + // String字符串类型,以'或"开头 + if (StringUtils.startsWithAny(str, "'", "\"")) { + classs.add(new Object[]{StringUtils.substring(str, 1, str.length() - 1), String.class}); + } + // boolean布尔类型,等于true或者false + else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) { + classs.add(new Object[]{Boolean.valueOf(str), Boolean.class}); + } + // long长整形,以L结尾 + else if (StringUtils.endsWith(str, "L")) { + classs.add(new Object[]{Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class}); + } + // double浮点类型,以D结尾 + else if (StringUtils.endsWith(str, "D")) { + classs.add(new Object[]{Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class}); + } + // 其他类型归类为整形 + else { + classs.add(new Object[]{Integer.valueOf(str), Integer.class}); + } + } + return classs; + } + + /** + * 获取参数类型 + * + * @param methodParams 参数相关列表 + * + * @return 参数类型列表 + */ + public static Class[] getMethodParamsType (List methodParams) { + Class[] classs = new Class[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) { + classs[index] = (Class) os[1]; + index++; + } + return classs; + } + + /** + * 获取参数值 + * + * @param methodParams 参数相关列表 + * + * @return 参数值列表 + */ + public static Object[] getMethodParamsValue (List methodParams) { + Object[] classs = new Object[methodParams.size()]; + int index = 0; + for (Object[] os : methodParams) { + classs[index] = (Object) os[0]; + index++; + } + return classs; + } +} diff --git a/src/main/java/com/fate/job/util/QuartzDisallowConcurrentExecution.java b/src/main/java/com/fate/job/util/QuartzDisallowConcurrentExecution.java new file mode 100644 index 0000000..71e0e60 --- /dev/null +++ b/src/main/java/com/fate/job/util/QuartzDisallowConcurrentExecution.java @@ -0,0 +1,18 @@ +package com.fate.job.util; + +import com.fate.job.domain.SysJob; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; + +/** + * 定时任务处理(禁止并发执行) + * + * @author fate + */ +@DisallowConcurrentExecution +public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob { + @Override + protected void doExecute (JobExecutionContext context, SysJob sysJob) throws Exception { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/src/main/java/com/fate/job/util/QuartzJobExecution.java b/src/main/java/com/fate/job/util/QuartzJobExecution.java new file mode 100644 index 0000000..11022f2 --- /dev/null +++ b/src/main/java/com/fate/job/util/QuartzJobExecution.java @@ -0,0 +1,16 @@ +package com.fate.job.util; + +import com.fate.job.domain.SysJob; +import org.quartz.JobExecutionContext; + +/** + * 定时任务处理(允许并发执行) + * + * @author fate + */ +public class QuartzJobExecution extends AbstractQuartzJob { + @Override + protected void doExecute (JobExecutionContext context, SysJob sysJob) throws Exception { + JobInvokeUtil.invokeMethod(sysJob); + } +} diff --git a/src/main/java/com/fate/job/util/ScheduleUtils.java b/src/main/java/com/fate/job/util/ScheduleUtils.java new file mode 100644 index 0000000..85e0514 --- /dev/null +++ b/src/main/java/com/fate/job/util/ScheduleUtils.java @@ -0,0 +1,121 @@ +package com.fate.job.util; + +import com.fate.common.core.constant.Constants; +import com.fate.common.core.constant.ScheduleConstants; +import com.fate.common.core.exception.job.TaskException; +import com.fate.common.core.exception.job.TaskException.Code; +import com.fate.common.core.utils.SpringUtils; +import com.fate.common.core.utils.StringUtils; +import com.fate.job.domain.SysJob; +import org.quartz.*; + +/** + * 定时任务工具类 + * + * @author fate + */ +public class ScheduleUtils { + /** + * 得到quartz任务类 + * + * @param sysJob 执行计划 + * + * @return 具体执行任务类 + */ + private static Class getQuartzJobClass (SysJob sysJob) { + boolean isConcurrent = "0".equals(sysJob.getConcurrent()); + return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class; + } + + /** + * 构建任务触发对象 + */ + public static TriggerKey getTriggerKey (Long jobId, String jobGroup) { + return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 构建任务键对象 + */ + public static JobKey getJobKey (Long jobId, String jobGroup) { + return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup); + } + + /** + * 创建定时任务 + */ + public static void createScheduleJob (Scheduler scheduler, SysJob job) throws SchedulerException, TaskException { + Class jobClass = getQuartzJobClass(job); + // 构建job信息 + Long jobId = job.getJobId(); + String jobGroup = job.getJobGroup(); + JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build(); + + // 表达式调度构建器 + CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression()); + cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder); + + // 按新的cronExpression表达式构建一个新的trigger + CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup)) + .withSchedule(cronScheduleBuilder).build(); + + // 放入参数,运行时的方法可以获取 + jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job); + + // 判断是否存在 + if (scheduler.checkExists(getJobKey(jobId, jobGroup))) { + // 防止创建时存在数据问题 先移除,然后在执行创建操作 + scheduler.deleteJob(getJobKey(jobId, jobGroup)); + } + + // 判断任务是否过期 + if (StringUtils.isNotNull(CronUtils.getNextExecution(job.getCronExpression()))) { + // 执行调度任务 + scheduler.scheduleJob(jobDetail, trigger); + } + + // 暂停任务 + if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) { + scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup)); + } + } + + /** + * 设置定时任务策略 + */ + public static CronScheduleBuilder handleCronScheduleMisfirePolicy (SysJob job, CronScheduleBuilder cb) + throws TaskException { + switch (job.getMisfirePolicy()) { + case ScheduleConstants.MISFIRE_DEFAULT: + return cb; + case ScheduleConstants.MISFIRE_IGNORE_MISFIRES: + return cb.withMisfireHandlingInstructionIgnoreMisfires(); + case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED: + return cb.withMisfireHandlingInstructionFireAndProceed(); + case ScheduleConstants.MISFIRE_DO_NOTHING: + return cb.withMisfireHandlingInstructionDoNothing(); + default: + throw new TaskException("The task misfire policy '" + job.getMisfirePolicy() + + "' cannot be used in cron schedule tasks", Code.CONFIG_ERROR); + } + } + + /** + * 检查包名是否为白名单配置 + * + * @param invokeTarget 目标字符串 + * + * @return 结果 + */ + public static boolean whiteList (String invokeTarget) { + String packageName = StringUtils.substringBefore(invokeTarget, "("); + int count = StringUtils.countMatches(packageName, "."); + if (count > 1) { + return StringUtils.containsAnyIgnoreCase(invokeTarget, Constants.JOB_WHITELIST_STR); + } + Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]); + String beanPackageName = obj.getClass().getPackage().getName(); + return StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_WHITELIST_STR) + && !StringUtils.containsAnyIgnoreCase(beanPackageName, Constants.JOB_ERROR_STR); + } +} diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 0000000..0dd5eee --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,2 @@ +Spring Boot Version: ${spring-boot.version} +Spring Application Name: ${spring.application.name} diff --git a/src/main/resources/bootstrap.yml b/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..df2d3f3 --- /dev/null +++ b/src/main/resources/bootstrap.yml @@ -0,0 +1,25 @@ +# Tomcat +server: + port: 9203 + +# Spring +spring: + application: + # 应用名称 + name: fate-job + profiles: + # 环境配置 + active: dev + cloud: + nacos: + discovery: + # 服务注册地址 + server-addr: 182.254.222.21:8848 + config: + # 配置中心地址 + server-addr: 182.254.222.21:8848 + # 配置文件格式 + file-extension: yml + # 共享配置 + shared-configs: + - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..92c1a0b --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + ${log.pattern} + + + + + + ${log.path}/info.log + + + + ${log.path}/info.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + INFO + + ACCEPT + + DENY + + + + + ${log.path}/error.log + + + + ${log.path}/error.%d{yyyy-MM-dd}.log + + 60 + + + ${log.pattern} + + + + ERROR + + ACCEPT + + DENY + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/job/SysJobLogMapper.xml b/src/main/resources/mapper/job/SysJobLogMapper.xml new file mode 100644 index 0000000..7c2d950 --- /dev/null +++ b/src/main/resources/mapper/job/SysJobLogMapper.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + select job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + from sys_job_log + + + + + + + + + + delete + from sys_job_log + where job_log_id = #{jobLogId} + + + + delete from sys_job_log where job_log_id in + + #{jobLogId} + + + + + truncate table sys_job_log + + + + insert into sys_job_log( + job_log_id, + job_name, + job_group, + invoke_target, + job_message, + status, + exception_info, + create_time + )values( + #{jobLogId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{jobMessage}, + #{status}, + #{exceptionInfo}, + sysdate() + ) + + + diff --git a/src/main/resources/mapper/job/SysJobMapper.xml b/src/main/resources/mapper/job/SysJobMapper.xml new file mode 100644 index 0000000..78ca578 --- /dev/null +++ b/src/main/resources/mapper/job/SysJobMapper.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + select job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + create_by, + create_time, + remark + from sys_job + + + + + + + + + + delete + from sys_job + where job_id = #{jobId} + + + + delete from sys_job where job_id in + + #{jobId} + + + + + update sys_job + + job_name = #{jobName}, + job_group = #{jobGroup}, + invoke_target = #{invokeTarget}, + cron_expression = #{cronExpression}, + misfire_policy = #{misfirePolicy}, + concurrent = #{concurrent}, + status = #{status}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = sysdate() + + where job_id = #{jobId} + + + + insert into sys_job( + job_id, + job_name, + job_group, + invoke_target, + cron_expression, + misfire_policy, + concurrent, + status, + remark, + create_by, + create_time + )values( + #{jobId}, + #{jobName}, + #{jobGroup}, + #{invokeTarget}, + #{cronExpression}, + #{misfirePolicy}, + #{concurrent}, + #{status}, + #{remark}, + #{createBy}, + sysdate() + ) + + +