简介:这份资源面向Java后端开发者与需要实现定时任务动态管理的项目团队,提供SSM框架整合Quartz并将任务配置与状态持久化到数据库的完整示例。包内共55个文件,以40个jar依赖包、7个xml配置、3个properties属性文件、2个jsp页面及少量class与mf元数据文件为主,压缩包约28.2MB,涵盖Spring、Spring MVC、MyBatis、Quartz、Shiro、C3P0等核心组件,目录结构清晰,便于按模块查阅。资源重点演示了通过SchedulerFactoryBean管理调度器、自定义Job作业类、MyBatis操作Quartz多张数据表,以及控制器接口实现任务的动态增删改查,并配有前端页面进行任务列表展示与表单管理。已有706人学习下载,适合希望掌握定时任务持久化方案、理解SSM与Quartz协作流程的开发者参考借鉴。
1. 从一次定时任务翻车说起:SSM 整合 Quartz 持久化到数据库到底解决什么问题
很多做 Java Web 的朋友都遇到过这种场景:项目里用@Scheduled注解写了个定时任务,本地跑得好好的,一上线就出幺蛾子——集群部署时两台机器同时触发,数据被重复处理;想临时停掉某个任务,得改代码重新打包发布;任务执行失败了,连个记录都查不到,只能翻日志大海捞针。我之前接手一个后台管理系统,里面七八个定时任务全是硬编码在 Spring 配置里的,每次调整执行时间都要走一遍发版流程,运维同事看到我就头疼。
这个 SSM 整合 Quartz 并持久化到数据库的项目,核心就是解决上面这些痛点。它把定时任务的定义、触发器、执行状态全部存进 MySQL,通过 SSM(Spring + SpringMVC + MyBatis)提供一套增删改查接口,让你在页面上就能动态管理任务——新增、暂停、恢复、立即执行、修改 Cron 表达式,统统不用重启服务。适合正在做后台管理系统、需要任务调度能力的开发者,尤其是那些被集群重复执行和任务状态不可见折磨过的同学。下面我从项目结构开始,一步步拆给你看。
2. 项目骨架与核心依赖:SSM 和 Quartz 是怎么拼在一起的
2.1 目录结构一览
先看整体目录,心里有个谱。这是一个标准的 Maven Web 项目结构,在 SSM 基础上增加了 Quartz 相关配置和任务管理模块:
src/main/java/com/example/schedule/ ├── controller/ │ └── JobController.java # 任务管理接口 ├── service/ │ ├── JobService.java │ └── impl/JobServiceImpl.java # 核心调度逻辑 ├── mapper/ │ └── JobMapper.java # MyBatis 映射接口 ├── entity/ │ └── ScheduleJob.java # 任务实体 ├── quartz/ │ └── MyJobFactory.java # 解决 Spring Bean 注入问题 └── util/ └── ScheduleUtils.java # 任务工具类 src/main/resources/ ├── spring/ │ ├── applicationContext.xml # Spring 核心配置 │ ├── spring-mvc.xml # MVC 配置 │ └── spring-quartz.xml # Quartz 整合配置 ├── mapper/ │ └── JobMapper.xml ├── quartz.properties # Quartz 参数 └── jdbc.properties这个结构里最容易被忽略的是quartz包下的MyJobFactory,后面会专门讲,它是解决 Job 中无法注入 Service 的关键。
2.2 Maven 依赖配置
依赖版本要匹配,Quartz 2.3.x 和 Spring 4.x/5.x 配合比较稳。核心依赖如下:
<!-- Quartz 调度核心 --> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency> <!-- Spring 整合 Quartz 的桥接包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.3.20</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.11</version> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency>spring-context-support这个包很多人会漏掉,它提供了SchedulerFactoryBean,是 Spring 和 Quartz 之间的粘合剂。没有它,你就得手动管理 Scheduler 生命周期,麻烦得很。
2.3 数据库表设计
Quartz 持久化需要一套自己的表来存 JobDetail、Trigger、Calendar 等信息。官方提供了建表 SQL,在 Quartz 发行包的docs/dbTables目录下能找到。以 MySQL 为例,核心表有 11 张,常用的几张:
| 表名 | 作用 |
|---|---|
| QRTZ_JOB_DETAILS | 存储 JobDetail 信息 |
| QRTZ_TRIGGERS | 存储 Trigger 基本信息 |
| QRTZ_CRON_TRIGGERS | 存储 Cron 类型的 Trigger |
| QRTZ_SIMPLE_TRIGGERS | 存储 Simple 类型的 Trigger |
| QRTZ_FIRED_TRIGGERS | 存储正在执行的 Trigger 状态 |
| QRTZ_SCHEDULER_STATE | 存储 Scheduler 实例状态 |
除了这些,还需要一张业务表来存任务描述信息,方便前端展示:
CREATE TABLE sys_schedule_job ( job_id BIGINT PRIMARY KEY AUTO_INCREMENT, job_name VARCHAR(100) NOT NULL COMMENT '任务名称', job_group VARCHAR(50) DEFAULT 'DEFAULT' COMMENT '任务分组', cron_expression VARCHAR(50) NOT NULL COMMENT 'cron表达式', bean_class VARCHAR(200) NOT NULL COMMENT '任务类全限定名', method_name VARCHAR(100) DEFAULT 'execute' COMMENT '执行方法名', status TINYINT DEFAULT 0 COMMENT '0正常 1暂停', remark VARCHAR(255) COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;bean_class和method_name这两个字段是动态调度的关键——通过反射调用指定类的指定方法,这样新增任务时只需要在页面上填类名和方法名,不用改代码。
2.4 Quartz 配置文件
quartz.properties里几个参数必须改,否则持久化不生效:
# 使用 JDBC 持久化 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX # 数据库代理类,MySQL 用这个 org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 表前缀,和建表 SQL 保持一致 org.quartz.jobStore.tablePrefix=QRTZ_ # 集群模式开启 org.quartz.jobStore.isClustered=true # 集群实例 ID 自动生成 org.quartz.scheduler.instanceId=AUTO # 线程池大小 org.quartz.threadPool.threadCount=10isClustered=true是集群部署的关键,它让多个节点通过数据库锁协调,避免同一个任务被重复触发。instanceId=AUTO让每个节点自动生成唯一 ID,不用手动配。
2.5 Spring 整合配置
spring-quartz.xml是整合的核心,把 SchedulerFactoryBean 交给 Spring 管理:
<bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <!-- 数据源,Quartz 用它读写任务表 --> <property name="dataSource" ref="dataSource"/> <!-- Quartz 配置文件位置 --> <property name="configLocation" value="classpath:quartz.properties"/> <!-- 覆盖已存在的任务 --> <property name="overwriteExistingJobs" value="true"/> <!-- 延迟启动,等 Spring 容器初始化完成 --> <property name="startupDelay" value="10"/> <!-- 自定义 JobFactory,解决 Bean 注入 --> <property name="jobFactory" ref="myJobFactory"/> <!-- 自动启动 --> <property name="autoStartup" value="true"/> </bean>startupDelay设 10 秒是为了等 Spring 容器完全初始化,否则任务启动时可能拿不到 Bean。jobFactory指向自定义工厂,这是下一个要讲的重点。
3. 核心模块拆解:从新增任务到触发执行的完整链路
3.1 任务实体与 Mapper
ScheduleJob实体对应sys_schedule_job表,字段和表结构一一对应:
public class ScheduleJob implements Serializable { private Long jobId; private String jobName; private String jobGroup; private String cronExpression; private String beanClass; // 全限定类名 private String methodName; // 执行方法 private Integer status; // 0正常 1暂停 private String remark; // getter/setter 省略 }Mapper 接口提供基本的增删改查:
public interface JobMapper { List<ScheduleJob> selectAll(); ScheduleJob selectById(Long jobId); int insert(ScheduleJob job); int update(ScheduleJob job); int deleteById(Long jobId); int updateStatus(@Param("jobId") Long jobId, @Param("status") Integer status); }对应的 XML 就是标准 CRUD,不展开。重点看 Service 层怎么把数据库操作和 Quartz 调度串起来。
3.2 自定义 JobFactory:解决 Bean 注入的玄学问题
这是整个项目最容易翻车的地方。Quartz 默认的JobFactory每次执行任务时直接newInstance()创建 Job 实例,不走 Spring 容器,导致 Job 里用@Autowired注入的 Service 全是 null。我当初在这卡了大半天,日志里只报空指针,根本看不出原因。
解决办法是自定义 JobFactory,让 Job 实例从 Spring 容器获取:
@Component public class MyJobFactory extends AdaptableJobFactory { @Autowired private AutowireCapableBeanFactory capableBeanFactory; @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { // 先调用父类创建实例 Object jobInstance = super.createJobInstance(bundle); // 再通过 Spring 完成依赖注入 capableBeanFactory.autowireBean(jobInstance); return jobInstance; } }AdaptableJobFactory是 Quartz 提供的适配器,createJobInstance是创建 Job 实例的入口。关键在autowireBean这行,它让 Spring 对实例进行属性注入,这样 Job 里的@Autowired才能生效。配置里jobFactory指向这个 Bean,整个链路就通了。
3.3 动态任务的执行入口
所有动态任务统一走一个 Job 类,通过反射调用目标方法:
public class DynamicJob implements Job { @Override public void execute(JobExecutionContext context) throws JobExecutionException { // 从 JobDataMap 取出任务信息 JobDataMap dataMap = context.getJobDetail().getJobDataMap(); String beanClass = dataMap.getString("beanClass"); String methodName = dataMap.getString("methodName"); try { // 反射获取目标 Bean Object targetBean = SpringContextHolder.getBean( Class.forName(beanClass)); // 反射调用方法 Method method = targetBean.getClass() .getMethod(methodName); method.invoke(targetBean); } catch (Exception e) { throw new JobExecutionException(e); } } }JobDataMap是 Quartz 提供的传参机制,任务创建时把beanClass和methodName塞进去,执行时取出来。SpringContextHolder是一个工具类,实现ApplicationContextAware接口,持有 Spring 上下文,方便在非 Spring 管理的类里获取 Bean。
3.4 新增任务的完整流程
Service 层新增任务时,要同时操作数据库和 Quartz 调度器:
@Override @Transactional public void addJob(ScheduleJob job) throws Exception { // 1. 先入库 jobMapper.insert(job); // 2. 构建 JobDetail JobDetail jobDetail = JobBuilder.newJob(DynamicJob.class) .withIdentity(job.getJobName(), job.getJobGroup()) .usingJobData("beanClass", job.getBeanClass()) .usingJobData("methodName", job.getMethodName()) .build(); // 3. 构建 Trigger CronTrigger trigger = TriggerBuilder.newTrigger() .withIdentity(job.getJobName(), job.getJobGroup()) .withSchedule(CronScheduleBuilder .cronSchedule(job.getCronExpression())) .build(); // 4. 交给调度器 scheduler.scheduleJob(jobDetail, trigger); // 5. 如果状态是暂停,立即暂停 if (job.getStatus() == 1) { scheduler.pauseJob(JobKey.jobKey( job.getJobName(), job.getJobGroup())); } }withIdentity用任务名和分组作为唯一标识,重复添加会报错。usingJobData把反射需要的信息存进 JobDataMap。scheduleJob是真正把任务注册到调度器的方法,执行后任务就开始按 Cron 表达式触发。
3.5 暂停、恢复与删除
暂停和恢复操作比较简单,直接调调度器 API:
// 暂停 public void pauseJob(Long jobId) throws SchedulerException { ScheduleJob job = jobMapper.selectById(jobId); scheduler.pauseJob(JobKey.jobKey( job.getJobName(), job.getJobGroup())); jobMapper.updateStatus(jobId, 1); } // 恢复 public void resumeJob(Long jobId) throws SchedulerException { ScheduleJob job = jobMapper.selectById(jobId); scheduler.resumeJob(JobKey.jobKey( job.getJobName(), job.getJobGroup())); jobMapper.updateStatus(jobId, 0); } // 删除 public void deleteJob(Long jobId) throws SchedulerException { ScheduleJob job = jobMapper.selectById(jobId); scheduler.deleteJob(JobKey.jobKey( job.getJobName(), job.getJobGroup())); jobMapper.deleteById(jobId); }注意删除时要先删调度器里的任务,再删数据库记录。反过来如果先删数据库,调度器里的任务还在跑,就变成孤儿任务了。
3.6 修改 Cron 表达式
修改执行时间需要先删旧 Trigger 再建新 Trigger:
public void updateCron(Long jobId, String cron) throws Exception { ScheduleJob job = jobMapper.selectById(jobId); // 构建新的 Trigger CronTrigger newTrigger = TriggerBuilder.newTrigger() .withIdentity(job.getJobName(), job.getJobGroup()) .withSchedule(CronScheduleBuilder.cronSchedule(cron)) .build(); // 替换旧 Trigger scheduler.rescheduleJob( TriggerKey.triggerKey(job.getJobName(), job.getJobGroup()), newTrigger); // 更新数据库 job.setCronExpression(cron); jobMapper.update(job); }rescheduleJob是 Quartz 提供的原子替换方法,比先删后建安全,中间不会出现任务丢失的窗口期。
4. 避坑指南:动态任务调度中容易踩的五个坑
4.1 任务重复执行
现象:集群部署后,同一个任务在两台机器上同时触发,数据被处理两次。
原因:quartz.properties里isClustered没设为 true,或者数据库表没建全,导致节点间无法协调。
解决:确认org.quartz.jobStore.isClustered=true,并且QRTZ_SCHEDULER_STATE表存在。集群模式下 Quartz 通过数据库行锁保证同一时刻只有一个节点触发任务。
4.2 Job 中注入 Service 为 null
现象:Job 的execute方法里调用 Service 报空指针,但 Service 本身在 Controller 里用得好好的。
原因:Quartz 默认 JobFactory 不走 Spring 容器,直接 new 实例。
解决:配置自定义MyJobFactory,在SchedulerFactoryBean里指定jobFactory属性。这个坑前面专门讲过,不再赘述。
4.3 Cron 表达式写错导致任务不触发
现象:任务添加成功,但从来不执行,日志里也没有报错。
原因:Cron 表达式格式错误,比如把0 0/5 * * * ?写成0 0/5 * * * *,最后一位应该是?而不是*。
解决:用在线 Cron 生成器验证表达式,或者在代码里加校验:
try { new CronExpression(cron); } catch (ParseException e) { throw new IllegalArgumentException("Cron表达式不合法"); }4.4 任务执行时间过长导致阻塞
现象:某个任务执行时间超过触发间隔,后续触发被阻塞,日志里出现Misfire相关警告。
原因:Quartz 默认线程池大小是 10,如果任务耗时长且并发高,线程不够用。
解决:调大org.quartz.threadPool.threadCount,同时给 Trigger 设置withMisfireHandlingInstructionDoNothing(),让错过的触发直接跳过,不补执行。
4.5 数据库连接池耗尽
现象:运行一段时间后,任务不再触发,日志报Couldn't get a connection。
原因:Quartz 自己管理一套数据库连接,如果和业务共用数据源且连接池配置过小,容易被占满。
解决:给 Quartz 单独配一个数据源,或者把 Druid 的maxActive调大。我一般会在SchedulerFactoryBean里单独指定一个dataSource,和业务数据源隔离。
5. 二次开发与扩展:从能跑到好用的几个改造方向
5.1 任务执行日志记录
项目默认只存任务定义,不存执行记录。实际用起来,你肯定想知道每个任务什么时候执行了、成功还是失败、耗时多久。改造思路是新建一张sys_job_log表,在DynamicJob.execute里记录:
public class DynamicJob implements Job { @Override public void execute(JobExecutionContext context) { long start = System.currentTimeMillis(); JobLog log = new JobLog(); log.setJobName(context.getJobDetail().getKey().getName()); log.setStartTime(new Date(start)); try { // 反射调用... log.setStatus(0); } catch (Exception e) { log.setStatus(1); log.setErrorMsg(e.getMessage()); } finally { log.setCostTime(System.currentTimeMillis() - start); jobLogMapper.insert(log); } } }这样每次执行都有记录,排查问题不用再翻服务器日志。注意日志表要定期清理,不然数据量涨得很快。
5.2 支持手动触发一次
有时候任务没到执行时间,但你想立即跑一次验证逻辑。Quartz 提供了triggerJob方法:
public void runOnce(Long jobId) throws SchedulerException { ScheduleJob job = jobMapper.selectById(jobId); scheduler.triggerJob(JobKey.jobKey( job.getJobName(), job.getJobGroup())); }这个操作不会影响原有的触发计划,只是额外触发一次。前端加个「立即执行」按钮,调试时特别方便。
5.3 任务分组的界面化展示
job_group字段目前只是存着,没在界面上体现。可以在 Controller 里按分组聚合返回:
@GetMapping("/list") public Result list() { List<ScheduleJob> jobs = jobService.selectAll(); Map<String, List<ScheduleJob>> grouped = jobs.stream() .collect(Collectors.groupingBy(ScheduleJob::getJobGroup)); return Result.success(grouped); }前端按分组渲染成折叠面板,任务多了也不会乱。
5.4 验证改造是否生效
改完代码后,按这个顺序验证一遍:先启动项目,看控制台有没有Scheduler started的输出;然后调新增接口创建一个每分钟执行的任务,观察日志里是否每分钟打印一次;再调暂停接口,确认日志停止输出;最后调恢复接口,确认日志重新出现。这套流程走下来,基本能覆盖核心链路。
我一般会在DynamicJob里加一行log.info("任务执行: {}", jobName),方便肉眼确认。等确认没问题了再删掉,换成正式的日志记录。
从那以后我每次做定时任务,都强制走一遍「集群配置检查 → JobFactory 验证 → Cron 表达式校验」这三步,再也没出现过重复执行和注入失败的问题。希望帮到你。
本文还有配套的精品资源,点击获取