定时任务 XXL-Job
这篇解决什么
按点扫描超时单、补打卡、清日志,不要堵在 HTTP 请求里。读完能配执行器、写 @XxlJob Handler,并在管理端「定时任务运维」里对上卡片、手动跑一次。
调度用 XXL-Job 2.4.2。默认端口 48080,管理端前缀 /admin-api。本地 profile 默认关掉执行器,菜单里仍能发现并触发已扫描到的 Handler。
示意图:Cron 从调度中心进执行器;运维页直接反射调用同一批 Handler。
组件位置
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| Job Starter | 拉起 XxlJobSpringExecutor,同时打开 @EnableAsync | ruoyi-office/yudao-framework/yudao-spring-boot-starter-job/ |
| 自动配置 | xxl.job.enabled=true 时注册执行器 | .../quartz/config/YudaoXxlJobAutoConfiguration.java |
| 配置项 | enabled、accessToken、admin、executor | .../quartz/config/XxlJobProperties.java |
| 注册入口 | 与异步任务同一份 imports | .../META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 核心库 | xxl-job-core,BOM 2.4.2 | ruoyi-office/yudao-dependencies/pom.xml |
| 运维接口 | 启动扫描 @XxlJob,列表 + 手动触发 | ruoyi-office/yudao-server/.../controller/admin/JobOpsController.java |
| 运维页 | 卡片按模块筛选,点「立即执行」 | ruoyi-office-vben/apps/web-antd/src/views/infra/job-ops/index.vue |
| 运维 API | GET /infra/job-ops/list、POST /infra/job-ops/trigger | .../src/api/infra/job-ops/index.ts |
| 中文说明 | @JobDesc,运维页读 remark | yudao-framework/yudao-common/.../annotation/JobDesc.java |
| 多租户切面 | @TenantJob 按租户并行跑 | yudao-spring-boot-starter-biz-tenant/.../job/TenantJobAspect.java |
system、infra 以及多数业务 *-server 已引入 yudao-spring-boot-starter-job。新模块先加这个依赖,再在 job 包写 Handler。
菜单在基础设施 → 系统运维 → 定时任务运维。官方菜单 id:5259 目录,5260 页面。路由 /infra/operation/job-ops。按钮权限:infra:job-ops:query、infra:job-ops:trigger。列表接口未再标权限,登录即可;触发必须有 infra:job-ops:trigger。
配置与执行器
YudaoXxlJobAutoConfiguration 在 xxl.job.enabled=true(缺省也是 true)时创建 XxlJobSpringExecutor。属性来自 XxlJobProperties:调度中心地址、通讯 Token、执行器 AppName / IP / 端口 / 日志目录。
单体入口 application.yaml 只配执行器侧:
xxl:
job:
executor:
appname: ${spring.application.name}
logpath: ${user.home}/logs/xxl-job/${spring.application.name}
accessToken: default_tokenspring.application.name 是 yudao-server,调度中心里的 AppName 必须写成这个。accessToken 要和调度中心一致,默认 default_token。
本地 application-local.yaml 关掉执行器,只留调度中心地址:
xxl:
job:
enabled: false
admin:
addresses: http://127.0.0.1:9090/xxl-job-admin本地默认不注册执行器
enabled: false 时整份 YudaoXxlJobAutoConfiguration 不装,进程不会向 9090 报到。Cron 不会触发。要联调调度中心,改成 true,先起 XXL-Job Admin,再重启 yudao-server。
执行器端口默认 -1(随机)。日志目录默认 ${user.home}/logs/xxl-job/yudao-server,保留 30 天,-1 表示不删。启动成功时日志会出现 初始化 XXL-Job 执行器的配置,以及 register jobhandler success, name:demoJob。
调度中心「执行器管理」用自动注册。任务管理里新增任务时,JobHandler 填 @XxlJob("…") 的字符串,必须和代码里完全一致,例如 punchRecordGenerateJob。
示意图:两条入口打到同一个方法;运维页不经过调度中心。
写一个 Job
放在对应模块的 job 包,类加 @Component。方法上三个注解各管一件事:调度名、租户、运维说明。
@Component
public class DemoJob {
@XxlJob("demoJob")
@TenantJob
@JobDesc("示例任务,仅用于演示 XXL-Job 集成效果,无实际业务逻辑")
public void execute() {
System.out.println("美滋滋");
}
}| 名称 | 说明 | 仓库路径 |
|---|---|---|
@XxlJob | Handler 名,调度中心和运维页都靠它找方法 | com.xxl.job.core.handler.annotation.XxlJob |
@TenantJob | 只标在方法上,按租户并行执行 | .../tenant/core/job/TenantJob.java |
@JobDesc | 运维卡片上的中文用途 | DemoJob、PunchRecordGenerateJob 等 |
| 无参方法 | JobOps 直接 invoke | JobOpsController.triggerJob |
| 带参方法 | 运维页弹出参数框,传入 String | 同上,hasParam=true |
业务任务把逻辑放进 Service,Job 只当入口。打卡生成是这种:
@XxlJob("punchRecordGenerateJob")
@TenantJob
@JobDesc("每日为所有在职员工生成当天的空白打卡记录")
public String execute() {
return punchRecordService.generateDailyPunchRecords();
}| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 示例 | 无业务副作用 | yudao-module-system-server/.../job/demo/DemoJob.java |
| 打卡 | 每天给在职员工补空白记录 | yudao-module-hrm-server/.../attendance/PunchRecordGenerateJob.java |
| 支付通知 | 扫描待通知并回调;Service 里再加锁 | yudao-module-pay-server/.../notify/PayNotifyJob.java |
| 访问日志清理 | 物理删 14 天前,每批 100 条 | yudao-module-infra-server/.../logger/AccessLogCleanJob.java |
| 令牌清理 | 过期超过 14 天的 access / refresh | yudao-module-system-server/.../token/TokenCleanJob.java |
JobOpsController 启动时扫全部 Bean 的 @XxlJob。模块名取包路径 cn.iocoder.yudao.module.{模块} 的那一段。卡片标题先用类名去掉 Job;remark 读 @JobDesc,没有就提示补注解。
说明写在方法上
运维页不会去读类注释。新任务请同时写 @XxlJob 和 @JobDesc,否则卡片上只剩「暂无说明」。
需要回写调度日志时用 XxlJobHelper.handleSuccess / handleFail。TokenCleanJob 把清理条数拼进成功结果。运维页走反射,返回值会显示在提示里。
同一时刻只允许一条在跑的任务,不要只靠 Cron 错开,在 Service 里加分布式锁。支付通知的 key 是 pay_notify:lock:%d,见 分布式锁。
多租户
调度线程没有登录用户,也没有请求头 tenant-id。查带 tenant_id 的表时,必须自己进租户上下文。
@TenantJob 由 TenantJobAspect 切开:取出全部租户 id,parallelStream 里 TenantUtils.execute(tenantId, …) 再调原方法。某个租户抛错会记进该租户的结果,并 XxlJobHelper.handleFail。
查租户表必须加 @TenantJob
漏标时,Holder 是空的。MyBatis 租户插件会拦 SQL,或业务只打到「当前租户」(调度里通常没有)。TicketSlaCheckJob 只标了 @XxlJob,查工单前要自己保证租户上下文,不要照抄成默认写法。
跨租户清理用 @TenantIgnore,不要再用 @TenantJob 扫一遍。访问日志、错误日志、令牌清理都是这种:一次删全库过期行。
@XxlJob("accessLogCleanJob")
@TenantIgnore
@JobDesc("物理清理超过 14 天的接口访问日志,释放数据库空间")
public void execute() {
Integer count = apiAccessLogService.cleanAccessLog(14, 100);
}Handler 要幂等
@TenantJob 并行跑各租户。某个租户失败后,调度中心重试会整次再跑,已经成功的租户也会再进一遍。按天补记录、按 id 扫描,写成可重复执行。
多租户开关和 TenantBaseDO 见 SaaS 多租户。日志清理和访问日志表见 操作日志、访问日志、异常日志。
运维页
菜单打开后,页面把 /infra/job-ops/list 的结果铺成卡片。当前环境扫到 41 个 Handler,可按模块筛、按 Handler / 类名 / 说明搜。

本系统截图:基础设施 → 系统运维 → 定时任务运维。每张卡是模块、Handler、类名和「立即执行」。
「立即执行」走 POST /infra/job-ops/trigger?jobHandler=…。无参任务先确认;带参任务弹出文本框。Controller 按全类名取 Bean,再反射调用带该 @XxlJob 的方法。本地 xxl.job.enabled=false 时这条通路仍然可用。
| 名称 | 说明 | 仓库路径 |
|---|---|---|
| 元信息 | jobHandler / module / className / remark / hasParam | JobOpsController.JobMeta |
| 模块中文 | hrm→人力资源,infra→基础设施 | views/infra/job-ops/index.vue 的 moduleNameMap |
| 触发权限 | infra:job-ops:trigger | JobOpsController.triggerJob |
Cron 表达式在调度中心维护,运维页不存调度时间。常用写法:
| 名称 | 说明 | 仓库路径 |
|---|---|---|
0 0 12 * * ? | 每天中午 12 点 | 调度中心 Cron |
0 15 10 ? * MON-FRI | 工作日 10:15 | 同上 |
0 0/30 9-17 * * ? | 9 点到 17 点每半小时 | 同上 |
0 15 10 15 * ? | 每月 15 日 10:15 | 同上 |
0 15 10 L * ? | 每月最后一天 10:15 | 同上 |
Job 里不要写死自己的 Cron。改周期只动调度中心,避免发版。
@Async 和 Job 共用同一个 Starter。接口返回后再落库,走线程池,不要塞进 @XxlJob。见 异步任务。
配置与操作
运维在 基础设施 → 系统运维 → 定时任务运维(/infra/operation/job-ops,菜单 5260)看卡片、点立即执行。Cron 改 XXL-Job 调度中心,不要写死在 Handler 里。
本地 xxl.job.enabled 默认关,页面仍能扫到并手动跑。不要打开已删的 /infra/job。开启见 基建功能设计。截图见上文运维列表。
