传统的定时任务,要么是使用@Scheduled在程序中写死的定时策略,要么是使用
Quartz或者xxl-job定时任务框架,就很重。
本文介绍的定时方案采用hutool工具包的CronUtil配合反射实现,支持选择定时任务类,自定义参数,主打轻量、灵活。
此方案也是借鉴的小诺开源框架,测试页面套用的若依的定时任务页面,在那基础上稍加改动就好
cn.hutool
hutool-all
5.8.12
@Data
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;
/** 是否并发执行(0允许 1禁止) */
@Excel(name = "并发执行", readConverterExp = "0=允许,1=禁止")
private String concurrent;
/** 任务状态(0正常 1暂停) */
@Excel(name = "任务状态", readConverterExp = "0=正常,1=暂停")
private String status;
/** 任务参数 */
@Excel(name = "任务参数")
private String remark;
}
先写一个接口,所有定时任务都实现这个接口,方便我们遍历所有可用的定时任务类
public interface CommonTimerTaskRunner {
/**
* 任务执行的具体内容
*
* @author xuyuxiang
* @date 2022/8/15 16:09
**/
void action(String param);
}
然后编写具体的定时任务,每个定时任务单独写一个类,不要混在一起,方便管理
@Slf4j
@Component
public class DevJobTimerTaskRunner implements CommonTimerTaskRunner {
private static final String INITPARAM = "{\"KEY1\":200,\"第二个参数\":\"aaaaa\"}";
private int n = 1;
@Override
public void action(String param) {
JSONObject jsonObject = JSONUtil.parseObj(param);
log.info("我是一个定时任务,正在在被执行第{}次,参数1:{},参数2:{}", n, jsonObject.getInt("KEY1"), jsonObject.getStr("第二个参数"));
n = n + 1;
}
}
这里的INITPARAM 属性是一个参数示例值,每个定时任务的可选参数可能都不一样,json结构不一致,这个参数会在添加和修改定时任务时同下卡框一起带出来
后台也是在若依框架基础上测试的,使用的mybatis框架,这里就忽略dao和mapper细节,直接贴servce
SysJobServiceImpl
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.cron.CronUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.ruoyi.common.constant.ScheduleConstants;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.quartz.domain.SelectOptionVO;
import com.ruoyi.quartz.domain.SysJob;
import com.ruoyi.quartz.mapper.SysJobMapper;
import com.ruoyi.quartz.service.CommonTimerTaskRunner;
import com.ruoyi.quartz.service.ISysJobService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 定时任务调度信息 服务层
*
* @author ruoyi
*/
@Service
public class SysJobServiceImpl implements ISysJobService {
@Autowired
private SysJobMapper jobMapper;
/**
* 获取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) {
SysJob oldjob = jobMapper.selectJobById(job.getJobId());
if (ScheduleConstants.Status.PAUSE.getValue().equals(oldjob.getStatus())) {
throw new BaseException("该任务已处于暂停状态");
}
CronUtil.remove(job.getJobId() + "");
job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
return jobMapper.updateJob(job);
}
/**
* 启动定时任务
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int resumeJob(SysJob job) {
Long jobId = job.getJobId();
SysJob oldjob = jobMapper.selectJobById(jobId);
if (ScheduleConstants.Status.NORMAL.getValue().equals(oldjob.getStatus()) && ScheduleConstants.MISFIRE_IGNORE_MISFIRES.equals(oldjob.getMisfirePolicy())) {
throw new BaseException("该定时任务已处于运行状态,请勿重复执行");
}
//注册定时任务
CronUtil.schedule(job.getJobId() + "", job.getCronExpression(), () -> {
try {
// 运行定时任务
((CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(job.getInvokeTarget()))).action(job.getRemark());
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:" + job.getInvokeTarget());
}
});
job.setStatus(ScheduleConstants.Status.NORMAL.getValue());
return jobMapper.updateJob(job);
}
/**
* 删除任务后,所对应的trigger也将被删除
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteJob(SysJob job) {
CronUtil.remove(job.getJobId() + "");
return jobMapper.deleteJobById(job.getJobId());
}
/**
* 批量删除调度信息
*
* @param jobIds 需要删除的任务ID
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteJobByIds(Long[] jobIds) {
for (Long jobId : jobIds) {
SysJob job = jobMapper.selectJobById(jobId);
deleteJob(job);
}
}
/**
* 任务调度状态修改
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int changeStatus(SysJob job) {
int rows = 0;
String status = job.getStatus();
if (ScheduleConstants.Status.NORMAL.getValue().equals(status)) {
if (ScheduleConstants.MISFIRE_IGNORE_MISFIRES.equals(job.getMisfirePolicy())) {
rows = resumeJob(job);
}
} else if (ScheduleConstants.Status.PAUSE.getValue().equals(status)) {
if (ScheduleConstants.MISFIRE_IGNORE_MISFIRES.equals(job.getMisfirePolicy())) {
rows = pauseJob(job);
}
}
jobMapper.updateJob(job);
return rows;
}
/**
* 立即运行任务
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean runNow(SysJob job) {
SysJob sysJob = jobMapper.selectJobById(job.getJobId());
try {
// 直接运行一次
((CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(sysJob.getInvokeTarget()))).action(sysJob.getRemark());
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:" + sysJob.getInvokeTarget());
}
return true;
}
/**
* 新增任务
*
* @param job 调度信息 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int insertJob(SysJob job) {
checkParam(job);
job.setStatus(ScheduleConstants.Status.PAUSE.getValue());
jobMapper.insertJob(job);
//立即启动定时任务
if (ScheduleConstants.Status.NORMAL.getValue().equals(job.getStatus())) {
if (ScheduleConstants.MISFIRE_IGNORE_MISFIRES.equals(job.getMisfirePolicy())) {
//开启定时任务
resumeJob(job);
} else if (ScheduleConstants.MISFIRE_FIRE_AND_PROCEED.equals(job.getMisfirePolicy())) {
try {
// 直接运行一次
((CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(job.getInvokeTarget()))).action(job.getRemark());
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:" + job.getInvokeTarget());
}
}
}
return 1;
}
/**
* 任务参数校验
*
* @param job
*/
void checkParam(SysJob job) {
//校验表达式
if (!CronExpression.isValidExpression(job.getCronExpression())) {
throw new BaseException("cron表达式:" + job.getCronExpression() + "格式不正确");
}
//校验定时任务类
try {
Class actionClass = Class.forName(job.getInvokeTarget());
if (!CommonTimerTaskRunner.class.isAssignableFrom(actionClass)) {
throw new BaseException("定时任务对应的类:" + job.getInvokeTarget() + "不符合要求");
}
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:" + job.getInvokeTarget());
}
SysJob sysUser2 = new SysJob();
sysUser2.setInvokeTarget(job.getInvokeTarget());
sysUser2.setCronExpression(job.getCronExpression());
List jobList = jobMapper.selectJobList(sysUser2);
if (!CollectionUtils.isEmpty(jobList)) {
throw new BaseException("存在重复执行的定时任务,名称为:" + job.getJobName());
}
}
/**
* 更新任务的时间表达式
*
* @param job 调度信息
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int updateJob(SysJob job) {
//校验表达式
if (!CronExpression.isValidExpression(job.getCronExpression())) {
throw new BaseException("cron表达式:" + job.getCronExpression() + "格式不正确");
}
//校验定时任务类
try {
Class actionClass = Class.forName(job.getInvokeTarget());
if (!CommonTimerTaskRunner.class.isAssignableFrom(actionClass)) {
throw new BaseException("定时任务对应的类:" + job.getInvokeTarget() + "不符合要求");
}
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:" + job.getInvokeTarget());
}
//立即启动定时任务
SysJob oldjob = jobMapper.selectJobById(job.getJobId());
//修改状态
if (!oldjob.getStatus().equals(job.getStatus())) {
if (ScheduleConstants.Status.NORMAL.getValue().equals(job.getStatus())) {
//改为正常状态,检查策略
if (ScheduleConstants.MISFIRE_IGNORE_MISFIRES.equals(job.getMisfirePolicy())) {
resumeJob(job);
} else if (ScheduleConstants.MISFIRE_FIRE_AND_PROCEED.equals(job.getMisfirePolicy())) {
try {
//执行一次
((CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(job.getInvokeTarget()))).action(job.getRemark());
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:" + job.getInvokeTarget());
}
}
} else {
//改为禁用状态,并且修改前有任务时在运行时,停止任务
if (ScheduleConstants.MISFIRE_IGNORE_MISFIRES.equals(oldjob.getMisfirePolicy())) {
CronUtil.remove(job.getJobId() + "");
}
}
}
if (ScheduleConstants.Status.NORMAL.getValue().equals(job.getStatus())) {
if (ScheduleConstants.MISFIRE_IGNORE_MISFIRES.equals(job.getMisfirePolicy())) {
//开启定时任务
resumeJob(job);
} else if (ScheduleConstants.MISFIRE_FIRE_AND_PROCEED.equals(job.getMisfirePolicy())) {
try {
// 直接运行一次
((CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(job.getInvokeTarget()))).action(job.getRemark());
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:" + job.getInvokeTarget());
}
}
}
return jobMapper.updateJob(job);
}
/**
* 查询所有实现了CommonTimerTaskRunner 的类名
*
* @return
*/
@Override
public List getActionClass() {
Map commonTimerTaskRunnerMap = SpringUtil.getBeansOfType(CommonTimerTaskRunner.class);
if (ObjectUtil.isNotEmpty(commonTimerTaskRunnerMap)) {
Collection values = commonTimerTaskRunnerMap.values();
return values.stream().map(commonTimerTaskRunner -> {
String className = commonTimerTaskRunner.getClass().getName();
String url = null;
try {
Field urlField = commonTimerTaskRunner.getClass().getDeclaredField("INITPARAM");
urlField.setAccessible(true);
url = (String) urlField.get(commonTimerTaskRunner);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
SelectOptionVO selectOptionVO = new SelectOptionVO();
selectOptionVO.setName(className);
selectOptionVO.setValue(url);
return selectOptionVO;
}).collect(Collectors.toList());
} else {
return CollectionUtil.newArrayList();
}
}
}
SelectOptionVO 是一个对应select组件的VO,就name和value两个属性,其他几个是枚举类,看job实体备注就清楚了
AjaxResult 是一个通用返回VO,包含code,message,data这些通用属性
/**
* 调度任务信息操作处理
*
* @author ruoyi
*/
@RestController
@RequestMapping("/monitor/job")
public class SysJobController extends BaseController {
@Autowired
private ISysJobService jobService;
/**
* 查询定时任务列表
*/
@PreAuthorize("@ss.hasPermi('monitor:job:list')")
@GetMapping("/list")
public TableDataInfo list(SysJob sysJob) {
startPage();
List list = jobService.selectJobList(sysJob);
return getDataTable(list);
}
/**
* 导出定时任务列表
*/
@PreAuthorize("@ss.hasPermi('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, "定时任务");
}
/**
* 获取定时任务详细信息
*/
@PreAuthorize("@ss.hasPermi('monitor:job:query')")
@GetMapping(value = "/{jobId}")
public AjaxResult getInfo(@PathVariable("jobId") Long jobId) {
return success(jobService.selectJobById(jobId));
}
/**
* 新增定时任务
*/
@PreAuthorize("@ss.hasPermi('monitor:job:add')")
@Log(title = "定时任务", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysJob job) throws TaskException {
job.setCreateBy(getUsername());
return toAjax(jobService.insertJob(job));
}
/**
* 修改定时任务
*/
@PreAuthorize("@ss.hasPermi('monitor:job:edit')")
@Log(title = "定时任务", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysJob job) throws TaskException {
job.setUpdateBy(getUsername());
return toAjax(jobService.updateJob(job));
}
/**
* 定时任务状态修改
*/
@PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
@Log(title = "定时任务", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody SysJob job) {
SysJob newJob = jobService.selectJobById(job.getJobId());
if (newJob.getStatus().equals(job.getStatus())) {
return toAjax(1);
}
newJob.setStatus(job.getStatus());
return toAjax(jobService.changeStatus(newJob));
}
/**
* 定时任务立即执行一次
*/
@PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
@Log(title = "定时任务", businessType = BusinessType.UPDATE)
@PutMapping("/run")
public AjaxResult run(@RequestBody SysJob job) {
boolean result = jobService.runNow(job);
return result ? success() : error("任务不存在或已过期!");
}
/**
* 删除定时任务
*/
@PreAuthorize("@ss.hasPermi('monitor:job:remove')")
@Log(title = "定时任务", businessType = BusinessType.DELETE)
@DeleteMapping("/{jobIds}")
public AjaxResult remove(@PathVariable Long[] jobIds) throws TaskException {
jobService.deleteJobByIds(jobIds);
return success();
}
/**
* 删除定时任务
*/
@GetMapping("/getJobclass")
public AjaxResult getJobclass() {
return success(jobService.getActionClass());
}
}
若依原本使用的quartz框架,有一个并发选项,我这里完全不用到,只是没清理,其他的都有注释自己看吧
搜索
重置
新增
修改
删除
导出
日志
修改
删除
handleCommand(command, scope.row)" v-hasPermi="['monitor:job:changeStatus', 'monitor:job:query']">
更多
执行一次
任务详细
调度日志
执行参数
传入定时任务的json参数
调用示例:{"key1":"aaa","key2":123}
生成表达式
立即执行
执行一次
放弃执行
{{ dict.label }}
{{ form.jobId }}
{{ form.jobName }}
{{ jobGroupFormat(form) }}
{{ form.createTime }}
{{ form.cronExpression }}
{{ parseTime(form.nextValidTime) }}
{{ form.invokeTarget }}
正常
失败
允许
禁止
默认策略
立即执行
执行一次
放弃执行
import request from '@/utils/request'
// 查询定时任务调度列表
export function listJob(query) {
console.log("查询参数,",query);
return request({
url: '/monitor/job/list',
method: 'get',
params: query
})
}
// 查询定时任务调度详细
export function getJob(jobId) {
return request({
url: '/monitor/job/' + jobId,
method: 'get'
})
}
// 查询定时任务调度列表
export function jobClass() {
return request({
url: '/monitor/job/getJobclass',
method: 'get'
})
}
// 新增定时任务调度
export function addJob(data) {
return request({
url: '/monitor/job',
method: 'post',
data: data
})
}
// 修改定时任务调度
export function updateJob(data) {
return request({
url: '/monitor/job',
method: 'put',
data: data
})
}
// 删除定时任务调度
export function delJob(jobId) {
return request({
url: '/monitor/job/' + jobId,
method: 'delete'
})
}
// 任务状态修改
export function changeJobStatus(jobId, status) {
const data = {
jobId,
status
}
return request({
url: '/monitor/job/changeStatus',
method: 'put',
data: data
})
}
// 定时任务立即执行一次
export function runJob(jobId, jobGroup) {
const data = {
jobId,
jobGroup
}
return request({
url: '/monitor/job/run',
method: 'put',
data: data
})
}
@Slf4j
@Configuration
public class DevJobListener implements ApplicationListener, Ordered {
@SuppressWarnings("ALL")
@Override
public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
SysJob job = new SysJob();
job.setStatus("0");
job.setMisfirePolicy("1");
SpringUtil.getBean(ISysJobService.class).selectJobList(job)
.forEach(devJob -> CronUtil.schedule(devJob.getJobId() + "", devJob.getCronExpression(), () -> {
try {
// 运行定时任务
((CommonTimerTaskRunner) SpringUtil.getBean(Class.forName(devJob.getInvokeTarget()))).action(devJob.getRemark());
} catch (ClassNotFoundException e) {
throw new BaseException("定时任务找不到对应的类,名称为:{}", devJob.getInvokeTarget());
}
}));
// 设置秒级别的启用
CronUtil.setMatchSecond(true);
log.info("启动定时器执行器");
CronUtil.restart();
}
@Override
public int getOrder() {
return LOWEST_PRECEDENCE;
}
}