一、前置内容

若依定时任务的初始化

若依框架 Quartz :定时任务初始化源码全拆解(附图解)-CSDN博客

二、添加接口

 打开SysJobController

    /**
     * 新增定时任务
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:add')")
    @Log(title = "定时任务", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult 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(getUsername());
        return toAjax(jobService.insertJob(job));
    }

2.1 获取调用任务方法

这里的job.getInvokeTarget()就是获取调用方法

  • 一种是Bean调用:对象名.方法(对象要在IOC容器中)
  • 一种是Class类调用通过反射:包名.类名.方法(只支持本地)
  • 所以上述代码中所说的ldap,rmi,http这种远程调用协议不支持

2.3 违规字符串

    /**
     * 定时任务违规的字符
     */
    public static final String[] JOB_ERROR_STR = { "java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
            "org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config", "com.ruoyi.generator" 
  • java.net.URL 

    • 定时任务中如果允许直接构造 URL 并发起请求,可能被用来发起未授权的外部调用、数据泄露(比如把内部数据发送到外部恶意地址),甚至配合其他漏洞形成 SSRF(服务器端请求伪造)攻击。

  • javax.naming.InitialContext 
    • JNDI 是 Java 命名与目录接口,在定时任务中使用它可以被攻击者利用,通过构造恶意的 JNDI 链接(如指向恶意 LDAP 服务器),触发代码执行,这是 Java 生态中非常经典的高危漏洞利用方式。
  • org.yaml.snakeyaml 
    • SnakeYAML 是 YAML 解析库,而 YAML 格式支持反序列化。如果定时任务中允许解析外部可控的 YAML 数据,攻击者可以构造恶意的 YAML 内容,触发反序列化漏洞,直接在服务器上执行任意代码。
  • org.springframeworkorg.apache
    • ​​​​​​​这些是 Spring、Apache 等核心框架的包名。允许在定时任务中直接调用这些框架的底层 API,可能被用来绕过权限校验、修改系统配置,甚至直接执行危险的系统操作。
  • com.ruoyi.common.utils.file
    • ​​​​​​​这个包通常包含文件操作工具类,允许在定时任务中直接使用,可能被用来读取 / 写入敏感文件(如配置文件、数据库密码文件),甚至删除系统关键文件。
  • com.ruoyi.common.configcom.ruoyi.generator
    • ​​​​​​​这些是若依框架的核心配置和代码生成模块,直接调用可能被用来篡改系统配置、生成恶意代码并执行,破坏系统的完整性。

2.4 白名单

    /**
     * 检查包名是否为白名单配置
     * 
     * @param invokeTarget 目标字符串
     * @return 结果
     */
    public static boolean whiteList(String invokeTarget)
    {
        String packageName = StringUtils.substringBefore(invokeTarget, "(");
        int count = StringUtils.countMatches(packageName, ".");
        if (count > 1)
        {
            return StringUtils.startsWithAny(invokeTarget, Constants.JOB_WHITELIST_STR);
        }
        Object obj = SpringUtils.getBean(StringUtils.split(invokeTarget, ".")[0]);
        String beanPackageName = obj.getClass().getPackage().getName();
        return StringUtils.startsWithAny(beanPackageName, Constants.JOB_WHITELIST_STR)
                && !StringUtils.startsWithAny(beanPackageName, Constants.JOB_ERROR_STR);
    }
    /**
     * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
     */
    public static final String[] JOB_WHITELIST_STR = { "com.ruoyi.quartz.task" };
  • 从传入的任务目标invokeTarget中提取包名(有完整包名直接截取,无完整包名则通过 Spring Bean 反射获取)
  • 校验该包名是否在允许的白名单内(只能在com.ruoyi.quartz.task下),同时不在禁止的错误名单内
  • 校验通过返回true(允许执行任务),否则返回false(禁止执行)

2.5添加定时任务

打开SysJobServiceImpl

   /**
     * 新增任务
     * 
     * @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;
    }

添加到数据库中,并创建任务

创建任务的具体细节在初始化篇已经详细讲过

若依框架 Quartz :定时任务初始化源码全拆解(附图解)-CSDN博客

三、 更改任务状态接口

打开SysJobController

    /**
     * 定时任务状态修改
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping("/changeStatus")
    public AjaxResult changeStatus(@RequestBody SysJob job) throws SchedulerException
    {
        SysJob newJob = jobService.selectJobById(job.getJobId());
        newJob.setStatus(job.getStatus());
        return toAjax(jobService.changeStatus(newJob));
    }

打开SysJobServiceImpl

    /**
     * 任务调度状态修改
     * 
     * @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;
    }

3.1 恢复任务

    /**
     * 恢复任务
     * 
     * @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;
    }

先改数据库,再恢复调度

3.2 暂停任务

   /**
     * 暂停任务
     * 
     * @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;
    }

这里也是先更新数据库再暂停调度器

四、更新任务接口

    /**
     * 修改定时任务
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:edit')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult 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(getUsername());
        return toAjax(jobService.updateJob(job));
    }

前面部分和添加相似

    /**
     * 更新任务的时间表达式
     * 
     * @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);
    }

4.1 更新对比

这里采用了数据库直接更新,而定时任务选择先删除再重新创建一个

  • 调度器的任务配置结构复杂,除了基础信息,还包含触发器、监听器、执行器绑定等关联关系。
  • 直接更新调度器任务的部分属性(如 cron 表达式)虽然可行,但需要处理各种边界情况,且不同版本的调度器 API 兼容性差。
  • 采用 “先删后建” 的方式,相当于用全新的配置重建任务,能确保所有配置完全生效,避免因部分更新不彻底导致的任务异常。
对比维度 直接更新(数据库) 先删后建(调度器)
技术原理 基于数据库的 UPDATE 语句,直接修改原记录 先删除调度器中旧任务,再用新配置创建新任务
操作成本 低,仅需一次 SQL 操作 高,需要两次调度器 API 调用(删 + 建)
原子性保障 由数据库事务保证,天然支持回滚 依赖代码逻辑保证,若删除后创建失败会导致任务丢失
配置生效性 直接修改,立即生效 完全重建,所有配置 100% 生效,无残留问题
适用场景 任务基础信息(状态、描述等)的小幅修改 任务调度规则(cron 表达式、执行器)的大幅变更
代码复杂度 简单,调用 update 方法即可 稍复杂,需处理删除、创建的异常场景
风险点 无额外风险 若创建失败,会出现任务 “短暂消失” 的情况

五、立即执行接口

这个接口适合我们来做测试

    /**
     * 定时任务立即执行一次
     */
    @PreAuthorize("@ss.hasPermi('monitor:job:changeStatus')")
    @Log(title = "定时任务", businessType = BusinessType.UPDATE)
    @PutMapping("/run")
    public AjaxResult run(@RequestBody SysJob job) throws SchedulerException
    {
        boolean result = jobService.run(job);
        return result ? success() : error("任务不存在或已过期!");
    }
    /**
     * 立即运行任务
     * 
     * @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;
    }
  • 提取任务 ID / 分组,查询数据库获取任务完整配置
  • 封装任务参数,生成任务唯一标识
  • 校验任务是否存在,存在则立即触发执行
  • 返回触发是否成功的结果
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐