动态定时任务-支持选择执行类和自定义参数

前言:

传统的定时任务,要么是使用@Scheduled在程序中写死的定时策略,要么是使用
Quartz或者xxl-job定时任务框架,就很重。
本文介绍的定时方案采用hutool工具包的CronUtil配合反射实现,支持选择定时任务类,自定义参数,主打轻量、灵活。
此方案也是借鉴的小诺开源框架,测试页面套用的若依的定时任务页面,在那基础上稍加改动就好
动态定时任务-支持选择执行类和自定义参数_第1张图片

1,项目依赖

    
    
        cn.hutool
        hutool-all
        5.8.12
    

2,定义一个定时任务类

@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;
}

3,编写定时任务

先写一个接口,所有定时任务都实现这个接口,方便我们遍历所有可用的定时任务类

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结构不一致,这个参数会在添加和修改定时任务时同下卡框一起带出来

4,定时任务的增删改查实现

后台也是在若依框架基础上测试的,使用的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实体备注就清楚了

5,增删改查接口

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());
    }
}

6,vue页面

若依原本使用的quartz框架,有一个并发选项,我这里完全不用到,只是没清理,其他的都有注释自己看吧





7,vue需要的js

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
  })
}

8,最后补上服务启动时启动开启状态的定时任务

@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;
    }
}

你可能感兴趣的:(实现案例,java,定时任务,动态,反射,自定义参数)