若依框架 Quartz :定时任务接口源码全拆解
·
一、前置内容
若依定时任务的初始化
若依框架 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.springframework、org.apache这些是 Spring、Apache 等核心框架的包名。允许在定时任务中直接调用这些框架的底层 API,可能被用来绕过权限校验、修改系统配置,甚至直接执行危险的系统操作。
com.ruoyi.common.utils.file这个包通常包含文件操作工具类,允许在定时任务中直接使用,可能被用来读取 / 写入敏感文件(如配置文件、数据库密码文件),甚至删除系统关键文件。
com.ruoyi.common.config、com.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 / 分组,查询数据库获取任务完整配置
- 封装任务参数,生成任务唯一标识
- 校验任务是否存在,存在则立即触发执行
- 返回触发是否成功的结果
更多推荐


所有评论(0)