​​​​Quartz简介

Quartz是一个开源的任务调度框架。基于定时、定期的策略来执行任务是它的核心功能。Quartz有3个核心要素:调度器(Scheduler)、任务(Job)、触发器(Trigger)。Quartz完全使用Java开发,可以集成到各种规模的应用程序中。它能够承载成千上万的任务调度,并且支持集群。它支持将数据存储数据库中以实现持久化,并支持绝大多数的数据库。它将任务与触发设计为松耦合,即一个任务可以对应多个触发器,这样能够轻松构造出极为复杂的触发策略。

运行机制

设置应用自启动接口(server-package1.xml)

<com.core.servlet.IBootObject caption="应用自启动接口">
    <item id="TimerJobReg" caption="应用启动执行" implement="com.pansoft.timerScheduler.TimerJobReg"/>
</com.core.servlet.IBootObject>

TimerJobReg(初始化组件)

TimerJobReg:系统启动时异步注册定时任务(初始化组件,实现了 IBootObject 接口,异步调用IOMTimerTaskSchedulerregisterJob 方法)

IBootObject:osp-base-sdk包里init()方法,在应用启动时会被容器或者框架自动调用

	@Override
	public void init() {
		logger.info("定时任务初始化:"+new SimpleDateFormat("yyyyMMdd HH:mm:ss").format(new Date()));
		JParamObject PO = JParamObject.Create();
		CompletableFuture.runAsync(() -> {
			try {
				EAI.DAL.IOM("IOMTimerTaskScheduler", "registerJob", PO, null);
			} catch (Exception e) {
				e.printStackTrace();
			}
		});
	}

​​​IOMTimerTaskScheduler(门面模式)

public JResponseObject registerJob(Object Param, Object Data, Object CustomObject,
                                   Object AdditiveObject) throws Exception {
    JParamObject PO = (JParamObject) Param;
    JResponseObject RO = new JResponseObject();

    try {
        String where = PO.GetValueByParamName("where","");

        IOMTimerTaskSchedulerSvr.orgnzJobs(PO,where);

    } catch (Exception e) {
        e.printStackTrace();
        RO.setErrorCode(-1);
        RO.setErrorString(e.getMessage());
    }
    return RO;
}

IOMTimerTaskScheduler:继承JActiveObject,门面模式,为对外暴露的服务接口,将请求转发给内部服务类IOMTimerTaskSchedulerSvr

registerJob —启动时初始化所有定时任务

用途:系统启动时,从数据库加载所有启用的定时任务,并注册到 Quartz 调度器中

三个核心方法:

注册任务(registerJob )

修改任务(modifyJob):动态修改任务(启停,改cron表达式(时间)

手动执行任务(executeJobOnce):反射创建Job实例,构造 JobExecutionContext(模拟 Quartz 上下文),直接调用 job.execute(context)

IOMTimerTaskSchedulerSvr(核心实现类)

	private static void startTimerJob(EFRowSet row) {
		String taskID = row.getString("TASK_BH", "");
		String taskMC = row.getString("TASK_MC", "");
		String jobClass = row.getString("F_TASK_CLASS", "").trim();
		String cronTime = row.getString("F_CRON", "").trim();
		// 读取并发数配置,默认为 1(串行)
		int maxConcurrent = row.getInt("F_MAX_CONCURRENT", 1);
		// 删除旧任务(同时清理信号量)
		QuartzManager.removeJob(taskID);
		ConcurrentControlJobWrapper.clearSemaphore(taskID); // 清理旧信号量
		// 判断是否启用
		if (maxConcurrent <= 0) {
			// 禁用任务:记录日志,但不计算 nextFireTime
			logTaskForDisabled(taskID, taskMC, "已禁用(maxConcurrent=" + maxConcurrent + ")");
			return;
		}
		// 验证 Cron 表达式合法性
		if (cronTime.isEmpty()) {
			logTaskForDisabled(taskID, taskMC, "Cron 表达式为空,跳过注册");
			return;
		}
		// 准备 JobDataMap
		JobDataMap jobDataMap = new JobDataMap();
		jobDataMap.put("REAL_JOB_CLASS", jobClass);
		jobDataMap.put("MAX_CONCURRENT", maxConcurrent);
		// 注册包装器 Job
		boolean result = QuartzManager.startJob(
				taskID,
				ConcurrentControlJobWrapper.class,
				cronTime,
				jobDataMap
		);
		if (result) {
			// 启用成功:传入合法 cronTime 计算下次执行时间
			logTaskForEnabled(taskID, taskMC, cronTime, maxConcurrent);
		} else {
			// 注册失败:也记录日志(不计算 nextFireTime)
			logTaskForDisabled(taskID, taskMC, "Quartz 注册失败");
		}
	}

从数据库加载任务;

根据任务状态、服务器 IP 等条件决定是否启动;

使用 Quartz 调度器注册/移除任务;

支持手动立即执行任务;

记录任务日志。

模块

作用

orgnzJobs

初始化所有(或符合条件的)定时任务,用于系统启动或动态刷新

executeJob

手动触发一次任务执行(不走调度器,直接调用 job.execute())

getTimerTask

从表 SYS_TIMER_TASK_NEW 查询任务元数据

startTimerJob

单个任务的启动逻辑:校验状态、IP、类加载、注册到 Quartz

QuartzManager

封装了 Quartz 的 Scheduler 操作(add/remove/start)

Log + Jloger

自定义日志记录,用于审计任务启停

QuartzManager(调度操作)

	public static boolean startJob(String jobName, Class<? extends Job> jobClass,
									   String time, JobDataMap jobDataMap) {
			boolean result = false;
			try {
				Scheduler sched = getScheduler();

				JobDetail jobDetail = new JobDetail();
				jobDetail.setName(jobName);
				jobDetail.setGroup(Utils.JOB_GROUP_NAME);
				jobDetail.setJobClass(jobClass);
				if (jobDataMap != null) {
					jobDetail.setJobDataMap(jobDataMap); // 传递配置参数
				}

				CronTrigger trigger = new CronTrigger(jobName, Utils.TRIGGER_GROUP_NAME);
				trigger.setCronExpression(time);

				sched.scheduleJob(jobDetail, trigger);

				if (!sched.isShutdown()) {
					sched.start();
				}
				result = true;
			} catch (Exception e) {
				e.printStackTrace();
			}
			return result;
		}

startJob(jobName, job, time):注册一个 Job + CronTrigger 到调度器

modifyJobTime(scheduler, jobDetail, time):修改已有任务的 Cron 表达式(先删后加)

removeJob(jobName):完全移除任务(暂停触发器 → 取消调度 → 删除 Job)

StatefulJob 的作用(仅限 Quartz 1.x):

禁止同一个 JobDetail 的并发执行;

即:如果上一次 execute() 还没结束,下一次触发会被阻塞/丢弃(取决于 misfire 策略);

在 Quartz 2.x+ 中已被废弃,改用注解 @DisallowConcurrentExecution

quartz.properties在quratz-all.1.6.6内,调度信息存储在内存,重启清零

信号量是进程内共享的:适用于单机部署;若集群部署,需改用 Redis 分布式锁;

// 真正的分布式并发控制(以Redis为例)
public class DistributedConcurrentJobWrapper implements Job {
    private static final String LOCK_PREFIX = "job:concurrent:";
    
    public void execute(JobExecutionContext context) {
        String jobKey = context.getJobDetail().getKey().toString();
        String lockKey = LOCK_PREFIX + jobKey;
        int maxConcurrent = getMaxConcurrent(context);
        
        // 使用Redis计数(原子操作)
        Long current = redisTemplate.opsForValue().increment(lockKey, 1);
        if (current > maxConcurrent) {
            redisTemplate.opsForValue().decrement(lockKey, 1);
            return; // 超过并发,丢弃
        }
        try {
            // 执行真正的任务
        } finally {
            redisTemplate.opsForValue().decrement(lockKey, 1);
        }
    }
}

quartz配置(quartz.properties):

Quartz 的默认加载顺序:
1.首先查找系统属性 org.quartz.config 指定的文件
2.然后在 classpath 中查找 quartz.properties 文件
3.如果都找不到,使用内置默认配置

# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#
1.调度器(Scheduler)基本配置
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
作用:为当前调度器实例命名。
说明:
在单机环境中可任意命名;
在集群环境中,所有节点必须使用相同的 instanceName(配合 instanceId 区分实例);
默认值就是 DefaultQuartzScheduler。

org.quartz.scheduler.rmi.export: false
作用:是否将调度器通过 RMI(远程方法调用)暴露出去。
说明:设为 true 可让其他 JVM 远程控制此调度器(极少使用),一般保持 false。

org.quartz.scheduler.rmi.proxy: false
作用:是否创建一个 RMI 代理来连接远程调度器。
说明:若你的应用要连接别人暴露的 RMI 调度器才设为 true,否则 false。

org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
作用:是否将 Job 的执行包裹在 JTA 用户事务中。
说明:仅在使用 Java EE 容器 + JTA 事务时才需开启,普通 Spring Boot/Java SE 应用保持 false。
2. 线程池(ThreadPool)配置
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
作用:指定线程池实现类。
说明:
SimpleThreadPool 是 Quartz 内置的简单线程池;
也可替换为自定义线程池(如集成 Spring 的 ThreadPoolTaskExecutor,但需额外适配)。

org.quartz.threadPool.threadCount: 10
作用:线程池中最大工作线程数。
说明:
表示最多可同时执行 10 个 Job;
若有 15 个任务同时触发,5 个会排队等待;
根据业务并发量调整(一般 5~50 足够)。

org.quartz.threadPool.threadPriority: 5
作用:线程优先级(1~10,5 为普通优先级)。
说明:通常保持默认 5,除非有特殊调度需求。

org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
作用:工作线程是否继承初始化线程的上下文类加载器(ContextClassLoader)。
说明:
设为 true 可避免在 Web 容器(如 Tomcat)中因类加载器隔离导致的 ClassNotFoundException;
建议保持 true,尤其在 Web 应用中。

3. 容错与补偿(Misfire)配置
org.quartz.jobStore.misfireThreshold: 60000
作用:判定任务“错过触发(misfire)”的时间阈值(单位:毫秒)。
说明:
默认 60,000 毫秒 = 60 秒;
如果一个任务本应在 10:00:00 触发,但调度器在 10:00:59 才处理它 → 未 misfire;
如果到 10:01:01 才处理 → 视为 misfire,将按 Trigger 的 misfire 策略处理(如立即执行、忽略等);
可根据任务敏感度调整(如关键任务可设为 5000 毫秒)。

org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
作用:指定任务数据的存储方式。
说明:
RAMJobStore:所有 Job、Trigger、Calendar 等信息仅保存在内存(JVM Heap)中;
优点:速度快、零依赖;
缺点:应用重启后所有调度信息丢失;
若需持久化,应改为 org.quartz.impl.jdbcjobstore.JobStoreTX 并配置数据库。

内存模式(RAMJobStore) → 重启清零

  • 所有调度信息(Job、Trigger、Calendar 等)仅保存在 JVM 内存中
  • 优点:速度快、零依赖
  • 缺点:应用重启后所有任务丢失
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
# 或 JobStoreCMT(用于 JTA 事务)
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.dataSource.myDS.URL = jdbc:oracle:...
org.quartz.dataSource.myDS.user = ...
org.quartz.dataSource.myDS.password = ...

 数据库模式(JDBCJobStore) → 持久化,重启不丢

  • 调度信息存储在 12 张 Quartz 表 中(如 QRTZ_JOB_DETAILSQRTZ_TRIGGERS 等)
  • 优点:持久化、支持集群
  • 缺点:需建表、有数据库依赖、性能略低

只要 jobStore.class 是 RAMJobStore,就是内存模式;只要是 JDBCJobStore 相关类,就是数据库模式。

内存模式只适合:本地开发、临时脚本、非关键任务

维度 内存模式 (RAMJobStore) 数据库模式 (JDBCJobStore)
存储位置 JVM 堆内存 数据库表(QRTZ_*
持久化 ❌ 应用重启后全部丢失 ✅ 重启后自动恢复
集群支持 ❌ 不支持(多实例会重复执行) ✅ 支持(通过数据库锁协调)
任务动态更新 需手动重建(如你的 modifyJob 可通过 API 实时更新
依赖 无外部依赖 需要数据库 + 建表
性能 ⚡ 极快(微秒级调度) 🐢 略慢(毫秒级,有 DB IO)
可靠性 低(进程崩溃即丢任务) 高(事务保障、可审计)

如何改成数据库存储?(持久化)

1.在数据库中创建 Quartz 表

  • 脚本位置:quartz-all-1.6.6.jar 中的 org/quartz/impl/jdbcjobstore/tables_*.sql
  • Oracle 用 tables_oracle.sql

2.提供 quartz.properties 文件(放在 classpath 根目录)

org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 20
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
org.quartz.jobStore.useProperties = false
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_

org.quartz.dataSource.myDS.driver = oracle.jdbc.OracleDriver
org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@localhost:1521:orcl
org.quartz.dataSource.myDS.user = your_user
org.quartz.dataSource.myDS.password = your_pass
org.quartz.dataSource.myDS.maxConnections = 20

3.确保应用启动时加载该配置

在 Quartz 2.2.x 中,可以只写 driver/url/user/password,它会用 DriverManagerConnectionProvider

但在 2.3+ 中:

  • DriverManagerConnectionProvider 被移除
  • 如果 classpath 没有 HikariCP 也没有 C3P0,Quartz 会尝试加载 C3P0
  • 所以 Quartz 2.3+ 必须显式提供一个连接池实现

解决方法:添加c3p0依赖或HikariCP依赖(高性能、轻量)

<dependency>
    <groupId>com.mchange</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.5.5</version>
</dependency>
<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.4.5</version> <!-- 兼容 JDK8 -->
</dependency>

两者差异比较(C3P0于2015年不再维护):

特性 C3P0 HikariCP Druid
性能 ⚡ 极快(业界最快)
代码量 >200 类 ~130 类(精简) 较大(功能多)
并发模型 基于锁 无锁(ConcurrentBag + ThreadLocal) 优化锁
连接获取延迟 极低
监控能力 JMX + Micrometer ✅ 强大 Web 控制台
SQL 防火墙/审计 ✅ 支持
活跃维护 ❌ 停滞 ✅ 持续更新 ✅ 阿里维护
默认安全性

界面配置

任务调度:SYS_TIMER_TASK_NEW

TASK_BH:任务编号

TASK_MC:任务名称

F_TASK_CLASS:类全名(含路径)

TASK_TYPE:日期条件(W:无,Y:年度,M:月度,D:日期)

H_STATUS:运行状态(RUN:启动,STOP:停止)

F_CRON:时间表达式

F_NOTE:备注

F_SERVER:服务器IP

目前只支持禁止并发(通过StatefulJob)及允许无限并发(通过普通Job)

如果需要设置并发数,需要在SYS_TIMER_TASK_NEW中添加字段设置最大并发数(如F_MAX_CONCURRENT)

  1. 数据库加字段(如 F_MAX_CONCURRENT);
  2. 引入包装器(Wrapper) + 信号量(Semaphore);
  3. 修改 startJob 逻辑。
  4. 信号量是进程内共享的:适用于单机部署;若集群部署,需改用 Redis 分布式锁;5
  5. 丢弃策略:当前超过并发上限时直接丢弃,如需重试可记录到延迟队列;

Corn(时间表达式)

最常见的 标准 Cron 表达式有 6 位或 7 位

Linux crontab 通常是 5 位(没有“秒”和“年”):分 时 日 月 周

Quartz(Java)默认是 6 位(含“秒”,不含“年”)或 7 位(含“年”)

Spring @Scheduled 使用的是 6 位(秒 分 时 日 月 周)

Cron 表达式是一种用于配置定时任务执行时间的字符串格式,广泛应用于 Linux 系统(如 crontab)、Java(如 Spring @Scheduled)、Quartz 调度框架等。

字段位置

含义

允许值

允许特殊字符

1

秒(可选)

0–59

, - * /

2

分钟

0–59

, - * /

3

小时

0–23

, - * /

4

日(月份中的)

1–31

, - * ? / L W C

5

1–12 或 JAN–DEC

, - * /

6

星期

0–7(0 和 7 都是周日)或 SUN–SAT

, - * ? / L # C

7(可选)

1970–2099

, - * /

特殊字符说明

字符

含义

*

任意值(如 * 在分钟位表示每分钟)

?

不指定值,用于“日”和“星期”互斥(只能一个指定具体值)

-

范围(如 10-12 表示 10,11,12)

,

枚举(如 MON,WED,FRI)

/

步长(如 0/5 表示从 0 开始每 5 个单位一次)

L

Last,最后(如 L 在日字段表示该月最后一天)

W

最近的工作日(周一到周五)

#

第几个星期几(如 6#3 表示每月第 3 个周五)

常见Cron表达示例:

每天凌晨2点执行:0 0 2 * * ?

每小时整点执行:0 0 * * * ?

每5分钟执行一次:0 */5 * * * ?

每周一上午9:30执行:0 30 9 ? * MON

每月1号凌晨1点执行:0 0 1 1 * ?

工作日(周一到周五)每天8:00执行:0 0 8 ? * MON-FRI

每月最后一个工作日17:00执行:0 0 17 LW * ?

不同系统对星期的起始定义不同:

Quartz/Spring:1 = 周日,2 = 周一,…,7 = 周六

Linux crontab:0 或 7 = 周日,1 = 周一,…,6 = 周六

在线 Cron 表达式生成器

https://crontab.guru/(适用于 Linux 5 位)

https://www.freeformatter.com/cron-expression-generator-quartz.html(适用于 Quartz 6/7 位)

信号量(Semaphore—— 许可证发放机

信号量(Semaphore)是 Java 并发包(java.util.concurrent)中一种用于控制并发访问数量的同步工具

想象一个停车场:

  • 总共 3 个车位(对应 maxConcurrent = 3);
  • 入口有一个发牌机(Semaphore);
  • 每辆车(Job 实例)进来前必须领一张牌;
  • 如果没牌了(3 个车位已满),新车只能掉头离开(丢弃)或排队等(我们选择丢弃);
  • 车离开时归还牌照,下一辆才能进。
Semaphore semaphore = new Semaphore(3); // 最多 3 个许可

// 尝试获取许可

if (semaphore.tryAcquire(1, TimeUnit.SECONDS)) {

    try {

        // 执行业务逻辑(占用车位)

    } finally {

        semaphore.release(); // 归还许可(释放车位)

    }

} else {

    // 没拿到许可 → 超过并发上限,丢弃本次执行

}

Semaphore 保证:同一任务最多 N 个实例同时运行

包装器(Wrapper)—— “代理执行人”

你是一个老板(调度器),要安排员工干活:

  • ·员工 A、B、C 各自负责不同任务(业务 Job);
  • ·但你不直接派活给他们,而是通过一个管家(Wrapper);
  • ·管家手里拿着“并发许可证”(Semaphore);
  • ·每次派活前,管家先检查:这个任务还能不能加人?
  • ·  能 → 叫对应员工干活;
  • ·  不能 → 直接回绝:“人手满了,这次不干”。
// 业务 Job(员工)

public class DataSyncJob implements Job { ... }

// 包装器(管家)

public class ConcurrentControlJobWrapper implements Job {

    public void execute(JobExecutionContext context) {

        // 1. 检查并发许可(Semaphore)

        // 2. 如果允许,反射调用 DataSyncJob.execute()

    }

}

ConcurrentControlJobWrapper(动态并发数实现类)

package com.pansoft.timerScheduler;

import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * 支持最大并发数控制的 Job 包装器(兼容 Quartz 1.x)
 * 包装器(管家)
 * 主要功能:
 * 1.检查并发许可(Semaphore信号量)
 * 2.如果允许,发射调用DataSyncJob.execute()
 */
public class ConcurrentControlJobWrapper implements Job {

    // 每个 taskID 对应一个信号量(全局共享)
    private static final Map<String, Semaphore> SEMAPHORE_MAP = new ConcurrentHashMap<>();
    private static final Logger logger = LoggerFactory.getLogger(ConcurrentControlJobWrapper.class);
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String taskID = context.getJobDetail().getKey().getName();
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();

        // 从配置中获取最大并发数
        Integer maxConcurrent = (Integer) dataMap.get("MAX_CONCURRENT");
        if (maxConcurrent == null || maxConcurrent <= 0) {
            logger.info("[Quartz] 任务 [" + taskID + "] 被禁用(maxConcurrent=" + maxConcurrent + ")");
            return;
        }

        /* 获取或创建该任务的信号量
        公平模式(true):先请求的线程优先获得许可证(FIFO);
        非公平模式(false,默认):可能“插队”,吞吐量更高但可能饿死。
        在定时任务场景,公平模式更合理——避免某些任务永远抢不到资源
         */
        Semaphore semaphore = SEMAPHORE_MAP.computeIfAbsent(taskID, k -> new Semaphore(maxConcurrent, true));//第二个参数 = true → 公平模式

        boolean acquired = false;
        try {
            /*
             尝试获取许可(最多等待 1 秒,避免永久阻塞调度线程)
             Semaphore 类的一个带超时的尝试获取许可的方法
             参数1:超时时间数值
             参数2:unit(时间单位)类型:TimeUnit(枚举)
             acquired=true 信号量内部的许可计数器(permits)就会立即减 1
             */
            acquired = semaphore.tryAcquire(1, TimeUnit.SECONDS);
            /*
             * 排队等待 可能造成调度线程阻塞
             * 并发数已满,新任务会一直排队
             * acquired = acquire();//计数器 -1
             */
            if (!acquired) {
                logger.info("[Quartz] 任务 [" + taskID + "] 超过最大并发数 " + maxConcurrent + ",本次触发被丢弃");
                return;
            }

            // 反射执行真实 Job
            String realJobClass = (String) dataMap.get("REAL_JOB_CLASS");
            Class<?> clazz = Class.forName(realJobClass);//通过类名加载Class对象
            Job realJob = (Job) clazz.newInstance();//创建Job实例
            realJob.execute(context);

        } catch (Exception e) {
            throw new JobExecutionException("执行任务 [" + taskID + "] 失败", e);
        } finally {
            if (acquired) {
                semaphore.release();//计数器 +1
            }
        }
    }

    /**
     * 清理指定任务的信号量(用于任务删除/更新时)
     */
    public static void clearSemaphore(String taskID) {
        SEMAPHORE_MAP.remove(taskID);
    }
}

为什么采用丢弃策略(tryAcquire(long,Timunit))

acquire():排队等待

缺点:

  • 当前 Quartz 调度线程会被挂起;

  • 如果并发数已满,新任务会一直排队;

  • 可能导致调度器线程池耗尽,其他任务无法触发!

tryAcquire(1, TimeUnit.SECONDS):丢弃策略

参数1:超时时间数值(long);参数2:时间单位(TimeUnit)

优点:

  • 超过设置的时间就记录日志退出;
  • 调度线程快速释放,不影响其他任务调度;
  • 符合“丢弃超额任务,保证系统稳定” 的设计目标。

并发测试结果

并发=1

并发=3

并发=0

StatefulJob和Job的区别

StatefulJob 和普通 Job 是 Quartz 调度框架中两种不同类型的作业(Job),它们的核心区别在于 是否允许多个实例并发执行。

 普通 Job(无状态作业)

允许多个实例并发执行。

如果一个任务每 3 秒执行一次,而某次执行耗时 10 秒,那么在第 6 秒、第 9 秒……会继续触发新的执行实例,与正在运行的实例并行运行。

适用于:无共享状态、线程安全、可并行处理的任务(如发送通知、日志清理等)。

StatefulJob(有状态作业,2.0已废弃)

  • 禁止并发执行:同一 JobDetail 的多个触发不会重叠。
  • 如果上一次执行还没完成,下一次触发会被阻塞等待,直到前一次结束。
  • 相当于给 Job 加了一个隐式锁(基于 JobDetail 的 key)。
  • Quartz1.x写法
// 已废弃!不要使用

public class MyStatefulJob implements StatefulJob {

    public void execute(JobExecutionContext context) {

        // 同一时间只会有一个实例运行

    }

}
  • Quartz 2.0+:使用 @DisallowConcurrentExecution

Quartz 2.0 之后,不再通过接口区分,而是通过 注解 控制并发行为:

禁止并发(相当于旧的 StatefulJob):

@DisallowConcurrentExecution  // ← 关键注解!

public class MySerialJob implements Job {

    public void execute(JobExecutionContext context) {

        // 同一 JobDetail 不会并发执行

    }

}

允许并发(默认行为,普通 Job):

// 无注解 = 允许并发

public class MyParallelJob implements Job {

    public void execute(JobExecutionContext context) {

        // 可能多个实例同时运行

    }

}

特性

普通 Job

StatefulJob

新方式(Quartz 2+)

并发执行

允许

禁止

用 @DisallowConcurrentExecution 禁止

状态共享

无(每次新建实例)

无(但串行)

无(但可通过注解控制串行)

是否推荐

推荐

已废弃

推荐使用注解

实现方式

实现 Job

实现 StatefulJob

实现 Job + 注解

注:文中使用版本为1.6.6

文中代码 - Quartz 1.x 写法(已废弃14年!)
JobDetail jobDetail = new JobDetail();  // Quartz 1.x
CronTrigger trigger = new CronTrigger(); // Quartz 1.x
StatefulJob接口 // Quartz 1.x,2009年后就废弃了

1.X-2.X升级

将quartz-all-1.6.6.jar替换为quartz-2.3.2.jar

Job类替换

移除移除StatefulJob,改为Job,并添加注解控制并发(@DisallowConcurrentExecution )

@DisallowConcurrentExecution
public class TJob_deleteLog implements Job {
	
	@Override
	public void execute(JobExecutionContext arg0) throws JobExecutionException {
     }
   }

JobDetail接口相关替换

1.X:

JobDetail job = new JobDetail("job1", MyJob.class); 
CronTrigger trigger = new CronTrigger("trigger1", "group1", "0/5 * * * * ?");

2.X:

import static org.quartz.JobBuilder.*;
import static org.quartz.TriggerBuilder.*;
import static org.quartz.CronScheduleBuilder.*;

JobDetail job = newJob(MyJob.class)
    .withIdentity("job1", "group1")
    .build();

CronTrigger trigger = newTrigger()
    .withIdentity("trigger1", "group1")
    .withSchedule(cronSchedule("0/5 * * * * ?"))
.build();

注:2.X所有构建需通过Builder模式,不允许直接new

        2.X中JobDetail接口不再有getName方法

1.X:String taskID = context.getJobDetail().getName();
2.X:String taskID = context.getJobDetail().getKey().getName();

QuartzManager修改

	public static boolean startJob(String jobName, Class<? extends Job> jobClass, String cronExpr, JobDataMap jobDataMap) {
		if (jobName == null || jobName.trim().isEmpty()) {
			throw new IllegalArgumentException("jobName cannot be null or empty");
		}
		if (jobClass == null) {
			throw new IllegalArgumentException("jobClass cannot be null");
		}
		if (cronExpr == null || cronExpr.trim().isEmpty()) {
			throw new IllegalArgumentException("cronExpr cannot be null or empty");
		}

		try {
			// 1. 构建 JobDetail 定义任务 要执行什么/带哪些参数/叫什么名字/属于哪个组
			JobDetail job = newJob(jobClass)//job类
					.withIdentity(jobName) // group 默认为 DEFAULT name+gtoup
					.setJobData(jobDataMap != null ? jobDataMap : new JobDataMap())//参数 (job实现类、最大并发数)
					.build();

			// 2. 构建 CronTrigger 定义触发器 基于cron表达式触发任务Trigger(触发器)的实现类
			CronTrigger trigger = newTrigger()
					.withIdentity(jobName + "_trigger") // trigger 名称与 job 关联
					.withSchedule(
							cronSchedule(cronExpr.trim())
									.withMisfireHandlingInstructionDoNothing() // 失效策略:跳过
					)
					.build();

			// 3. 调度任务 注册到调度器
			scheduler.scheduleJob(job, trigger);
			return true;

		} catch (Exception e) {
			System.err.println("Failed to schedule job: " + jobName + ", cron: " + cronExpr);
			e.printStackTrace();
			return false;
		}
	}

JobDetail 是 Quartz 调度框架中的核心概念之一,它代表一个 “任务的定义” —— 即:要执行什么?带哪些参数?叫什么名字?属于哪个组?

CronTrigger 是 Quartz 中用来“按 cron 表达式定时触发任务”的调度指令,它和 JobDetail(任务定义)配对使用,共同完成“在什么时间执行什么任务”的调度需求。

JobDetail 和其他组件的关系

  • 一个 JobDetail 可以被多个 Trigger 触发(比如每天 9 点和每周一 10 点都跑同一个报表)。
  • 一个 Trigger 只能绑定一个 JobDetail

MockJobExecutionContext(手动执行Job的模拟上下文)

package com.pansoft.timerScheduler;

import org.quartz.*;
import java.util.Date;

/**
 * 用于手动执行 Job 的模拟上下文(兼容 Quartz 2.3.2)
 */
public class MockJobExecutionContext implements JobExecutionContext {

    private final JobDetail jobDetail;
    private final long fireTimeMillis;

    public MockJobExecutionContext(JobDetail jobDetail) {
        this.jobDetail = jobDetail;
        this.fireTimeMillis = System.currentTimeMillis();
    }

    @Override
    public Scheduler getScheduler() {
        return null; // 手动执行不需要
    }

    @Override
    public Trigger getTrigger() {
        return null;
    }

    @Override
    public Calendar getCalendar() {
        return null;
    }

    @Override
    public boolean isRecovering() {
        return false;
    }

    @Override
    public TriggerKey getRecoveringTriggerKey() throws IllegalStateException {
        return null;
    }

    @Override
    public int getRefireCount() {
        return 0;
    }

    @Override
    public JobDataMap getMergedJobDataMap() {
        return jobDetail.getJobDataMap(); // 关键!
    }

    @Override
    public JobDetail getJobDetail() {
        return jobDetail;
    }

    @Override
    public Job getJobInstance() {
        return null;
    }

    @Override
    public Date getFireTime() {
        return new Date(fireTimeMillis);
    }

    @Override
    public Date getScheduledFireTime() {
        return getFireTime();
    }

    @Override
    public Date getPreviousFireTime() {
        return null;
    }

    @Override
    public Date getNextFireTime() {
        return null;
    }

    @Override
    public String getFireInstanceId() {
        return "MANUAL_" + System.currentTimeMillis();
    }

    @Override
    public void setResult(Object result) {}

    @Override
    public long getJobRunTime() {
        return 0;
    }

    @Override
    public void put(Object o, Object o1) {

    }

    @Override
    public Object get(Object o) {
        return null;
    }

    @Override
    public Object getResult() {
        return null;
    }

    // 其他方法可按需实现,多数 Job 不会用到
}

为什么升级2.X要新增类MockJobExecutionContext?

Quartz 1.x 中的 JobExecutionContext 是一个可直接实例化的 concrete class;
而 Quartz 2.x 中它变成了一个 interface(接口),且其实现类是 package-private(包私有)的,无法在外部直接创建。

优点:

完全兼容 job.execute(context) 调用;
不依赖 Quartz 内部实现;
只需实现 Job 真正用到的方法(如 getJobDetail()、getMergedJobDataMap())

其他方案

在Job实现类中新增方法供手动调用(繁琐,每个Job都需添加)

public class MyJob implements Job {
    public void execute(JobExecutionContext ctx) {
        doWork(ctx.getJobDetail().getJobDataMap());
    }

    // 新增方法供手动调用
    public void executeManually(Map<String, Object> params) {
        doWork(new JobDataMap(params));
    }
}

IOMTimerTaskSchedulerSvr(启动及手动执行修改)

启动:

	private static void startTimerJob(EFRowSet row) {
		String taskID = row.getString("TASK_BH", "");
		String taskMC = row.getString("TASK_MC", "");
		String jobClass = row.getString("F_TASK_CLASS", "").trim();
		String cronTime = row.getString("F_CRON", "").trim();
		// 读取并发数配置,默认为 1(串行)
		int maxConcurrent = row.getInt("F_MAX_CONCURRENT", 1);
		// 删除旧任务(同时清理信号量)
		QuartzManager.removeJob(taskID);
		ConcurrentControlJobWrapper.clearSemaphore(taskID); // 清理旧信号量
		// 判断是否启用
		if (maxConcurrent <= 0) {
			// 禁用任务:记录日志,但不计算 nextFireTime
			logTaskForDisabled(taskID, taskMC, "已禁用(maxConcurrent=" + maxConcurrent + ")");
			return;
		}
		// 验证 Cron 表达式合法性
		if (cronTime.isEmpty()) {
			logTaskForDisabled(taskID, taskMC, "Cron 表达式为空,跳过注册");
			return;
		}
		// 准备 JobDataMap
		JobDataMap jobDataMap = new JobDataMap();
		jobDataMap.put("REAL_JOB_CLASS", jobClass);
		jobDataMap.put("MAX_CONCURRENT", maxConcurrent);
		// 注册包装器 Job
		boolean result = QuartzManager.startJob(
				taskID,
				ConcurrentControlJobWrapper.class,
				cronTime,
				jobDataMap
		);
		if (result) {
			// 启用成功:传入合法 cronTime 计算下次执行时间
			logTaskForEnabled(taskID, taskMC, cronTime, maxConcurrent);
		} else {
			// 注册失败:也记录日志(不计算 nextFireTime)
			logTaskForDisabled(taskID, taskMC, "Quartz 注册失败");
		}
	}
	/**
	 * 记录已启用的定时任务日志(会计算下次执行时间)
	 */
	private static void logTaskForEnabled(String taskID, String taskMC, String cronTime, int maxConcurrent) {
		try {
			Log log = new Log();
			log.setTaskId(taskID);
			log.setAction("TIMER_TASK");
			log.setAction_des(taskMC);
			log.setNextFireTime(CronUtil.getNextFireTime(cronTime)); // 安全:cronTime 来自 F_CRON 字段
			log.setStatus("true");
			log.setExp("已启用 [并发=" + maxConcurrent + "]");
			Jloger.add(log);
		} catch (Exception e) {
			logger.error("记录启用任务日志失败: " + taskID);
			e.printStackTrace();
		}
	}
	/**
	 * 记录禁用或失败的定时任务日志(不计算下次执行时间)
	 */
	private static void logTaskForDisabled(String taskID, String taskMC, String reason) {
		try {
			Log log = new Log();
			log.setTaskId(taskID);
			log.setAction("TIMER_TASK");
			log.setAction_des(taskMC);
			log.setNextFireTime(""); // 或 null,表示无下次执行时间
			log.setStatus("false");
			log.setExp(reason); // F_EXP 存放状态说明
			Jloger.add(log);
		} catch (Exception e) {
			logger.error("记录禁用任务日志失败: " + taskID);
			e.printStackTrace();
		}
	}

手动执行:

	protected static boolean executeJob(JParamObject PO, String where) throws Exception {
		List<String> errList = new ArrayList<>();
		boolean result = true;

		try {
			EFDataSet ds = getTimerTask(where);

			for (int i = 0; i < ds.getRowCount(); i++) {
				EFRowSet row = ds.getRowSet(i);
				String taskID = row.getString("TASK_BH", "");
				String taskMC = row.getString("TASK_MC", "");
				String jobClass = row.getString("F_TASK_CLASS", "");

				if (StringUtils.isBlank(jobClass)) continue;

				try {
					// 1. 创建 Job 实例
					Class<?> loadedClass = Class.forName(jobClass);
					// 验证是否为合法 Job
					if (!Job.class.isAssignableFrom(loadedClass)) {
						errList.add("任务 [" + taskID + "] 类 " + jobClass + " 未实现 Job 接口");
						continue;
					}
					@SuppressWarnings("unchecked")
					Class<? extends Job> jobClazz = (Class<? extends Job>) loadedClass;
					// 创建实例(可选,用于提前验证)
					Job jobInstance = jobClazz.newInstance();
					// 2. 构建 JobDetail(使用 Builder)
					JobDataMap jobDataMap = new JobDataMap();
					jobDataMap.put("NOTE", "手动执行");
					jobDataMap.put("TASK_MC", taskMC);
					jobDataMap.put("PO", PO);

					JobDetail jobDetail = JobBuilder.newJob(jobClazz)
							.withIdentity(taskID, Utils.JOB_GROUP_NAME)
							.setJobData(jobDataMap)
							.build();

					// 3. 创建 Mock 上下文
					MockJobExecutionContext mockContext = new MockJobExecutionContext(jobDetail);

					// 4. 执行任务
					jobInstance.execute(mockContext);

				} catch (Exception e) {
					String msg = "手动执行任务 [" + taskID + "] 失败: " + e.toString();
					errList.add(msg);
					e.printStackTrace();
				}
			}

			if (!errList.isEmpty()) {
				throw new Exception(String.join("\n", errList));
			}

		} catch (Exception e) {
			result = false;
			throw e;
		}

		return result;
	}

日志替换

Job传参方式需修改,1.0可以直接put,2.0不支持直接存取对象,需要通过全局参数JobDataMap 存取

1.x.x
arg0.put("RESULT", "true"); //必须设置
arg0.put("DESC", "定时任务"); //任务描述
arg0.put("NOTE", "同步日期"+startDate+";"+F_EXP); //备注信息
2.x.x
JobDataMap jobDataMap = arg0.getMergedJobDataMap();
jobDataMap.put("RESULT", "true");
jobDataMap.put("DESC", "定时任务");
jobDataMap.put("NOTE", "同步日期" + newtime + ";" + F_EXP);
	public static void add(JobExecutionContext context) {
		try (JConnection conn = Db.getConn(); Statement st = conn.createStatement()) {
			// 从 JobDataMap 获取数据
			JobDataMap map = context.getMergedJobDataMap();

			String taskId = context.getJobDetail().getKey().getName();
			String result = getString(map, "RESULT", "");
			String desc = getString(map, "DESC", "");
			String note = getString(map, "NOTE", "");
			String exp = getString(map, "EXP", "").replaceAll("'", "");

			
			SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
			String zxsjStr = formatDate(context.getFireTime(), sf);
			String sczxsjStr = formatDate(context.getPreviousFireTime(), sf);
			String xczxsjStr = formatDate(context.getNextFireTime(), sf);
			String jhsjStr = formatDate(context.getScheduledFireTime(), sf);

			Log log = new Log();
			log.setTaskId(taskId);
			log.setUser("Quartz");
			log.setAction("TIMER_TASK");
			log.setAction_des(desc);
			log.setFireTime(zxsjStr);
			log.setPreviousFireTime(sczxsjStr);
			log.setNextFireTime(xczxsjStr);
			log.setScheduledFireTime(jhsjStr);
			log.setStatus(result);
			log.setExp(exp);
			log.setNote(note);
			Jloger.add(log);

		} catch (Exception e) {
			LoggerFactory.getLogger(TimerJobLogger.class).error("记录定时任务日志失败", e);
		}
	}

	// 辅助方法
	private static String getString(JobDataMap map, String key, String defaultValue) {
		Object val = map.get(key);
		return val == null ? defaultValue : val.toString();
	}

	private static String formatDate(Date date, SimpleDateFormat format) {
		return date == null ? "" : format.format(date);
	}

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐