SpringBoot整合Quartz实战:从建表到动态任务管理

news2026/3/13 17:23:07
1. 为什么你需要Quartz从“一次性”到“动态化”的调度进化如果你用过SpringBoot自带的Scheduled注解那你肯定知道它有多方便。加个注解配个cron表达式任务就能定时跑了。但用久了痛点就来了所有任务都得写在代码里改个执行时间就得重新打包部署任务状态没法持久化服务一重启谁在跑、跑到哪了全丢了更别提想动态地新增、修改或者暂停某个任务了基本没戏。这时候一个更专业的调度框架就该登场了。Quartz就是干这个的。它是个功能强大的开源作业调度库你可以把它想象成一个高度可定制的“闹钟中心”。它不仅能处理简单的“每隔X秒响一次”还能处理复杂的“每周一三五早上9点但节假日除外”这种场景。而它最核心的竞争力就是持久化和动态管理。通过把任务和触发器的信息存到数据库里任务调度就变成了有状态的、可追溯的、可集群的。你的服务重启了没关系Quartz从数据库里把任务状态读出来该接着跑的接着跑。你想在管理后台临时加个数据清理任务通过几行API调用就能动态创建完全不用动线上代码。SpringBoot官方提供了spring-boot-starter-quartz这让整合变得异常简单。但很多教程只讲到“能用”离“好用”和“实用”还差得远。今天我就结合自己趟过的坑带你从零开始把Quartz的数据库持久化配通并实现一套真正能在生产环境用的动态任务管理功能。咱们不玩虚的直接上干货。2. 项目搭建与核心配置让Quartz“记住”你的任务2.1 引入依赖与基础认知第一步在pom.xml里加入依赖。这里有个小细节我们通常会把spring-boot-starter-quartz和数据库驱动、连接池比如HikariCP一起引入因为持久化离不开它们。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-quartz/artifactId /dependency !-- 根据你的数据库选择例如MySQL -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId /dependency引入这个starter后SpringBoot会自动为我们配置一个Scheduler调度器实例。但默认配置是内存式的RAMJobStore我们要做的就是通过配置文件把它“扳”到数据库模式JobStoreTX。2.2 详解application.yml配置连接数据库的关键配置文件是Quartz持久化的灵魂。下面这份配置是我在多个项目中提炼出来的加了详细注释你几乎可以照搬。spring: datasource: url: jdbc:mysql://localhost:3306/your_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password hikari: # ... 你的连接池配置 quartz: # 关键指定使用JDBC进行持久化 job-store-type: jdbc # 是否等待任务完成后关闭调度器生产环境建议true wait-for-jobs-to-complete-on-shutdown: true # 覆盖应用级别的数据源明确指定Quartz使用哪个数据源非必须但清晰 jdbc: initialize-schema: never # 我们手动执行SQL建表所以设为never properties: org: quartz: scheduler: instanceName: MyClusteredScheduler # 调度器实例名集群中需唯一 instanceId: AUTO # 实例ID自动生成集群部署时很重要 jobStore: class: org.quartz.impl.jdbcjobstore.JobStoreTX # 使用JDBC事务性存储 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # MySQL标准委托类 tablePrefix: QRTZ_ # 表前缀默认就是这个 isClustered: true # 开启集群支持多实例时避免任务重复执行 clusterCheckinInterval: 15000 # 集群节点检入间隔毫秒 useProperties: false # 序列化策略false表示用Java序列化true则用字符串需注意类型安全 threadPool: class: org.quartz.simpl.SimpleThreadPool # 线程池实现类 threadCount: 10 # 工作线程数根据任务量调整 threadPriority: 5 # 线程优先级我来解释几个容易踩坑的点job-store-type: jdbc这是开关告诉Quartz用数据库存任务。driverDelegateClass这个必须和你的数据库匹配。用MySQL就是StdJDBCDelegate用PostgreSQL是PostgreSQLDelegateOracle是OracleDelegate。配错了会报各种奇怪的SQL语法错误。isClustered: true即使你现在是单机我也建议先打开。这会让Quartz使用行锁来保证任务在集群环境下的唯一执行为未来扩容留好余地。initialize-schema: neverSpringBoot虽然支持自动建表always或embedded但我强烈建议手动执行SQL。自动建表可能因为数据库方言问题失败而且你无法控制表结构细节。2.3 获取并执行建表SQL打好地基这是持久化的物理基础——数据库表。Quartz官网和GitHub仓库提供了所有主流数据库的建表脚本。我以最常用的MySQL为例告诉你最稳的获取路径。别去网上搜那些来历不明的SQL了直接去Quartz的GitHub官方仓库https://github.com/quartz-scheduler/quartz。进去后找到并进入docs/dbTables目录。你会看到一堆SQL文件比如tables_mysql.sql。点开它复制全部内容。这个脚本会创建11张以QRTZ_开头的表核心的有QRTZ_JOB_DETAILS存储Job的详细信息哪个类什么参数。QRTZ_TRIGGERS存储触发器的基本信息关联哪个Job状态是什么。QRTZ_CRON_TRIGGERS/QRTZ_SIMPLE_TRIGGERS分别存储Cron触发器和简单触发器的特有参数。QRTZ_FIRED_TRIGGERS正在执行的触发器信息用于故障恢复和集群协调。QRTZ_LOCKS集群环境下用到的锁表。拿到SQL后在你的业务数据库里执行一遍。执行成功后这11张表就是Quartz的“记忆中枢”了。以后所有任务的生命周期都会在这些表里留下记录。3. 核心实战设计动态任务管理的数据模型与API配置好了表建好了接下来就是重头戏写代码实现动态管理。我们的目标是通过一个Service接收前端或内部调用传来的参数就能随时创建、暂停、恢复或删除一个定时任务。3.1 设计参数封装类理解Quartz的表结构要动态创建首先得知道创建时需要哪些信息。最好的方式就是对照Quartz的几张核心表来设计我们的参数类。这样设计出来的模型最贴近Quartz的本质。首先是所有触发器都有的基础信息对应QRTZ_TRIGGERS表import lombok.Getter; import lombok.Setter; import javax.validation.constraints.NotEmpty; Getter Setter public class TriggerBaseParam { /** 调度器名称通常和配置里的instanceName一致 */ private String schedName; /** 触发器名称 (必须) */ NotEmpty(message 触发器名称不能为空) private String triggerName; /** 触发器分组 (必须) */ NotEmpty(message 触发器分组不能为空) private String triggerGroup; /** 关联的Job名称 (必须) */ NotEmpty(message Job名称不能为空) private String jobName; /** 关联的Job分组 (必须) */ NotEmpty(message Job分组不能为空) private String jobGroup; /** 任务描述 */ private String description; /** 触发器类型CRON 或 SIMPLE (必须) */ NotEmpty(message 触发器类型不能为空) private String triggerType; /** 任务开始时间时间戳 */ private Long startTime; /** 任务结束时间时间戳 */ private Long endTime; /** 优先级 */ private Integer priority 5; }然后是创建任务时需要扩展的参数它包含了具体执行类的信息以及CRON和SIMPLE触发器特有的参数Getter Setter public class TriggerCreateParam extends TriggerBaseParam { /** 核心任务执行类的全限定名 (必须) */ NotEmpty(message 任务执行类不能为空) private String jobClassName; /** CRON触发器专属参数 */ private String cronExpression; /** SIMPLE触发器专属参数 */ // 重复间隔秒 private Integer repeatInterval; // 重复次数0表示无限重复 private Integer repeatCount 0; /** 任务参数可选的扩展数据 */ private MapString, Object jobDataMap; }为什么这么设计你看TriggerBaseParam里的字段几乎都能在QRTZ_TRIGGERS表里找到对应列。而TriggerCreateParam里的jobClassName决定了任务具体做什么cronExpression或repeatInterval/repeatCount则决定了任务何时触发。这种设计让我们的API既清晰又和底层存储强关联。3.2 实现动态创建方法一行代码创建一个任务有了参数模型我们就可以编写核心的创建方法了。这个方法会做几件事校验参数、反射加载任务类、构建JobDetail和Trigger最后交给Scheduler。import org.quartz.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; Service public class DynamicQuartzService { Autowired private Scheduler scheduler; // SpringBoot会自动注入 /** * 动态创建并调度一个任务 * param param 创建参数 * throws SchedulerException 调度异常 * throws ClassNotFoundException 任务类找不到 */ public void createJob(TriggerCreateParam param) throws SchedulerException, ClassNotFoundException { // 1. 参数基础校验 if (param null || !StringUtils.hasText(param.getJobClassName())) { throw new IllegalArgumentException(创建参数或任务类名无效); } // 2. 校验任务类是否存在且实现了Job接口 Class? extends Job jobClass; try { Class? clazz Class.forName(param.getJobClassName()); if (!Job.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException(任务类必须实现 org.quartz.Job 接口); } jobClass (Class? extends Job) clazz; } catch (ClassNotFoundException e) { throw new ClassNotFoundException(未找到任务执行类: param.getJobClassName(), e); } // 3. 构建 JobDetail (任务详情) JobBuilder jobBuilder JobBuilder.newJob(jobClass) .withIdentity(param.getJobName(), param.getJobGroup()) .withDescription(param.getDescription()) .storeDurably(); // 设置任务持久化即使没有触发器关联也不会被删除 // 添加任务参数JobDataMap if (param.getJobDataMap() ! null !param.getJobDataMap().isEmpty()) { jobBuilder.usingJobData(new JobDataMap(param.getJobDataMap())); } JobDetail jobDetail jobBuilder.build(); // 4. 构建 Trigger (触发器) TriggerBuilderTrigger triggerBuilder TriggerBuilder.newTrigger() .withIdentity(param.getTriggerName(), param.getTriggerGroup()) .forJob(jobDetail) // 关联上面创建的Job .withDescription(param.getDescription()); // 设置开始和结束时间 if (param.getStartTime() ! null param.getStartTime() 0) { triggerBuilder.startAt(new Date(param.getStartTime())); } else { triggerBuilder.startNow(); // 默认立即开始 } if (param.getEndTime() ! null param.getEndTime() 0) { triggerBuilder.endAt(new Date(param.getEndTime())); } // 5. 根据触发器类型构建不同的调度策略 (ScheduleBuilder) ScheduleBuilder? scheduleBuilder; String triggerType param.getTriggerType().toUpperCase(); switch (triggerType) { case CRON: if (!StringUtils.hasText(param.getCronExpression())) { throw new IllegalArgumentException(CRON触发器必须提供cron表达式); } // 这里可以增加cron表达式合法性校验 scheduleBuilder CronScheduleBuilder.cronSchedule(param.getCronExpression()); break; case SIMPLE: if (param.getRepeatInterval() null || param.getRepeatInterval() 0) { throw new IllegalArgumentException(SIMPLE触发器必须提供有效的重复间隔(0)); } Integer count param.getRepeatCount(); if (count null || count 0) { throw new IllegalArgumentException(重复次数必须0 (0表示无限)); } SimpleScheduleBuilder simpleBuilder SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(param.getRepeatInterval()); if (count 0) { simpleBuilder.repeatForever(); } else { simpleBuilder.withRepeatCount(count - 1); // Quartz的重复次数包含第一次执行 } scheduleBuilder simpleBuilder; break; default: throw new IllegalArgumentException(不支持的触发器类型: triggerType 仅支持 CRON 或 SIMPLE); } // 6. 组装触发器并提交给调度器 Trigger trigger triggerBuilder.withSchedule(scheduleBuilder).build(); scheduler.scheduleJob(jobDetail, trigger); // 可选如果调度器是暂停的这里可以调用 scheduler.start() 确保任务被调度 } }这段代码有几个关键点我实测下来特别重要storeDurably(true)这个设置保证了即使这个Job暂时没有Trigger关联比如你把触发器删了JobDetail也不会从数据库里被清除。这对于需要复用Job定义的场景很有用。重复次数的计算SimpleScheduleBuilder的withRepeatCount(count)参数指的是额外的重复次数。比如你想总共执行5次那么这里应该传4。我在代码里做了处理让用户直接传期望的总次数0表示无限内部再减1这样更符合直觉。异常处理对类名、参数做了严格校验并在异常信息里给出明确提示方便调试。3.3 编写你的第一个Job执行类任务创建好了总得有个地方写具体的业务逻辑。这就是Job接口的实现类。import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 一个示例任务清理临时文件 */ public class TempFileCleanupJob implements Job { private static final Logger logger LoggerFactory.getLogger(TempFileCleanupJob.class); Override public void execute(JobExecutionContext context) throws JobExecutionException { // 1. 可以从上下文获取任务参数 JobDataMap dataMap context.getJobDetail().getJobDataMap(); String tempDirPath dataMap.getString(tempDir); // 获取创建任务时传入的参数 int retentionDays dataMap.getIntValue(retentionDays); logger.info(开始执行临时文件清理任务目录{}保留天数{}, tempDirPath, retentionDays); // 2. 这里写下你的实际业务逻辑例如 // File tempDir new File(tempDirPath); // ... 清理旧文件的逻辑 ... // 3. 模拟一个执行结果 int deletedFiles 15; // 假设删除了15个文件 logger.info(临时文件清理完成共删除 {} 个文件, deletedFiles); // 你可以把结果放回上下文供监听器使用 context.setResult(deletedFiles); } }这个类很简单但体现了Quartz Job的核心实现execute方法。所有动态创建的任务最终都会指向这样一个具体的类。通过JobExecutionContext你能拿到任务的身份信息、触发时间、以及创建时传入的jobDataMap这让任务逻辑非常灵活。4. 动态任务管理进阶暂停、恢复、删除与查询只会创建还不够一个完整的动态管理系统必须能对任务进行全生命周期管理。4.1 暂停与恢复触发器让任务临时“休息”有时候你可能需要临时关闭某个任务比如系统维护但又不想删除它因为以后还要用。这时就需要暂停和恢复功能。public class DynamicQuartzService { // ... 接上面的代码 /** * 暂停一个触发器任务将不再触发 */ public void pauseTrigger(String triggerName, String triggerGroup) throws SchedulerException { TriggerKey triggerKey TriggerKey.triggerKey(triggerName, triggerGroup); scheduler.pauseTrigger(triggerKey); logger.info(已暂停触发器: {}.{}, triggerGroup, triggerName); } /** * 恢复一个触发器任务将按照原计划继续触发 */ public void resumeTrigger(String triggerName, String triggerGroup) throws SchedulerException { TriggerKey triggerKey TriggerKey.triggerKey(triggerName, triggerGroup); scheduler.resumeTrigger(triggerKey); logger.info(已恢复触发器: {}.{}, triggerGroup, triggerName); } /** * 暂停整个Job它关联的所有触发器都会暂停 */ public void pauseJob(String jobName, String jobGroup) throws SchedulerException { JobKey jobKey JobKey.jobKey(jobName, jobGroup); scheduler.pauseJob(jobKey); logger.info(已暂停Job及其所有触发器: {}.{}, jobGroup, jobName); } /** * 恢复整个Job */ public void resumeJob(String jobName, String jobGroup) throws SchedulerException { JobKey jobKey JobKey.jobKey(jobName, jobGroup); scheduler.resumeJob(jobKey); logger.info(已恢复Job及其所有触发器: {}.{}, jobGroup, jobName); } }这里要注意pauseTrigger和pauseJob的区别。一个Job比如“发送日报”可以关联多个触发器比如“早上9点发”和“下午5点发”。暂停触发器只影响那一个触发规则暂停Job则会让这个任务的所有触发规则都失效。根据你的业务场景选择。4.2 删除任务与触发器彻底清理如果某个任务再也不需要了就应该删除它以释放资源。删除也有两种粒度删除触发器或者连JobDetail一起删除。public boolean deleteJob(String jobName, String jobGroup) throws SchedulerException { JobKey jobKey JobKey.jobKey(jobName, jobGroup); // 第二个参数为true表示如果这个Job还有关联的触发器也一并删除 boolean result scheduler.deleteJob(jobKey); if (result) { logger.info(已删除Job及其关联的触发器: {}.{}, jobGroup, jobName); } else { logger.warn(删除Job失败可能不存在: {}.{}, jobGroup, jobName); } return result; } public boolean unscheduleJob(String triggerName, String triggerGroup) throws SchedulerException { TriggerKey triggerKey TriggerKey.triggerKey(triggerName, triggerGroup); // 只移除触发器不删除JobDetail boolean result scheduler.unscheduleJob(triggerKey); if (result) { logger.info(已移除触发器: {}.{}对应的JobDetail仍保留, triggerGroup, triggerName); } return result; }deleteJob是更彻底的操作通常用于任务完全下线。unscheduleJob则更温和只是取消调度JobDetail还留在数据库里以后可以用新的触发器重新关联它。4.3 查询与监控掌握任务状态管理后台总得有个列表页展示所有任务吧这就需要查询功能。我们可以直接从注入的Scheduler里获取信息也可以去查我们建的那11张数据库表。这里展示通过Scheduler API查询的方式更实时。public ListMapString, Object getAllJobs() throws SchedulerException { ListMapString, Object jobList new ArrayList(); // 获取所有Job组名 for (String groupName : scheduler.getJobGroupNames()) { // 获取该组下的所有JobKey for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(groupName))) { MapString, Object jobInfo new HashMap(); jobInfo.put(jobName, jobKey.getName()); jobInfo.put(jobGroup, jobKey.getGroup()); JobDetail jobDetail scheduler.getJobDetail(jobKey); jobInfo.put(description, jobDetail.getDescription()); jobInfo.put(jobClass, jobDetail.getJobClass().getName()); // 获取这个Job关联的所有触发器 List? extends Trigger triggers scheduler.getTriggersOfJob(jobKey); ListMapString, Object triggerInfos new ArrayList(); for (Trigger trigger : triggers) { MapString, Object triggerInfo new HashMap(); TriggerKey triggerKey trigger.getKey(); triggerInfo.put(triggerName, triggerKey.getName()); triggerInfo.put(triggerGroup, triggerKey.getGroup()); triggerInfo.put(triggerState, scheduler.getTriggerState(triggerKey).toString()); triggerInfo.put(nextFireTime, trigger.getNextFireTime()); triggerInfo.put(previousFireTime, trigger.getPreviousFireTime()); if (trigger instanceof CronTrigger) { triggerInfo.put(cronExpression, ((CronTrigger) trigger).getCronExpression()); triggerInfo.put(type, CRON); } else if (trigger instanceof SimpleTrigger) { triggerInfo.put(repeatInterval, ((SimpleTrigger) trigger).getRepeatInterval()); triggerInfo.put(repeatCount, ((SimpleTrigger) trigger).getRepeatCount()); triggerInfo.put(type, SIMPLE); } triggerInfos.add(triggerInfo); } jobInfo.put(triggers, triggerInfos); jobList.add(jobInfo); } } return jobList; }这个方法返回的数据结构已经足够丰富可以让你在前端画出一个清晰的任务管理列表展示每个任务的状态、下次执行时间、触发器类型等关键信息。5. 生产环境必备监听器、异常处理与集群考量功能跑通只是第一步要上生产环境还得考虑健壮性和可观测性。5.1 使用监听器监控任务生命周期Quartz提供了强大的监听器Listener机制让你能在任务执行的关键节点插入自定义逻辑比如记录日志、发送告警、统计耗时。最常用的是JobListener它可以监听Job的执行开始、结束和是否被否决Veto。import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.JobListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; Component public class GlobalJobExecutionListener implements JobListener { private static final Logger logger LoggerFactory.getLogger(GlobalJobExecutionListener.class); Override public String getName() { return GlobalJobExecutionListener; } // Job即将被执行时 Override public void jobToBeExecuted(JobExecutionContext context) { String jobKey context.getJobDetail().getKey().toString(); logger.info(任务 [{}] 开始执行触发时间: {}, jobKey, context.getFireTime()); // 可以在这里记录开始时间用于后续计算耗时 context.put(startTime, System.currentTimeMillis()); } // Job执行被否决时例如触发器被暂停了 Override public void jobExecutionVetoed(JobExecutionContext context) { logger.warn(任务 [{}] 执行被否决, context.getJobDetail().getKey()); } // Job执行完毕后 Override public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { String jobKey context.getJobDetail().getKey().toString(); Long startTime (Long) context.get(startTime); long cost startTime ! null ? System.currentTimeMillis() - startTime : 0; if (jobException ! null) { // 任务执行出错了 logger.error(任务 [{}] 执行失败耗时: {}ms, 异常: {}, jobKey, cost, jobException.getMessage(), jobException); // 这里可以接入你的告警系统发邮件、发钉钉等 // alertService.sendAlert(Quartz任务执行失败, jobKey, jobException); } else { // 任务执行成功 Object result context.getResult(); logger.info(任务 [{}] 执行成功耗时: {}ms, 结果: {}, jobKey, cost, result); } } }写好了监听器还需要把它注册到Scheduler上。可以在配置类里做Configuration public class QuartzConfig { Autowired private GlobalJobExecutionListener globalJobListener; Autowired private Scheduler scheduler; PostConstruct public void addListeners() throws SchedulerException { // 添加全局Job监听器监听所有Job scheduler.getListenerManager().addJobListener(globalJobListener); // 你也可以添加TriggerListener、SchedulerListener } }有了监听器所有任务的执行情况就尽在掌握了。出问题时能第一时间收到通知排查日志也一目了然。5.2 异常处理与任务恢复策略任务执行难免会出错。Quartz提供了misfire错失触发处理机制。简单说就是本该在某个时间点触发的任务因为调度器关闭、线程池满、或者任务本身执行时间过长错过了下一次触发时间这时候该怎么处理我们在创建触发器时可以指定misfire策略。这是一个非常实用的高级特性。// 在创建CRON触发器时 CronScheduleBuilder cronScheduleBuilder CronScheduleBuilder.cronSchedule(cronExpression); // 设置错失触发后的处理策略以当前时间立刻触发一次然后按原计划继续 cronScheduleBuilder.withMisfireHandlingInstructionFireAndProceed(); // 或者忽略错过的触发等待下一次触发更常用 // cronScheduleBuilder.withMisfireHandlingInstructionDoNothing(); Trigger cronTrigger triggerBuilder.withSchedule(cronScheduleBuilder).build(); // 在创建SIMPLE触发器时 SimpleScheduleBuilder simpleScheduleBuilder SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(interval) .withRepeatCount(count); // 错失触发后立即触发并将错过的次数补上慎用可能引发雪崩 simpleScheduleBuilder.withMisfireHandlingInstructionFireNow(); // 或者忽略错过的从现在开始重新计算下次触发时间推荐 // simpleScheduleBuilder.withMisfireHandlingInstructionIgnoreMisfires(); Trigger simpleTrigger triggerBuilder.withSchedule(simpleScheduleBuilder).build();选择哪种策略取决于你的业务。对于每天一次的报表任务错过一次可能用FireAndProceed立刻补跑一次就行。对于每秒都在跑的监控任务错过一大堆用IgnoreMisfires忽略掉直接开始下一个周期可能更合适。FireNow补跑所有错过的次数在间隔短、错过多的情况下可能导致系统瞬间压力过大。5.3 集群部署下的注意事项当你把应用部署到多个节点时Quartz的集群模式就派上用场了。我们之前在配置里已经打开了isClustered: true这会让Quartz通过数据库的行锁来协调各个节点保证同一个任务在集群中只会被一个节点执行。集群配置有几个关键点相同的scheduler.instanceName集群内所有节点的调度器实例名必须相同它们属于同一个逻辑调度器。不同的scheduler.instanceId设置为AUTO即可Quartz会自动生成唯一的ID。相同的数据源所有节点必须连接同一个数据库这是它们通信和协调的基础。时间同步集群内所有服务器的时间必须同步使用NTP服务否则会导致触发时间计算混乱。在集群模式下监听器会在实际执行任务的节点上被触发。你的日志和监控系统需要能聚合所有节点的日志才能看到完整的任务执行情况。6. 真实场景案例构建一个动态报表生成系统光讲理论有点干我们用一个贴近业务的例子来串一下全过程。假设我们要做一个“动态报表生成系统”管理员可以在后台随时添加新的报表生成任务。第一步设计数据库表业务表除了Quartz那11张系统表我们还需要一张业务表来保存任务的定义和元信息。CREATE TABLE report_task ( id bigint(20) NOT NULL AUTO_INCREMENT, task_name varchar(100) NOT NULL COMMENT 任务名称, report_type varchar(50) NOT NULL COMMENT 报表类型用户日报、订单周报等, cron_expression varchar(50) NOT NULL COMMENT CRON表达式, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态1-启用0-停用, params_json text COMMENT 报表生成参数JSON格式, creator varchar(50) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_task_name (task_name) ) ENGINEInnoDB COMMENT报表任务定义表;第二步编写报表生成Jobpublic class ReportGenerationJob implements Job { Autowired private ReportService reportService; // 你的业务报表服务 Override public void execute(JobExecutionContext context) { JobDataMap dataMap context.getMergedJobDataMap(); // 注意用merged它合并了JobDetail和Trigger的数据 String reportType dataMap.getString(reportType); String taskId dataMap.getString(taskId); String paramsJson dataMap.getString(params); // 解析参数调用报表服务 ReportParams params JSON.parseObject(paramsJson, ReportParams.class); try { String reportFilePath reportService.generateReport(reportType, params); logger.info(报表生成成功任务ID: {}, 文件路径: {}, taskId, reportFilePath); // 可以在这里把文件路径存库或者发通知 } catch (Exception e) { logger.error(报表生成失败任务ID: {}, taskId, e); // 失败重试逻辑可以在这里实现或者抛出JobExecutionException让Quartz处理 throw new JobExecutionException(e, true); // true表示希望Quartz重新调度该任务 } } }第三步提供管理API基于我们前面写的DynamicQuartzService包装一个ReportTaskService。Service public class ReportTaskService { Autowired private DynamicQuartzService quartzService; Autowired private ReportTaskMapper reportTaskMapper; // MyBatis Mapper public void createReportTask(ReportTaskCreateRequest request) { // 1. 保存任务定义到业务表 ReportTask task convertToEntity(request); reportTaskMapper.insert(task); // 2. 动态创建Quartz任务 TriggerCreateParam param new TriggerCreateParam(); param.setJobName(REPORT_JOB_ task.getId()); param.setJobGroup(REPORT_GROUP); param.setTriggerName(REPORT_TRIGGER_ task.getId()); param.setTriggerGroup(REPORT_GROUP); param.setTriggerType(CRON); param.setCronExpression(task.getCronExpression()); param.setJobClassName(com.yourcompany.job.ReportGenerationJob); // 传递业务参数 MapString, Object jobData new HashMap(); jobData.put(reportType, task.getReportType()); jobData.put(taskId, String.valueOf(task.getId())); jobData.put(params, task.getParamsJson()); param.setJobDataMap(jobData); quartzService.createJob(param); } public void toggleTaskStatus(Long taskId, boolean active) throws SchedulerException { ReportTask task reportTaskMapper.selectById(taskId); String triggerName REPORT_TRIGGER_ taskId; String triggerGroup REPORT_GROUP; if (active) { quartzService.resumeTrigger(triggerName, triggerGroup); task.setStatus(1); } else { quartzService.pauseTrigger(triggerName, triggerGroup); task.setStatus(0); } reportTaskMapper.updateById(task); } // ... 其他方法修改Cron表达式、立即触发一次、删除任务等 }第四步前端管理界面提供一个简单的管理页面可以列表展示所有报表任务状态、下次执行时间、提供“新增”、“启用/禁用”、“编辑”、“立即执行”、“查看日志”等按钮。后端接口就调用我们上面写的Service。这样一个完整的、可动态管理的报表调度系统就搭建起来了。它具备了生产环境需要的所有要素持久化、动态增删改查、状态监控、错误处理。你可以根据这个模式把它扩展到任何需要定时执行的业务场景比如数据同步、缓存刷新、消息推送等等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2408290.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…