一、引言
在业务系统中,定时任务与工作流编排是常见的核心需求。本文以一个基于 JDK 21、Swing、DDD 与 MVC 架构的 Java 任务调度系统为例,从领域模型设计、仓储接口抽象到界面交互与调度集成,完整梳理一套可落地的实现思路。项目支持 Quartz 定时调度、Redis 集群节点发现、DAG 工作流编排以及任务执行历史统计,适合作为中大型桌面端调度系统的参考实现。
二、领域模型设计
领域模型是整个系统的地基,它决定了业务规则如何表达、如何被持久化以及如何被上层调度与界面复用。本节围绕任务实体、DAG 工作流与领域事件三个核心部分展开。
2.1 任务实体与状态管理
任务实体(Task)是系统的聚合根,承载名称、Cron 表达式、命令、超时时间、重试策略等关键属性。状态使用枚举 TaskStatus 管理,当前仅区分空闲与运行中两种状态,便于后续扩展。任务还支持一次性语义:当备注中包含「一次性」时,执行成功后会自动禁用,适合初始化数据等场景。
2.2 DAG 工作流与节点编排
DAG 工作流用于表达任务之间的依赖关系。DagDefinition 保存整个工作流的节点列表,DagNode 则描述单个可执行单元,支持绑定已有任务或内联命令,并通过 dependsOn 声明上游依赖。节点还提供失败策略(阻断或跳过)与重试次数,满足并行分支与容错需求。
2.3 领域事件与解耦
领域事件基类 DomainEvent 定义了任务开始、完成、失败以及 DAG 执行完成等事件类型。通过 EventBus 传递事件,可以将任务执行、日志记录与界面刷新解耦,提升系统的可维护性与扩展性。
三、仓储抽象与基础设施
仓储接口遵循 DDD 的依赖倒置原则,将领域模型与具体数据库实现分离。本节介绍任务、历史与 DAG 三类仓储接口的设计,以及它们在基础设施层的落地方式。
3.1 任务仓储接口
TaskRepository 提供任务的全量查询、分页搜索、保存更新、状态切换与启停控制等能力。接口设计面向领域服务与界面控制层,屏蔽底层 JDBC 细节,便于替换实现或引入缓存。
3.2 执行历史仓储接口
TaskHistoryRepository 负责记录每次任务的执行结果,支持按任务查询、最近记录、统计汇总以及按天分布等能力。这些数据是仪表盘与执行日志页面的数据来源,也是评估调度稳定性的重要依据。
3.3 DAG 仓储接口
DagRepository 提供工作流的增删改查能力,支撑 DAG 编排界面的数据读写。与任务仓储类似,接口保持精简,具体持久化逻辑由基础设施层实现。
四、实战示例:从启动到界面交互
本节通过主程序入口与 Swing 界面两个示例,展示系统如何从配置加载、数据库初始化、调度器启动,最终呈现为可操作的桌面应用。
4.1 主程序启动流程
Application 主类按照固定顺序完成系统引导:加载配置与日志、初始化 H2 数据库、启动 Redis 支持、启动 Quartz 调度器、写入种子数据,最后在 EDT 线程中创建主窗口。首次运行空库时,系统会自动插入健康检查、数据库备份与一次性初始化三个示例任务。
4.2 Swing 主窗口与工具栏
MainWindow 采用 BorderLayout 组织界面,顶部为操作工具栏,中部为任务列表、仪表盘、DAG 工作流与执行日志四个标签页,底部为状态栏。工具栏提供新增、编辑、删除、立即执行、暂停、恢复、启停、历史、DAG 编排以及 JSON 导入导出等操作,覆盖日常管理所需的主要功能。
4.3 任务编辑对话框与 Cron 校验
TaskEditDialog 是新增与编辑任务的统一入口,表单包含名称、Cron 表达式、命令、超时、重试等字段。保存前会进行必填校验,并提供 Cron 表达式校验与下次执行时间预览,帮助用户快速确认调度配置是否正确。
五、总结
本文从领域模型、仓储抽象、调度集成到 Swing 界面,完整梳理了一个 Java 任务调度系统的核心实现。整体采用 DDD 分层与 MVC 交互模式,兼顾业务表达、扩展性与界面可操作性。后续可在此基础上继续完善分布式调度协调、任务告警通知以及更丰富的 DAG 执行策略,让系统在真实生产环境中更加健壮。
项目结构:

java
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : DomainEvent.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.event;
import java.time.LocalDateTime;
/**
领域事件基类(EventBus 传递对象)
子类:TaskStartedEvent / TaskCompletedEvent / TaskFailedEvent / DagExecutedEvent
*/
public abstract class DomainEvent {
private final long taskId;
private final String taskName;
private final LocalDateTime occurredAt = LocalDateTime.now();
protected DomainEvent(long taskId, String taskName) {
this.taskId = taskId;
this.taskName = taskName;
}
public long getTaskId() { return taskId; }
public String getTaskName() { return taskName; }
public LocalDateTime getOccurredAt() { return occurredAt; }
/** 任务开始事件 */
public static class TaskStartedEvent extends DomainEvent {
public TaskStartedEvent(long taskId, String taskName) {
super(taskId, taskName);
}
}
/** 任务完成事件(成功) */
public static class TaskCompletedEvent extends DomainEvent {
private final String message;
public TaskCompletedEvent(long taskId, String taskName, String message) {
super(taskId, taskName);
this.message = message;
}
public String getMessage() { return message; }
}
/** 任务失败事件 */
public static class TaskFailedEvent extends DomainEvent {
private final String message;
public TaskFailedEvent(long taskId, String taskName, String message) {
super(taskId, taskName);
this.message = message;
}
public String getMessage() { return message; }
}
/** DAG 执行完成事件 */
public static class DagExecutedEvent extends DomainEvent {
private final String dagName;
public DagExecutedEvent(long dagId, String dagName, boolean success) {
super(dagId, dagName);
this.dagName = dagName;
this.success = success;
}
private final boolean success;
public boolean isSuccess() { return success; }
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : Application.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
DAG 工作流定义:任务依赖编排(A 完成后执行 B,支持并行分支)
nodesJson 保存节点列表(DagNode 的 JSON)
*/
public class DagDefinition {
private long id;
private String name = "";
private String cronExpr = ""; // 可选:定时触发整个 DAG
private boolean enabled = true;
private List<DagNode> nodes = new ArrayList<>();
private LocalDateTime createdAt;
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCronExpr() { return cronExpr; }
public void setCronExpr(String cronExpr) { this.cronExpr = cronExpr; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public List<DagNode> getNodes() { return nodes; }
public void setNodes(List<DagNode> nodes) { this.nodes = nodes; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : Application.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.model;
import java.util.ArrayList;
import java.util.List;
/**
DAG 节点:一个可执行单元(命令 或 引用现有任务 key)
字段与 .NET 版 DAG 节点语义对齐:
key 节点唯一标识(DAG 内)
name 显示名
taskKey 可空:绑定现有任务(task-{id})执行其命令
command 可空:内联命令
dependsOn 上游节点 key 列表
onFail STOP=失败阻断下游 / SKIP=失败跳过下游
retry 失败重试次数
*/
public class DagNode {
private String key = "";
private String name = "";
private String taskKey = "";
private String command = "";
private List<String> dependsOn = new ArrayList<>();
private String onFail = "STOP";
private int retry = 0;
private int timeoutSec = 60;
public String getKey() { return key; }
public void setKey(String key) { this.key = key; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getTaskKey() { return taskKey; }
public void setTaskKey(String taskKey) { this.taskKey = taskKey; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public List<String> getDependsOn() { return dependsOn; }
public void setDependsOn(List<String> dependsOn) { this.dependsOn = dependsOn; }
public String getOnFail() { return onFail; }
public void setOnFail(String onFail) { this.onFail = onFail; }
public int getRetry() { return retry; }
public void setRetry(int retry) { this.retry = retry; }
public int getTimeoutSec() { return timeoutSec; }
public void setTimeoutSec(int timeoutSec) { this.timeoutSec = timeoutSec; }
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : Task.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.model;
/**
任务实体(DDD 聚合根)
cron 为标准 5 字段:分 时 日 月 周(如 0 8 * * *)
*/
public class Task {
private long id;
private String name = "";
private String cronExpr = "";
private String command = "";
private int timeoutSec = 30;
private int maxRetry = 0;
private int retryGapSec = 5;
private boolean enabled = true;
private String remark = "";
private TaskStatus status = TaskStatus.IDLE;
/** 一次性任务:备注含"一次性",执行成功后自动禁用 */
public boolean isOneTime() {
return remark != null && remark.contains("一次性");
}
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getCronExpr() { return cronExpr; }
public void setCronExpr(String cronExpr) { this.cronExpr = cronExpr; }
public String getCommand() { return command; }
public void setCommand(String command) { this.command = command; }
public int getTimeoutSec() { return timeoutSec; }
public void setTimeoutSec(int timeoutSec) { this.timeoutSec = timeoutSec; }
public int getMaxRetry() { return maxRetry; }
public void setMaxRetry(int maxRetry) { this.maxRetry = maxRetry; }
public int getRetryGapSec() { return retryGapSec; }
public void setRetryGapSec(int retryGapSec) { this.retryGapSec = retryGapSec; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public TaskStatus getStatus() { return status; }
public void setStatus(TaskStatus status) { this.status = status; }
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : TaskHistory.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.model;
import java.time.LocalDateTime;
/** 任务执行历史实体 */
public class TaskHistory {
private long id;
private long taskId;
private LocalDateTime startTime;
private LocalDateTime endTime;
private boolean success;
private String message = "";
public long getId() { return id; }
public void setId(long id) { this.id = id; }
public long getTaskId() { return taskId; }
public void setTaskId(long taskId) { this.taskId = taskId; }
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
public boolean isSuccess() { return success; }
public void setSuccess(boolean success) { this.success = success; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public long costMillis() {
if (startTime == null || endTime == null) return 0;
return java.time.Duration.between(startTime, endTime).toMillis();
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : TaskStatus.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.model;
/** 任务状态枚举 /
public enum TaskStatus {
/* 空闲 /
IDLE,
/* 运行中 */
RUNNING;
public static TaskStatus of(String s) {
if (s == null) return IDLE;
try {
return valueOf(s.trim().toUpperCase());
} catch (IllegalArgumentException e) {
return IDLE;
}
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : DagRepository.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.repository;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import java.util.List;
/** DAG 工作流仓储接口 */
public interface DagRepository {
List<DagDefinition> findAll();
DagDefinition findById(long id);
DagDefinition save(DagDefinition dag);
boolean deleteById(long id);
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : TaskHistoryRepository.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.repository;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import java.util.List;
/** 任务历史仓储接口 */
public interface TaskHistoryRepository {
TaskHistory insert(TaskHistory history);
List<TaskHistory> listByTaskId(long taskId, int limit);
List<TaskHistory> listRecent(int limit);
/** 返回 [total, success, fail] */
long[] countStats();
long countToday();
/** 近 days 天每天次数(无记录补 0),返回 [日期yyyy-MM-dd, 次数] */
List<String[]> countByDay(int days);
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : TaskRepository.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.domain.repository;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import java.util.List;
/** 任务仓储接口(DDD 依赖倒置:基础设施层 JDBC 实现) */
public interface TaskRepository {
List<Task> findAll();
List<Task> findEnabled();
long count();
/** 关键字(名称/备注)分页,page 从 1 开始 */
List<Task> searchPage(String keyword, int page, int pageSize);
Task findById(long id);
/** 保存:id<=0 新增,否则更新;返回带 id 的实体 */
Task save(Task task);
boolean deleteById(long id);
boolean updateStatus(long id, TaskStatus status);
boolean updateEnabled(long id, boolean enabled);
}
java
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : Db.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.db;
import com.jewelry.taskscheduler.config.AppConfig;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/**
* H2 嵌入式数据库(DDD 基础设施层)
* 启动时 EnsureCreated 语义:表不存在则建表(与 .NET EF EnsureCreated 对齐,兼容既有库文件)
* 时间统一存 VARCHAR "yyyy-MM-dd HH:mm:ss"(与 .NET 版 Db.FormatTime 一致)
*/
public final class Db {
private Db() {
}
/** 初始化:建表(已存在自动跳过) */
public static void init() {
try (Connection c = open(); Statement st = c.createStatement()) {
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS t_task (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_name VARCHAR(200) NOT NULL,
cron_expr VARCHAR(100) NOT NULL,
command VARCHAR(1000) NOT NULL,
timeout_sec INT DEFAULT 30,
max_retry INT DEFAULT 0,
retry_gap_sec INT DEFAULT 5,
is_enable BOOLEAN DEFAULT TRUE,
remark VARCHAR(500) DEFAULT '',
task_status VARCHAR(20) DEFAULT 'IDLE'
)
""");
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS t_task_history (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
task_id BIGINT NOT NULL,
execute_start_time VARCHAR(30) NOT NULL,
execute_end_time VARCHAR(30) NOT NULL,
is_success BOOLEAN NOT NULL,
execute_msg VARCHAR(4000) DEFAULT ''
)
""");
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS t_dag (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
dag_name VARCHAR(200) NOT NULL,
cron_expr VARCHAR(100) DEFAULT '',
is_enable BOOLEAN DEFAULT TRUE,
nodes_json CLOB,
created_at VARCHAR(30) DEFAULT ''
)
""");
AppConfig.AppLog.info("H2 数据库初始化完成: " + AppConfig.INSTANCE.dbUrl);
} catch (Exception e) {
AppConfig.AppLog.error("H2 数据库初始化失败: " + e.getMessage());
throw new RuntimeException("数据库初始化失败", e);
}
}
/** 打开新连接(每次操作独立连接,与 .NET 短生命周期上下文一致) */
public static Connection open() throws java.sql.SQLException {
AppConfig c = AppConfig.INSTANCE;
return DriverManager.getConnection(c.dbUrl, c.dbUser, c.dbPassword);
}
/** 时间 → 文本(yyyy-MM-dd HH:mm:ss) */
public static String formatTime(java.time.LocalDateTime t) {
return t.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/** 文本 → 时间 */
public static java.time.LocalDateTime parseTime(String s) {
if (s == null || s.isBlank()) {
return java.time.LocalDateTime.now();
}
try {
return java.time.LocalDateTime.parse(s.trim(),
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} catch (Exception e) {
return java.time.LocalDateTime.now();
}
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : EventBusAdapter.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.event;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
/**
* 领域事件总线(Guava EventBus 封装,单例)
* 发布任务开始/完成/失败/DAG 事件;UI 与日志系统订阅
*/
public final class EventBusAdapter {
private static final EventBusAdapter INSTANCE = new EventBusAdapter();
private final ExecutorService executor = Executors.newFixedThreadPool(2, r -> {
Thread t = new Thread(r, "eventbus");
t.setDaemon(true);
return t;
});
private final EventBus syncBus = new EventBus("domain");
private final AsyncEventBus asyncBus = new AsyncEventBus("domain-async", executor);
private EventBusAdapter() {
}
public static EventBusAdapter instance() {
return INSTANCE;
}
/** 发布领域事件(UI 用同步,后台任务用异步) */
public void post(DomainEvent event) {
try {
syncBus.post(event);
} catch (Exception e) {
AppConfig.AppLog.warn("事件发布失败: " + e.getMessage());
}
}
public void postAsync(DomainEvent event) {
asyncBus.post(event);
}
/** 注册订阅者(对象中 @Subscribe 方法) */
public void register(Object listener) {
syncBus.register(listener);
asyncBus.register(listener);
}
/**
* 便捷订阅:某类型事件的消费者
* 注意:Guava 反射会擦除泛型参数类型,@Subscribe 方法必须用具体类型(DomainEvent 基类),
* 内部按 type instanceof 分发,避免 ClassCastException
*/
public <T extends DomainEvent> void on(Class<T> type, Consumer<T> consumer) {
register(new Object() {
@Subscribe
public void handle(DomainEvent event) {
if (type.isInstance(event)) {
consumer.accept(type.cast(event));
}
}
});
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : RedisSupport.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.redis;
import com.jewelry.taskscheduler.config.AppConfig;
import redis.clients.jedis.Jedis;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Redis 支持(可选,配置 redis.enabled=false 时全部跳过,纯本地模式)
* 能力:
* 1. 分布式锁(SET NX EX)------多实例任务防重复执行
* 2. 节点心跳上报(HSET + 过期)------集群节点列表展示
*/
public class RedisSupport {
private static final RedisSupport INSTANCE = new RedisSupport();
private volatile Jedis jedis;
private final String nodeId = UUID.randomUUID().toString().substring(0, 8);
private final ScheduledExecutorService heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "redis-heartbeat");
t.setDaemon(true);
return t;
});
public static RedisSupport instance() {
return INSTANCE;
}
public boolean enabled() {
return AppConfig.INSTANCE.redisEnabled;
}
/** 启动:连接 + 启动心跳上报 */
public void start() {
if (!enabled()) {
AppConfig.AppLog.info("Redis 未启用,运行于本地模式(分布式锁/心跳已跳过)");
return;
}
try {
jedis = new Jedis(AppConfig.INSTANCE.redisHost, AppConfig.INSTANCE.redisPort);
if (!AppConfig.INSTANCE.redisPassword.isBlank()) {
jedis.auth(AppConfig.INSTANCE.redisPassword);
}
jedis.ping();
AppConfig.AppLog.info("Redis 已连接 " + AppConfig.INSTANCE.redisHost + ":" + AppConfig.INSTANCE.redisPort
+ ",节点ID=" + nodeId);
// 心跳:每 5 秒上报一次
heartbeatExecutor.scheduleAtFixedRate(this::heartbeat, 1, 5, TimeUnit.SECONDS);
} catch (Exception e) {
AppConfig.AppLog.error("Redis 连接失败: " + e.getMessage() + "(本次运行跳过 Redis 能力)");
jedis = null;
}
}
public void shutdown() {
heartbeatExecutor.shutdownNow();
if (jedis != null) {
try {
jedis.close();
} catch (Exception ignored) {
}
}
}
/** 节点心跳上报:HSET 节点 → 时间戳 + 过期 */
private void heartbeat() {
try {
String key = AppConfig.INSTANCE.redisHeartbeatKey;
jedis.hset(key, nodeId, String.valueOf(System.currentTimeMillis()));
jedis.expire(key, AppConfig.INSTANCE.redisHeartbeatTimeoutSec + 15);
} catch (Exception e) {
AppConfig.AppLog.warn("心跳上报失败: " + e.getMessage());
}
}
/** 获取在线节点列表(nodeId → 最后心跳时间) */
public Map<String, String> listNodes() {
if (jedis == null || !enabled()) {
return Map.of();
}
try {
String key = AppConfig.INSTANCE.redisHeartbeatKey;
long timeout = AppConfig.INSTANCE.redisHeartbeatTimeoutSec * 1000L;
long now = System.currentTimeMillis();
Map<String, String> all = jedis.hgetAll(key);
// 过滤超时节点
all.entrySet().removeIf(e -> {
try {
return now - Long.parseLong(e.getValue()) > timeout;
} catch (Exception ex) {
return true;
}
});
return all;
} catch (Exception e) {
return Map.of();
}
}
/** 分布式锁:SET NX EX,成功返回 true */
public boolean tryLock(String key, int ttlSec) {
if (jedis == null || !enabled()) {
return true; // 本地模式视为已获锁(Quartz 已防同实例并发)
}
try {
String r = jedis.set(key, nodeId, new redis.clients.jedis.params.SetParams().nx().ex(ttlSec));
return "OK".equals(r);
} catch (Exception e) {
AppConfig.AppLog.warn("获取分布式锁失败 " + key + ": " + e.getMessage());
return true;
}
}
public void unlock(String key) {
if (jedis == null || !enabled()) {
return;
}
try {
jedis.del(key);
} catch (Exception ignored) {
}
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : DagRepositoryJdbc.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.repository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import com.jewelry.taskscheduler.domain.model.DagNode;
import com.jewelry.taskscheduler.domain.repository.DagRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/** DAG 工作流仓储 H2/JDBC 实现(nodes_json 存节点 JSON) */
public class DagRepositoryJdbc implements DagRepository {
private static final ObjectMapper JSON = new ObjectMapper();
@Override
public List<DagDefinition> findAll() {
List<DagDefinition> list = new ArrayList<>();
try (Connection c = Db.open();
Statement st = c.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM t_dag ORDER BY id")) {
while (rs.next()) {
list.add(map(rs));
}
} catch (Exception e) {
throw new RuntimeException("查询 DAG 失败", e);
}
return list;
}
@Override
public DagDefinition findById(long id) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("SELECT * FROM t_dag WHERE id = ?")) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? map(rs) : null;
}
} catch (Exception e) {
throw new RuntimeException("查询 DAG 失败 id=" + id, e);
}
}
@Override
public DagDefinition save(DagDefinition dag) {
if (dag.getId() <= 0) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("""
INSERT INTO t_dag(dag_name, cron_expr, is_enable, nodes_json, created_at)
VALUES (?,?,?,?,?)
""", Statement.RETURN_GENERATED_KEYS)) {
ps.setString(1, dag.getName());
ps.setString(2, dag.getCronExpr());
ps.setBoolean(3, dag.isEnabled());
ps.setString(4, nodesJson(dag));
ps.setString(5, Db.formatTime(dag.getCreatedAt() != null ? dag.getCreatedAt() : java.time.LocalDateTime.now()));
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
dag.setId(keys.getLong(1));
}
}
} catch (Exception e) {
throw new RuntimeException("新增 DAG 失败", e);
}
} else {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement(
"UPDATE t_dag SET dag_name=?, cron_expr=?, is_enable=?, nodes_json=? WHERE id=?")) {
ps.setString(1, dag.getName());
ps.setString(2, dag.getCronExpr());
ps.setBoolean(3, dag.isEnabled());
ps.setString(4, nodesJson(dag));
ps.setLong(5, dag.getId());
ps.executeUpdate();
} catch (Exception e) {
throw new RuntimeException("更新 DAG 失败 id=" + dag.getId(), e);
}
}
return dag;
}
@Override
public boolean deleteById(long id) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("DELETE FROM t_dag WHERE id = ?")) {
ps.setLong(1, id);
return ps.executeUpdate() > 0;
} catch (Exception e) {
throw new RuntimeException("删除 DAG 失败 id=" + id, e);
}
}
private String nodesJson(DagDefinition dag) {
try {
return JSON.writeValueAsString(dag.getNodes());
} catch (Exception e) {
throw new RuntimeException("DAG 节点序列化失败", e);
}
}
private DagDefinition map(ResultSet rs) throws java.sql.SQLException {
DagDefinition d = new DagDefinition();
d.setId(rs.getLong("id"));
d.setName(rs.getString("dag_name"));
d.setCronExpr(rs.getString("cron_expr"));
d.setEnabled(rs.getBoolean("is_enable"));
d.setCreatedAt(Db.parseTime(rs.getString("created_at")));
String json = rs.getString("nodes_json");
if (json != null && !json.isBlank()) {
try {
d.setNodes(JSON.readValue(json, new TypeReference<List<DagNode>>() {
}));
} catch (Exception e) {
throw new RuntimeException("DAG 节点反序列化失败 id=" + d.getId(), e);
}
}
return d;
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : TaskHistoryRepositoryJdbc.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.repository;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.repository.TaskHistoryRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** 任务历史仓储 H2/JDBC 实现 */
public class TaskHistoryRepositoryJdbc implements TaskHistoryRepository {
@Override
public TaskHistory insert(TaskHistory h) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("""
INSERT INTO t_task_history(task_id, execute_start_time, execute_end_time, is_success, execute_msg)
VALUES (?,?,?,?,?)
""", Statement.RETURN_GENERATED_KEYS)) {
ps.setLong(1, h.getTaskId());
ps.setString(2, Db.formatTime(h.getStartTime()));
ps.setString(3, Db.formatTime(h.getEndTime()));
ps.setBoolean(4, h.isSuccess());
ps.setString(5, truncate(h.getMessage(), 4000));
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
h.setId(keys.getLong(1));
}
}
return h;
} catch (Exception e) {
throw new RuntimeException("写入执行历史失败", e);
}
}
@Override
public List<TaskHistory> listByTaskId(long taskId, int limit) {
List<TaskHistory> list = new ArrayList<>();
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement(
"SELECT * FROM t_task_history WHERE task_id=? ORDER BY id DESC LIMIT ?")) {
ps.setLong(1, taskId);
ps.setInt(2, limit);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(map(rs));
}
}
} catch (Exception e) {
throw new RuntimeException("查询任务历史失败 taskId=" + taskId, e);
}
return list;
}
@Override
public List<TaskHistory> listRecent(int limit) {
List<TaskHistory> list = new ArrayList<>();
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement(
"SELECT * FROM t_task_history ORDER BY id DESC LIMIT ?")) {
ps.setInt(1, limit);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(map(rs));
}
}
} catch (Exception e) {
throw new RuntimeException("查询最近历史失败", e);
}
return list;
}
@Override
public long[] countStats() {
try (Connection c = Db.open();
Statement st = c.createStatement();
ResultSet rs = st.executeQuery(
"SELECT COUNT(*), SUM(CASE WHEN is_success THEN 1 ELSE 0 END) FROM t_task_history")) {
if (rs.next()) {
long total = rs.getLong(1);
long ok = rs.getLong(2);
return new long[]{total, ok, total - ok};
}
} catch (Exception e) {
throw new RuntimeException("统计历史失败", e);
}
return new long[]{0, 0, 0};
}
@Override
public long countToday() {
String today = LocalDate.now().toString();
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement(
"SELECT COUNT(*) FROM t_task_history WHERE execute_start_time LIKE ?")) {
ps.setString(1, today + "%");
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getLong(1) : 0;
}
} catch (Exception e) {
throw new RuntimeException("统计今日失败", e);
}
}
@Override
public List<String[]> countByDay(int days) {
Map<String, Long> count = new HashMap<>();
try (Connection c = Db.open();
Statement st = c.createStatement();
ResultSet rs = st.executeQuery("SELECT execute_start_time FROM t_task_history")) {
while (rs.next()) {
String time = rs.getString(1);
if (time != null && time.length() >= 10) {
count.merge(time.substring(0, 10), 1L, Long::sum);
}
}
} catch (Exception e) {
throw new RuntimeException("统计按天失败", e);
}
// 近 days 天补 0
List<String[]> result = new ArrayList<>(days);
DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate today = LocalDate.now();
for (int i = 0; i < days; i++) {
String day = today.minusDays(days - 1 - i).format(f);
result.add(new String[]{day, String.valueOf(count.getOrDefault(day, 0L))});
}
return result;
}
private TaskHistory map(ResultSet rs) throws java.sql.SQLException {
TaskHistory h = new TaskHistory();
h.setId(rs.getLong("id"));
h.setTaskId(rs.getLong("task_id"));
h.setStartTime(Db.parseTime(rs.getString("execute_start_time")));
h.setEndTime(Db.parseTime(rs.getString("execute_end_time")));
h.setSuccess(rs.getBoolean("is_success"));
h.setMessage(rs.getString("execute_msg"));
return h;
}
private static String truncate(String s, int max) {
if (s == null) {
return "";
}
return s.length() <= max ? s : s.substring(0, max);
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : TaskRepositoryJdbc.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.repository;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
/** 任务仓储 H2/JDBC 实现(公共方法与接口语义对齐 .NET TaskRepo) */
public class TaskRepositoryJdbc implements TaskRepository {
@Override
public List<Task> findAll() {
List<Task> list = new ArrayList<>();
try (Connection c = Db.open();
Statement st = c.createStatement();
ResultSet rs = st.executeQuery("SELECT * FROM t_task ORDER BY id")) {
while (rs.next()) {
list.add(map(rs));
}
} catch (Exception e) {
throw new RuntimeException("查询任务失败", e);
}
return list;
}
@Override
public List<Task> findEnabled() {
List<Task> list = new ArrayList<>();
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("SELECT * FROM t_task WHERE is_enable = TRUE ORDER BY id")) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(map(rs));
}
}
} catch (Exception e) {
throw new RuntimeException("查询启用任务失败", e);
}
return list;
}
@Override
public long count() {
try (Connection c = Db.open();
Statement st = c.createStatement();
ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM t_task")) {
return rs.next() ? rs.getLong(1) : 0;
} catch (Exception e) {
throw new RuntimeException("统计任务失败", e);
}
}
@Override
public List<Task> searchPage(String keyword, int page, int pageSize) {
if (page < 1) {
page = 1;
}
if (pageSize < 1) {
pageSize = 10;
}
List<Task> list = new ArrayList<>();
String sql = "SELECT * FROM t_task";
if (keyword != null && !keyword.isBlank()) {
sql += " WHERE task_name LIKE ? OR remark LIKE ?";
}
sql += " ORDER BY id LIMIT ? OFFSET ?";
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement(sql)) {
int i = 1;
if (keyword != null && !keyword.isBlank()) {
String kw = "%" + keyword.trim() + "%";
ps.setString(i++, kw);
ps.setString(i++, kw);
}
ps.setInt(i++, pageSize);
ps.setInt(i, (page - 1) * pageSize);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(map(rs));
}
}
} catch (Exception e) {
throw new RuntimeException("分页查询任务失败", e);
}
return list;
}
@Override
public Task findById(long id) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("SELECT * FROM t_task WHERE id = ?")) {
ps.setLong(1, id);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? map(rs) : null;
}
} catch (Exception e) {
throw new RuntimeException("查询任务失败 id=" + id, e);
}
}
@Override
public Task save(Task task) {
if (task.getId() <= 0) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("""
INSERT INTO t_task(task_name, cron_expr, command, timeout_sec, max_retry, retry_gap_sec, is_enable, remark, task_status)
VALUES (?,?,?,?,?,?,?,?,?)
""", Statement.RETURN_GENERATED_KEYS)) {
fill(ps, task);
ps.executeUpdate();
try (ResultSet keys = ps.getGeneratedKeys()) {
if (keys.next()) {
task.setId(keys.getLong(1));
}
}
} catch (Exception e) {
throw new RuntimeException("新增任务失败", e);
}
} else {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("""
UPDATE t_task SET task_name=?, cron_expr=?, command=?, timeout_sec=?, max_retry=?,
retry_gap_sec=?, is_enable=?, remark=?, task_status=? WHERE id=?
""")) {
fill(ps, task);
ps.setLong(10, task.getId());
ps.executeUpdate();
} catch (Exception e) {
throw new RuntimeException("更新任务失败 id=" + task.getId(), e);
}
}
return task;
}
private void fill(PreparedStatement ps, Task t) throws java.sql.SQLException {
ps.setString(1, t.getName());
ps.setString(2, t.getCronExpr());
ps.setString(3, t.getCommand());
ps.setInt(4, t.getTimeoutSec());
ps.setInt(5, t.getMaxRetry());
ps.setInt(6, t.getRetryGapSec());
ps.setBoolean(7, t.isEnabled());
ps.setString(8, t.getRemark());
ps.setString(9, t.getStatus().name());
}
@Override
public boolean deleteById(long id) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("DELETE FROM t_task WHERE id = ?")) {
ps.setLong(1, id);
return ps.executeUpdate() > 0;
} catch (Exception e) {
throw new RuntimeException("删除任务失败 id=" + id, e);
}
}
@Override
public boolean updateStatus(long id, TaskStatus status) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("UPDATE t_task SET task_status=? WHERE id=?")) {
ps.setString(1, status.name());
ps.setLong(2, id);
return ps.executeUpdate() > 0;
} catch (Exception e) {
throw new RuntimeException("更新任务状态失败 id=" + id, e);
}
}
@Override
public boolean updateEnabled(long id, boolean enabled) {
try (Connection c = Db.open();
PreparedStatement ps = c.prepareStatement("UPDATE t_task SET is_enable=? WHERE id=?")) {
ps.setBoolean(1, enabled);
ps.setLong(2, id);
return ps.executeUpdate() > 0;
} catch (Exception e) {
throw new RuntimeException("更新任务启用状态失败 id=" + id, e);
}
}
private Task map(ResultSet rs) throws java.sql.SQLException {
Task t = new Task();
t.setId(rs.getLong("id"));
t.setName(rs.getString("task_name"));
t.setCronExpr(rs.getString("cron_expr"));
t.setCommand(rs.getString("command"));
t.setTimeoutSec(rs.getInt("timeout_sec"));
t.setMaxRetry(rs.getInt("max_retry"));
t.setRetryGapSec(rs.getInt("retry_gap_sec"));
t.setEnabled(rs.getBoolean("is_enable"));
t.setRemark(rs.getString("remark"));
t.setStatus(TaskStatus.of(rs.getString("task_status")));
return t;
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : CronCompat.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.scheduler;
/**
* Cron 兼容转换:标准 5 字段 → Quartz 6 字段
* 5字段: 分 时 日 月 周(周日 0/7,支持 * , - /)
* 6字段: 秒 分 时 日 月 周(Quartz 周字段 1-7,日/周互斥用 ?)
*/
public final class CronCompat {
private CronCompat() {
}
/** 5 字段 → Quartz 6 字段 */
public static String toQuartz(String cron) {
if (cron == null) {
throw new IllegalArgumentException("cron 不能为空");
}
String[] parts = cron.trim().split("\\s+");
if (parts.length != 5) {
throw new IllegalArgumentException("cron 必须是 5 字段(分 时 日 月 周): " + cron);
}
String minute = parts[0];
String hour = parts[1];
String dom = parts[2];
String month = parts[3];
String dow = parts[4];
// 日与周互斥:指定了日期(非 *)→ 周用 ?;否则日用 ?
if (dom.equals("*")) {
dom = "?";
} else {
dow = "?";
}
return "0 " + minute + " " + hour + " " + dom + " " + month + " " + normalizeDow(dow);
}
/** 周字段 0-6 → 1-7(Quartz 周 1=周日):0→7,其余 +1;* / ? 原样返回 */
private static String normalizeDow(String dow) {
if (dow == null || dow.equals("?") || dow.equals("*")) {
return dow;
}
StringBuilder sb = new StringBuilder();
for (String part : dow.split(",")) {
if (sb.length() > 0) {
sb.append(',');
}
String p = part.trim();
if (p.matches("\\d+")) {
int v = Integer.parseInt(p);
if (v == 0) {
v = 7;
} else if (v > 0 && v <= 6) {
v++;
} else {
throw new IllegalArgumentException("周字段取值 0-6 或 7 之外: " + dow);
}
sb.append(v);
} else if (p.contains("-")) {
// 范围 0-6 / 1-5
String[] r = p.split("-");
int lo = Integer.parseInt(r[0].trim());
int hi = Integer.parseInt(r[1].trim());
lo = lo == 0 ? 7 : lo + 1;
hi = hi == 0 ? 7 : hi + 1;
sb.append(lo).append('-').append(hi);
} else if (p.contains("/")) {
// 步长 0/2 → 7/2? 语义近似:保留起始映射
String[] r = p.split("/");
int base = Integer.parseInt(r[0].trim());
base = base == 0 ? 7 : base + 1;
sb.append(base).append('/').append(r[1].trim());
} else {
throw new IllegalArgumentException("不支持的周字段: " + dow);
}
}
return sb.toString();
}
/** 校验 5 字段 cron 是否合法(Quartz 可解析) */
public static void validate(String cron) {
String q = toQuartz(cron);
try {
new org.quartz.CronExpression(q);
} catch (Exception e) {
throw new IllegalArgumentException("cron 无效(" + cron + " → " + q + "): " + e.getMessage(), e);
}
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : DagTriggerJob.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.scheduler;
import com.jewelry.taskscheduler.application.DagService;
import com.jewelry.taskscheduler.config.AppConfig;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
/** Quartz DAG 触发 Job:到点执行整个 DAG 工作流 */
@DisallowConcurrentExecution
public class DagTriggerJob implements Job {
@Override
public void execute(JobExecutionContext context) {
long dagId = context.getMergedJobDataMap().getLong("dagId");
AppConfig.AppLog.info("Quartz 触发 DAG dag_id=" + dagId);
DagService.instance().runAsync(dagId);
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : QuartzSchedulerService.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.scheduler;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;
import java.util.List;
/**
* Quartz 调度服务(基础设施层,单例)
* 启动注册全部启用任务;注册/注销/暂停/恢复/手动触发/下次执行时间
* 对应 .NET QuartzSchedulerService
*/
public class QuartzSchedulerService {
private static final QuartzSchedulerService INSTANCE = new QuartzSchedulerService();
private static final String GROUP = "tasks";
private final TaskRepository taskRepo = new TaskRepositoryJdbc();
private Scheduler scheduler;
public static QuartzSchedulerService instance() {
return INSTANCE;
}
/** 启动调度器并注册全部启用任务 */
public synchronized void start() {
if (scheduler != null) {
return;
}
try {
scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
List<Task> enabled = taskRepo.findEnabled();
for (Task t : enabled) {
try {
register(t);
} catch (Exception e) {
AppConfig.AppLog.error("Quartz 注册任务失败 task_id=" + t.getId() + ": " + e.getMessage());
}
}
AppConfig.AppLog.info("Quartz 调度器已启动,共注册 " + enabled.size() + " 个启用任务");
} catch (SchedulerException e) {
throw new RuntimeException("Quartz 启动失败", e);
}
}
public synchronized void shutdown() {
if (scheduler != null) {
try {
scheduler.shutdown(true);
AppConfig.AppLog.info("Quartz 调度器已停止");
} catch (SchedulerException e) {
AppConfig.AppLog.warn("Quartz 停止失败: " + e.getMessage());
}
scheduler = null;
}
}
public boolean isRunning() {
if (scheduler == null) {
return false;
}
try {
return scheduler.isStarted();
} catch (SchedulerException e) {
return false;
}
}
public String stateText() {
if (scheduler == null) {
return "未启动";
}
try {
return scheduler.isStarted() ? "运行中" : "已停止";
} catch (SchedulerException e) {
return "异常";
}
}
/** 注册任务(cron 触发,重复注册覆盖) */
public void register(Task task) throws SchedulerException {
ensure();
String cron6 = CronCompat.toQuartz(task.getCronExpr());
JobDetail job = JobBuilder.newJob(TaskExecutionJob.class)
.withIdentity("task-" + task.getId(), GROUP)
.usingJobData(new JobDataMap(java.util.Map.of("taskId", task.getId())))
.build();
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger-" + task.getId(), GROUP)
.withSchedule(CronScheduleBuilder.cronSchedule(cron6))
.forJob(job)
.build();
// Java 版无 scheduleJob(job, trigger, replace) 重载:先删旧注册实现覆盖语义
scheduler.deleteJob(job.getKey());
scheduler.scheduleJob(job, trigger);
AppConfig.AppLog.info("Quartz 注册任务 task_id=" + task.getId() + " cron=" + task.getCronExpr() + " → " + cron6);
}
/** 注销任务 */
public void unregister(long taskId) throws SchedulerException {
if (scheduler == null) {
return;
}
scheduler.unscheduleJob(TriggerKey.triggerKey("trigger-" + taskId, GROUP));
scheduler.deleteJob(JobKey.jobKey("task-" + taskId, GROUP));
AppConfig.AppLog.info("Quartz 注销任务 task_id=" + taskId);
}
/** 暂停任务 */
public void pause(long taskId) throws SchedulerException {
if (scheduler != null) {
scheduler.pauseJob(JobKey.jobKey("task-" + taskId, GROUP));
AppConfig.AppLog.info("Quartz 暂停任务 task_id=" + taskId);
}
}
/** 恢复任务 */
public void resume(long taskId) throws SchedulerException {
if (scheduler != null) {
scheduler.resumeJob(JobKey.jobKey("task-" + taskId, GROUP));
AppConfig.AppLog.info("Quartz 恢复任务 task_id=" + taskId);
}
}
/** 立即执行(手动触发,不影响 cron 计划):直接触发已注册 Job */
public void runOnce(Task task) throws SchedulerException {
ensure();
JobKey key = JobKey.jobKey("task-" + task.getId(), GROUP);
if (!scheduler.checkExists(key)) {
// 任务未注册到 Quartz(如禁用状态)时先注册再触发
register(task);
}
scheduler.triggerJob(key);
AppConfig.AppLog.info("Quartz 手动触发任务 task_id=" + task.getId());
}
/** 注册 DAG(cron 触发整个工作流) */
public void registerDag(DagDefinition dag) throws SchedulerException {
ensure();
if (dag.getCronExpr() == null || dag.getCronExpr().isBlank()) {
return;
}
String cron6 = CronCompat.toQuartz(dag.getCronExpr());
JobDetail job = JobBuilder.newJob(DagTriggerJob.class)
.withIdentity("dag-" + dag.getId(), GROUP)
.usingJobData(new JobDataMap(java.util.Map.of("dagId", dag.getId())))
.build();
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("dagtrigger-" + dag.getId(), GROUP)
.withSchedule(CronScheduleBuilder.cronSchedule(cron6))
.forJob(job)
.build();
scheduler.deleteJob(job.getKey());
scheduler.scheduleJob(job, trigger);
AppConfig.AppLog.info("Quartz 注册 DAG 任务 dag_id=" + dag.getId() + " cron=" + dag.getCronExpr() + " → " + cron6);
}
public void unregisterDag(long dagId) throws SchedulerException {
if (scheduler == null) {
return;
}
scheduler.unscheduleJob(TriggerKey.triggerKey("dagtrigger-" + dagId, GROUP));
scheduler.deleteJob(JobKey.jobKey("dag-" + dagId, GROUP));
}
/** 任务下次触发时间 */
public Date nextFireTime(long taskId) {
if (scheduler == null) {
return null;
}
try {
Trigger t = scheduler.getTrigger(TriggerKey.triggerKey("trigger-" + taskId, GROUP));
return t == null ? null : t.getNextFireTime();
} catch (SchedulerException e) {
return null;
}
}
private void ensure() throws SchedulerException {
if (scheduler == null) {
throw new SchedulerException("Quartz 调度器尚未启动");
}
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : TaskExecutionJob.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.infrastructure.scheduler;
import com.jewelry.taskscheduler.application.TaskExecutionService;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.event.EventBusAdapter;
import com.jewelry.taskscheduler.infrastructure.redis.RedisSupport;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
/**
* Quartz 任务执行 Job
* @DisallowConcurrentExecution 防止同一任务并发(替代旧运行锁)
* 流程:置 RUNNING → 分布式锁(可选)→ 执行(子进程+重试+写历史+事件)→ 一次性自禁用 → 置 IDLE
*/
@DisallowConcurrentExecution
public class TaskExecutionJob implements Job {
private final TaskRepository taskRepo = new TaskRepositoryJdbc();
private final TaskExecutionService executionService = new TaskExecutionService();
@Override
public void execute(JobExecutionContext context) {
long taskId = context.getMergedJobDataMap().getLong("taskId");
Task task = taskRepo.findById(taskId);
if (task == null) {
AppConfig.AppLog.warn("Quartz Job 找不到任务 task_id=" + taskId + ",已忽略执行");
return;
}
// 分布式锁(Redis 开启时):防多实例重复执行同一任务
String lockKey = "taskscheduler:lock:task:" + taskId;
boolean locked = false;
if (RedisSupport.instance().enabled()) {
locked = RedisSupport.instance().tryLock(lockKey, 300);
if (!locked) {
AppConfig.AppLog.warn("任务 task_id=" + taskId + " 已被其他节点执行,本次跳过");
return;
}
}
try {
taskRepo.updateStatus(taskId, TaskStatus.RUNNING);
TaskHistory history = executionService.executeSingle(task);
if (history != null) {
AppConfig.AppLog.info("Quartz 执行完成 task_id=" + taskId
+ " 结果=" + (history.isSuccess() ? "成功" : "失败"));
}
// 一次性任务(备注含"一次性")执行成功后自动禁用,真正"只执行一次"
if (task.isOneTime() && history != null && history.isSuccess()) {
taskRepo.updateEnabled(taskId, false);
AppConfig.AppLog.info("一次性任务 task_id=" + taskId + "【" + task.getName() + "】执行完成,已自动禁用");
QuartzSchedulerService.instance().unregister(taskId);
}
} catch (Exception e) {
AppConfig.AppLog.error("Quartz 任务执行异常 task_id=" + taskId + ": " + e.getMessage());
EventBusAdapter.instance().post(new DomainEvent.TaskFailedEvent(taskId, task.getName(), e.getMessage()));
} finally {
taskRepo.updateStatus(taskId, TaskStatus.IDLE);
if (locked) {
RedisSupport.instance().unlock(lockKey);
}
}
}
}
java
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : DagService.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.application;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
import com.jewelry.taskscheduler.domain.model.DagDefinition;
import com.jewelry.taskscheduler.domain.model.DagNode;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.DagRepository;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.event.EventBusAdapter;
import com.jewelry.taskscheduler.infrastructure.repository.DagRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import org.quartz.SchedulerException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* DAG 工作流服务(应用层):
* 任务依赖编排(A 完成后执行 B,支持并行分支),依赖失败按 onFail 策略 STOP/SKIP
* 支持 cron 定时触发整个 DAG(经 Quartz DagTriggerJob)
*/
public class DagService {
private static final DagService INSTANCE = new DagService();
private final DagRepository dagRepo = new DagRepositoryJdbc();
private final TaskRepository taskRepo = new TaskRepositoryJdbc();
private final TaskExecutionService executionService = new TaskExecutionService();
private final ExecutorService executor = Executors.newCachedThreadPool(r -> {
Thread t = new Thread(r, "dag-exec");
t.setDaemon(true);
return t;
});
public static DagService instance() {
return INSTANCE;
}
public List<DagDefinition> findAll() {
return dagRepo.findAll();
}
public DagDefinition findById(long id) {
return dagRepo.findById(id);
}
/** 保存 DAG 并同步 Quartz(有 cron 则注册触发) */
public DagDefinition save(DagDefinition dag) {
if (dag.getCreatedAt() == null) {
dag.setCreatedAt(LocalDateTime.now());
}
DagDefinition saved = dagRepo.save(dag);
syncScheduler(saved);
return saved;
}
public boolean delete(long id) {
boolean ok = dagRepo.deleteById(id);
if (ok) {
try {
QuartzSchedulerService.instance().unregisterDag(id);
} catch (SchedulerException e) {
AppConfig.AppLog.warn("删除 DAG 后注销 Quartz 失败 id=" + id);
}
}
return ok;
}
private void syncScheduler(DagDefinition dag) {
try {
if (dag.isEnabled() && dag.getCronExpr() != null && !dag.getCronExpr().isBlank()) {
QuartzSchedulerService.instance().registerDag(dag);
} else {
QuartzSchedulerService.instance().unregisterDag(dag.getId());
}
} catch (SchedulerException e) {
AppConfig.AppLog.error("同步 DAG 调度失败 dag_id=" + dag.getId() + ": " + e.getMessage());
}
}
/** 后台异步执行 DAG */
public void runAsync(long dagId) {
executor.submit(() -> run(dagId));
}
/** 执行 DAG:Kahn 拓扑排序,按依赖串行/并行执行节点 */
public boolean run(long dagId) {
DagDefinition dag = dagRepo.findById(dagId);
if (dag == null) {
AppConfig.AppLog.error("DAG 不存在 id=" + dagId);
return false;
}
AppConfig.AppLog.info("DAG 开始执行 dag_id=" + dagId + " 名称=" + dag.getName()
+ " 节点数=" + dag.getNodes().size());
// 节点 key → 节点
Map<String, DagNode> byKey = new HashMap<>();
for (DagNode n : dag.getNodes()) {
byKey.put(n.getKey(), n);
}
// 入度
Map<String, Integer> indegree = new HashMap<>();
Map<String, List<String>> dependents = new HashMap<>();
for (DagNode n : dag.getNodes()) {
indegree.put(n.getKey(), 0);
dependents.put(n.getKey(), new ArrayList<>());
}
for (DagNode n : dag.getNodes()) {
for (String dep : n.getDependsOn()) {
if (byKey.containsKey(dep)) {
indegree.merge(n.getKey(), 1, Integer::sum);
dependents.get(dep).add(n.getKey());
}
}
}
Queue<String> ready = new LinkedList<>();
for (Map.Entry<String, Integer> e : indegree.entrySet()) {
if (e.getValue() == 0) {
ready.add(e.getKey());
}
}
Set<String> executed = new HashSet<>();
Set<String> failed = new HashSet<>();
boolean stopped = false;
int executedCount = 0;
while (!ready.isEmpty() && !stopped) {
String key = ready.poll();
if (executed.contains(key)) {
continue;
}
DagNode node = byKey.get(key);
// 依赖中是否有失败节点
boolean depFailed = node.getDependsOn().stream().anyMatch(failed::contains);
if (depFailed) {
// 上游失败:SKIP 节点按跳过处理;STOP 节点阻断整个 DAG
String failPolicy = node.getOnFail() == null ? "STOP" : node.getOnFail();
if ("SKIP".equalsIgnoreCase(failPolicy)) {
AppConfig.AppLog.warn("DAG 节点 " + node.getKey() + " 因上游失败已跳过(SKIP)");
executed.add(key);
executedCount++;
} else {
AppConfig.AppLog.warn("DAG 节点 " + node.getKey() + " 上游失败,按 STOP 策略阻断下游");
stopped = true;
break;
}
}
if (!depFailed) {
boolean ok = executeNode(dag, node);
executed.add(key);
executedCount++;
if (ok) {
AppConfig.AppLog.info("DAG 节点执行成功: " + node.getKey());
} else {
failed.add(key);
AppConfig.AppLog.error("DAG 节点执行失败: " + node.getKey() + "(onFail=" + node.getOnFail() + ")");
// 节点失败:本节点无下游则继续,有下游则按下游策略处理
}
}
// 解锁下游
for (String d : dependents.getOrDefault(key, List.of())) {
indegree.merge(d, -1, Integer::sum);
if (indegree.get(d) == 0) {
ready.add(d);
}
}
}
boolean success = failed.isEmpty() && !stopped;
AppConfig.AppLog.info("DAG 执行完成 dag_id=" + dagId + " 结果=" + (success ? "成功" : "失败")
+ " 执行节点=" + executedCount + "/" + dag.getNodes().size());
EventBusAdapter.instance().post(new DomainEvent.DagExecutedEvent(dagId, dag.getName(), success));
return success;
}
/** 执行单个 DAG 节点:优先绑定任务(taskKey),否则内联命令 */
private boolean executeNode(DagDefinition dag, DagNode node) {
String command = null;
int timeout = node.getTimeoutSec() > 0 ? node.getTimeoutSec() : 60;
if (node.getTaskKey() != null && !node.getTaskKey().isBlank()) {
// taskKey 格式:task-{id}
try {
long tid = Long.parseLong(node.getTaskKey().replace("task-", "").trim());
Task t = taskRepo.findById(tid);
if (t != null) {
command = t.getCommand();
timeout = t.getTimeoutSec();
}
} catch (NumberFormatException ignored) {
}
}
if (command == null && node.getCommand() != null && !node.getCommand().isBlank()) {
command = node.getCommand();
}
if (command == null) {
AppConfig.AppLog.error("DAG 节点 " + node.getKey() + " 无命令且未绑定任务,执行失败");
return false;
}
// 重试
int attempts = 1 + Math.max(0, node.getRetry());
for (int i = 0; i < attempts; i++) {
TaskExecutionService.RunOutcome outcome = executionService.executeCommand(command, timeout);
if (outcome.success) {
return true;
}
if (i < attempts - 1) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
return false;
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : DashboardService.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.application;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskStatus;
import com.jewelry.taskscheduler.domain.repository.TaskHistoryRepository;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.repository.TaskHistoryRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import java.util.List;
/**
* 仪表盘服务(应用层):统计面板数据 + 近7天柱状图 + 最近执行记录
*/
public class DashboardService {
private final TaskRepository taskRepo = new TaskRepositoryJdbc();
private final TaskHistoryRepository historyRepo = new TaskHistoryRepositoryJdbc();
public long taskTotal() {
return taskRepo.count();
}
public long taskEnabled() {
return taskRepo.findEnabled().size();
}
public long taskRunning() {
return taskRepo.findAll().stream().filter(t -> t.getStatus() == TaskStatus.RUNNING).count();
}
/** 今日成功/失败(按最近 200 条统计) */
public long[] todayOkFail() {
long ok = 0;
long fail = 0;
String today = java.time.LocalDate.now().toString();
for (TaskHistory h : historyRepo.listRecent(200)) {
if (h.getStartTime() != null && h.getStartTime().toLocalDate().toString().equals(today)) {
if (h.isSuccess()) {
ok++;
} else {
fail++;
}
}
}
return new long[]{ok, fail};
}
public long[] countStats() {
return historyRepo.countStats();
}
public List<TaskHistory> listRecent(int limit) {
return historyRepo.listRecent(limit);
}
/** 近7天执行次数(含补0),返回 [日期, 次数] */
public List<String[]> countByDay(int days) {
return historyRepo.countByDay(days);
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : TaskExecutionService.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.application;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.event.DomainEvent;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import com.jewelry.taskscheduler.domain.repository.TaskHistoryRepository;
import com.jewelry.taskscheduler.infrastructure.event.EventBusAdapter;
import com.jewelry.taskscheduler.infrastructure.repository.TaskHistoryRepositoryJdbc;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 任务执行服务(应用层):子进程执行 + 超时 + 失败重试 + 写历史 + 领域事件
* 对应 .NET TaskSchedulerService.ExecuteSingleTask
*/
public class TaskExecutionService {
private final TaskHistoryRepository historyRepo = new TaskHistoryRepositoryJdbc();
/** 执行单任务(含重试),返回最后一条历史 */
public TaskHistory executeSingle(Task task) {
int attempts = 1 + Math.max(0, task.getMaxRetry());
TaskHistory last = null;
for (int i = 0; i < attempts; i++) {
last = executeOnce(task);
if (last.isSuccess()) {
return last;
}
if (i < attempts - 1) {
AppConfig.AppLog.warn(String.format("task_id=%d 执行失败,第%d次重试,间隔%d秒",
task.getId(), i + 1, task.getRetryGapSec()));
try {
Thread.sleep(Math.max(1, task.getRetryGapSec()) * 1000L);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
return last;
}
/** 执行一次任务并写历史 */
private TaskHistory executeOnce(Task task) {
LocalDateTime start = LocalDateTime.now();
AppConfig.AppLog.info("开始执行任务 task_id=" + task.getId() + " 任务名称=" + task.getName());
EventBusAdapter.instance().post(new DomainEvent.TaskStartedEvent(task.getId(), task.getName()));
RunOutcome outcome = executeCommand(task.getCommand(), task.getTimeoutSec());
LocalDateTime end = LocalDateTime.now();
TaskHistory h = new TaskHistory();
h.setTaskId(task.getId());
h.setStartTime(start);
h.setEndTime(end);
h.setSuccess(outcome.success);
h.setMessage(outcome.output);
historyRepo.insert(h);
if (outcome.success) {
AppConfig.AppLog.info("任务执行成功 task_id=" + task.getId());
EventBusAdapter.instance().post(new DomainEvent.TaskCompletedEvent(task.getId(), task.getName(), outcome.output));
} else {
AppConfig.AppLog.error("任务执行失败 task_id=" + task.getId() + ",输出=" + outcome.output);
EventBusAdapter.instance().post(new DomainEvent.TaskFailedEvent(task.getId(), task.getName(), outcome.output));
}
return h;
}
/** 执行命令(无历史记录,DAG 节点使用) */
public RunOutcome executeCommand(String command, int timeoutSec) {
// 执行前自动创建输出目标目录(echo >> logs\xx.log、copy ... db_backup\xx.db)
prepareOutputDirs(command);
try {
String os = System.getProperty("os.name", "").toLowerCase();
ProcessBuilder pb = os.contains("win")
? new ProcessBuilder("cmd.exe", "/C", command)
: new ProcessBuilder("/bin/sh", "-c", command);
pb.redirectErrorStream(true);
Process p = pb.start();
byte[] out = readAll(p.getInputStream());
boolean finished = p.waitFor(Math.max(1, timeoutSec), TimeUnit.SECONDS);
if (!finished) {
p.destroyForcibly();
return new RunOutcome(false, "【超时】超过" + timeoutSec + "秒被强制终止");
}
String output = decodeText(out);
boolean ok = p.exitValue() == 0;
if (!ok && output.isBlank()) {
output = "退出码: " + p.exitValue();
}
return new RunOutcome(ok, output);
} catch (Exception e) {
return new RunOutcome(false, "启动进程失败: " + e.getMessage());
}
}
/** 执行结果 */
public static class RunOutcome {
public final boolean success;
public final String output;
public RunOutcome(boolean success, String output) {
this.success = success;
this.output = output;
}
}
/** 读取全部字节 */
private static byte[] readAll(InputStream in) throws java.io.IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int n;
while ((n = in.read(buf)) != -1) {
bos.write(buf, 0, n);
}
return bos.toByteArray();
}
/**
* 智能解码:UTF-8 严格解码 + round-trip 校验,失败回退系统本地编码(Windows=GBK)
* 修复 cmd 中文输出乱码
*/
private static String decodeText(byte[] bytes) {
if (bytes.length == 0) {
return "";
}
// 1) UTF-8 严格解码 + round-trip 校验(重编码字节与原文一致才认定为 UTF-8 文本)
try {
String s = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(java.nio.ByteBuffer.wrap(bytes)).toString();
java.nio.ByteBuffer re = StandardCharsets.UTF_8.encode(s);
byte[] reBytes = new byte[re.remaining()];
re.get(reBytes);
if (java.util.Arrays.equals(reBytes, bytes)) {
return s;
}
} catch (CharacterCodingException | RuntimeException ignored) {
// 非 UTF-8,走回退
}
// 2) 回退系统本地编码(中文 Windows 为 GBK)
try {
return new String(bytes, Charset.forName(System.getProperty("sun.jnu.encoding", "GBK")));
} catch (Exception e) {
return new String(bytes, StandardCharsets.UTF_8);
}
}
/** 执行前自动创建命令输出目标目录(重定向目标 / copy 目标) */
private static void prepareOutputDirs(String command) {
if (command == null || command.isBlank()) {
return;
}
List<String> targets = new java.util.ArrayList<>();
// 重定向:>> / > 后路径
Pattern redirect = Pattern.compile("(?:>>|>)\\s*(?:\"([^\"]+)\"|([^\\s&|;<>]+))");
Matcher m = redirect.matcher(command);
while (m.find()) {
String t = m.group(1) != null ? m.group(1) : m.group(2);
if (t != null && !t.isBlank()) {
targets.add(t.trim());
}
}
// copy 命令目标
Matcher cm = Pattern.compile("\\bcopy\\b[^\\r\\n]*", Pattern.CASE_INSENSITIVE).matcher(command);
if (cm.find()) {
String[] parts = cm.group().split("[ \\t]+");
List<String> nonSwitch = new java.util.ArrayList<>();
for (String p : parts) {
if (!p.startsWith("/")) {
nonSwitch.add(p);
}
}
if (nonSwitch.size() >= 3) {
targets.add(nonSwitch.get(nonSwitch.size() - 1));
}
}
String workDir = System.getProperty("user.dir");
for (String t : targets) {
try {
Path p = Path.of(t);
Path full = p.isAbsolute() ? p : Path.of(workDir).resolve(p).normalize();
Path parent = full.getParent();
if (parent != null) {
Files.createDirectories(parent);
}
} catch (Exception ignored) {
// 解析失败不阻塞
}
}
}
}
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : TaskService.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.application;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import org.quartz.SchedulerException;
import java.util.ArrayList;
import java.util.List;
/**
* 任务服务(应用层,DDD 用例层):
* CRUD + Quartz 同步注册/注销 + 暂停/恢复/立即执行 + JSON 导入导出
*/
public class TaskService {
private final TaskRepository taskRepo = new TaskRepositoryJdbc();
public List<Task> findAll() {
return taskRepo.findAll();
}
public List<Task> findEnabled() {
return taskRepo.findEnabled();
}
public long count() {
return taskRepo.count();
}
public List<Task> searchPage(String keyword, int page, int pageSize) {
return taskRepo.searchPage(keyword, page, pageSize);
}
public Task findById(long id) {
return taskRepo.findById(id);
}
/** 保存(新增/更新)并同步 Quartz 注册/注销 */
public Task save(Task task) {
CronCompatValidate(task.getCronExpr());
Task saved = taskRepo.save(task);
syncScheduler(saved);
return saved;
}
public boolean delete(long id) {
boolean ok = taskRepo.deleteById(id);
if (ok) {
try {
QuartzSchedulerService.instance().unregister(id);
} catch (SchedulerException e) {
AppConfig.AppLog.warn("删除任务后注销 Quartz 失败 id=" + id + ": " + e.getMessage());
}
}
return ok;
}
/** 启用任务 → 注册;禁用任务 → 注销 */
private void syncScheduler(Task task) {
try {
if (task.isEnabled()) {
QuartzSchedulerService.instance().register(task);
} else {
QuartzSchedulerService.instance().unregister(task.getId());
}
} catch (SchedulerException e) {
AppConfig.AppLog.error("同步 Quartz 失败 task_id=" + task.getId() + ": " + e.getMessage());
}
}
public void pause(long id) {
try {
QuartzSchedulerService.instance().pause(id);
} catch (SchedulerException e) {
AppConfig.AppLog.error("暂停任务失败 id=" + id + ": " + e.getMessage());
}
}
public void resume(long id) {
try {
QuartzSchedulerService.instance().resume(id);
} catch (SchedulerException e) {
AppConfig.AppLog.error("恢复任务失败 id=" + id + ": " + e.getMessage());
}
}
public void runNow(Task task) {
try {
QuartzSchedulerService.instance().runOnce(task);
} catch (SchedulerException e) {
AppConfig.AppLog.error("立即执行失败 id=" + task.getId() + ": " + e.getMessage());
}
}
public java.util.Date nextFireTime(long id) {
return QuartzSchedulerService.instance().nextFireTime(id);
}
/** 启用/禁用并同步调度器 */
public void setEnabled(long id, boolean enabled) {
taskRepo.updateEnabled(id, enabled);
Task t = taskRepo.findById(id);
if (t != null) {
t.setEnabled(enabled);
syncScheduler(t);
}
}
/** cron 校验(5字段) */
private void CronCompatValidate(String cron) {
com.jewelry.taskscheduler.infrastructure.scheduler.CronCompat.validate(cron);
}
/** 导出全部任务为 JSON(与 examples/task_examples.json 的 tasks 结构一致) */
public void exportTasksToJson(String filePath) throws Exception {
List<Task> tasks = taskRepo.findAll();
var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
var root = new java.util.LinkedHashMap<String, Object>();
root.put("tasks", tasks);
root.put("dags", new ArrayList<>());
try (var out = new java.io.BufferedWriter(new java.io.OutputStreamWriter(
new java.io.FileOutputStream(filePath), java.nio.charset.StandardCharsets.UTF_8))) {
out.write(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(root));
}
}
/** 从 JSON 导入任务,兼容三种结构:
* 1) {"tasks":[...]} 对象;2) 纯任务数组;3) 字段兼容 camelCase(name/cronExpr...)与 snake_case(task_name/cron_expr/is_enable...) */
public int importTasksFromJson(String filePath) throws Exception {
var mapper = new com.fasterxml.jackson.databind.ObjectMapper();
String text;
try (var in = new java.io.BufferedReader(new java.io.InputStreamReader(
new java.io.FileInputStream(filePath), java.nio.charset.StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
sb.append(line).append('\n');
}
text = sb.toString();
}
com.fasterxml.jackson.databind.JsonNode root = mapper.readTree(text);
com.fasterxml.jackson.databind.JsonNode arr;
if (root.isArray()) {
arr = root;
} else if (root.has("tasks") && root.get("tasks").isArray()) {
arr = root.get("tasks");
} else {
throw new IllegalArgumentException("JSON 需为任务数组,或包含 tasks 数组的对象");
}
if (!arr.iterator().hasNext()) {
throw new IllegalArgumentException("没有可导入的任务");
}
int imported = 0;
for (com.fasterxml.jackson.databind.JsonNode n : arr) {
if (!n.isObject()) {
continue;
}
Task t = parseTaskNode(n);
if (t == null) {
continue;
}
save(t);
imported++;
}
return imported;
}
/** 按节点解析任务:camelCase/snake_case 字段名都兼容,缺 name/cron 或两者皆空则跳过 */
private Task parseTaskNode(com.fasterxml.jackson.databind.JsonNode n) {
String name = nodeStr(n, "name", "task_name");
String cron = nodeStr(n, "cronExpr", "cron_expr");
if (name == null || name.isBlank() || cron == null || cron.isBlank()) {
return null;
}
Task t = new Task();
t.setName(name.trim());
t.setCronExpr(cron.trim());
String command = nodeStr(n, "command", "command");
t.setCommand(command == null ? "" : command.trim());
t.setTimeoutSec(nodeInt(n, 30, "timeoutSec", "timeout_sec"));
t.setMaxRetry(nodeInt(n, 0, "maxRetry", "max_retry"));
t.setRetryGapSec(nodeInt(n, 5, "retryGapSec", "retry_gap_sec"));
t.setEnabled(nodeBool(n, true, "enabled", "is_enable"));
String remark = nodeStr(n, "remark", "remark");
t.setRemark(remark == null ? "" : remark.trim());
return t;
}
private String nodeStr(com.fasterxml.jackson.databind.JsonNode n, String camel, String snake) {
return n.hasNonNull(camel) ? n.get(camel).asText() : (n.hasNonNull(snake) ? n.get(snake).asText() : null);
}
private int nodeInt(com.fasterxml.jackson.databind.JsonNode n, int def, String camel, String snake) {
com.fasterxml.jackson.databind.JsonNode v = n.has(camel) ? n.get(camel) : (n.has(snake) ? n.get(snake) : null);
return v == null || v.isNull() || !v.canConvertToInt() ? def : v.asInt();
}
private boolean nodeBool(com.fasterxml.jackson.databind.JsonNode n, boolean def, String camel, String snake) {
if (n.has(camel)) {
return n.get(camel).asBoolean(def);
}
if (n.has(snake)) {
return n.get(snake).asBoolean(def);
}
return def;
}
}
java
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : MainController.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler.ui;
import com.jewelry.taskscheduler.application.DagService;
import com.jewelry.taskscheduler.application.DashboardService;
import com.jewelry.taskscheduler.application.TaskService;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.model.TaskHistory;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.DefaultTableModel;
import java.text.SimpleDateFormat;
import java.util.List;
/**
主控制器(MVC Controller):协调 View 与 应用层服务
*/
public class MainController {
private final TaskService taskService = new TaskService();
private final DashboardService dashboardService = new DashboardService();
private final DagService dagService = DagService.instance();
private MainWindow window;
public TaskService taskService() {
return taskService;
}
public DashboardService dashboardService() {
return dashboardService;
}
public DagService dagService() {
return dagService;
}
public void setWindow(MainWindow window) {
this.window = window;
}
public MainWindow window() {
return window;
}
// ===== 任务操作 =====
public void newTask(JFrame owner) {
new TaskEditDialog(owner, new Task(), taskService::save).setVisible(true);
window.refreshAll();
}
public void editTask(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
new TaskEditDialog(owner, task, taskService::save).setVisible(true);
window.refreshAll();
}
public void deleteTask(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
if (JOptionPane.showConfirmDialog(owner, "确定删除任务【" + task.getName() + "】?",
"确认删除", JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) {
taskService.delete(task.getId());
window.refreshAll();
}
}
public void runNow(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
taskService.runNow(task);
}
public void pause(JFrame owner, Task task) {
if (task == null) {
return;
}
taskService.pause(task.getId());
window.refreshAll();
}
public void resume(JFrame owner, Task task) {
if (task == null) {
return;
}
taskService.resume(task.getId());
window.refreshAll();
}
public void toggleEnabled(JFrame owner, Task task) {
if (task == null) {
return;
}
taskService.setEnabled(task.getId(), !task.isEnabled());
window.refreshAll();
}
/** 查看任务执行历史弹窗 */
public void showHistory(JFrame owner, Task task) {
if (task == null) {
JOptionPane.showMessageDialog(owner, "请先选择任务");
return;
}
List<TaskHistory> list = dashboardService().listRecent(50).stream()
.filter(h -> h.getTaskId() == task.getId())
.toList();
DefaultTableModel m = new DefaultTableModel(new String[]{"ID", "开始时间", "结束时间", "结果", "耗时(秒)", "信息"}, 0) {
@Override
public boolean isCellEditable(int r, int c) {
return false;
}
};
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (TaskHistory h : list) {
m.addRow(new Object[]{
h.getId(),
h.getStartTime() == null ? "" : h.getStartTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
h.getEndTime() == null ? "" : h.getEndTime().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
h.isSuccess() ? "成功" : "失败",
String.format("%.1f", h.costMillis() / 1000.0),
h.getMessage() == null ? "" : h.getMessage()});
}
JTable t = new JTable(m);
t.setRowHeight(24);
JOptionPane.showMessageDialog(owner, new JScrollPane(t),
"任务【" + task.getName() + "】执行历史(最近50条)", JOptionPane.PLAIN_MESSAGE);
}
/** 全局刷新入口(Swing Timer 5 秒调用) */
public void refreshAll() {
if (window == null) {
return;
}
window.refreshAll();
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : MainWindow.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.ui;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.infrastructure.redis.RedisSupport;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import com.jewelry.taskscheduler.ui.panel.DagPanel;
import com.jewelry.taskscheduler.ui.panel.DashboardPanel;
import com.jewelry.taskscheduler.ui.panel.LogPanel;
import com.jewelry.taskscheduler.ui.panel.TaskListPanel;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
主窗口(MVC View):工具栏 + 四标签页 + 状态栏
Swing Timer 自动刷新:仪表盘、任务下次执行时间、集群节点、统计面板
*/
public class MainWindow extends JFrame {
private final MainController controller;
private final TaskListPanel taskPanel;
private final DashboardPanel dashboardPanel;
private final DagPanel dagPanel;
private final LogPanel logPanel;
private JTabbedPane tabs;
private final JLabel lblQuartz = new JLabel();
private final JLabel lblRedis = new JLabel();
private final JLabel lblNodes = new JLabel();
private final JLabel lblTasks = new JLabel();
private final JLabel lblTime = new JLabel();
public MainWindow(MainController controller) {
super("Java 任务调度系统(Swing + DDD + MVC + Quartz + Redis + DAG)");
this.controller = controller;
taskPanel = new TaskListPanel(controller);
dashboardPanel = new DashboardPanel();
dagPanel = new DagPanel();
logPanel = new LogPanel();
buildToolBar();
buildTabs();
buildStatusBar();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1120, 760);
setMinimumSize(new Dimension(960, 640));
setLocationRelativeTo(null);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
shutdown();
}
});
// 全局异常捕获
Thread.setDefaultUncaughtExceptionHandler((t, ex) -> {
AppConfig.AppLog.error("未捕获异常 [" + t.getName() + "]: " + ex);
ex.printStackTrace();
});
// 自动刷新定时器(Swing Timer,按配置间隔)
int intervalMs = Math.max(1000, AppConfig.INSTANCE.refreshIntervalSec * 1000);
Timer timer = new Timer(intervalMs, e -> SwingUtilities.invokeLater(this::refreshAll));
timer.start();
refreshAll();
}
private void buildToolBar() {
JToolBar bar = new JToolBar();
bar.setFloatable(false);
JButton btnAdd = new JButton("新增任务");
JButton btnEdit = new JButton("编辑");
JButton btnDel = new JButton("删除");
JButton btnRun = new JButton("立即执行");
JButton btnPause = new JButton("暂停");
JButton btnResume = new JButton("恢复");
JButton btnEnable = new JButton("启用/禁用");
JButton btnHistory = new JButton("执行历史");
JButton btnDag = new JButton("DAG编排");
JButton btnImport = new JButton("导入JSON");
JButton btnExport = new JButton("导出JSON");
btnAdd.addActionListener(e -> controller.newTask(this));
btnEdit.addActionListener(e -> controller.editTask(this, taskPanel.selectedTask()));
btnDel.addActionListener(e -> controller.deleteTask(this, taskPanel.selectedTask()));
btnRun.addActionListener(e -> controller.runNow(this, taskPanel.selectedTask()));
btnPause.addActionListener(e -> controller.pause(this, taskPanel.selectedTask()));
btnResume.addActionListener(e -> controller.resume(this, taskPanel.selectedTask()));
btnEnable.addActionListener(e -> controller.toggleEnabled(this, taskPanel.selectedTask()));
btnHistory.addActionListener(e -> controller.showHistory(this, taskPanel.selectedTask()));
btnDag.addActionListener(e -> tabs.setSelectedIndex(2));
btnImport.addActionListener(e -> taskPanel.importJson());
btnExport.addActionListener(e -> taskPanel.exportJson());
bar.add(btnAdd);
bar.add(btnEdit);
bar.add(btnDel);
bar.addSeparator();
bar.add(btnRun);
bar.add(btnPause);
bar.add(btnResume);
bar.add(btnEnable);
bar.addSeparator();
bar.add(btnHistory);
bar.add(btnDag);
bar.addSeparator();
bar.add(btnImport);
bar.add(btnExport);
add(bar, BorderLayout.NORTH);
}
private void buildTabs() {
tabs = new JTabbedPane();
tabs.addTab("任务列表", taskPanel);
tabs.addTab("仪表盘", dashboardPanel);
tabs.addTab("DAG 工作流", dagPanel);
tabs.addTab("执行日志", logPanel);
add(tabs, BorderLayout.CENTER);
}
private void buildStatusBar() {
JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT, 14, 3));
bar.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, new java.awt.Color(210, 210, 218)));
bar.add(lblQuartz);
bar.add(lblRedis);
bar.add(lblNodes);
bar.add(lblTasks);
bar.add(lblTime);
add(bar, BorderLayout.SOUTH);
}
/** 全局刷新(定时器与操作后调用) */
public void refreshAll() {
taskPanel.reload();
dashboardPanel.refresh();
dagPanel.reload();
lblQuartz.setText("Quartz: " + QuartzSchedulerService.instance().stateText());
lblRedis.setText("Redis: " + (AppConfig.INSTANCE.redisEnabled ? "已启用" : "本地模式"));
int nodes = RedisSupport.instance().listNodes().size();
lblNodes.setText("集群节点: " + nodes);
lblTasks.setText("任务总数: " + controller.taskService().count());
lblTime.setText("时间: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
private void shutdown() {
try {
QuartzSchedulerService.instance().shutdown();
} catch (Exception ignored) {
}
RedisSupport.instance().shutdown();
AppConfig.AppLog.info("应用退出");
}
}
/**
encoding: utf-8
版权所有 2026 ©涂聚文有限公司 ®
许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
Author : geovindu,Geovin Du 涂聚文.
IDE : IntelliJ IDEA 2024.3.6 Java 21
database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
OS : window10
Datetime : 2026 - 2026/9/5 - 21:08
User : geovindu
Product : IntelliJ IDEA
Project : Taskscheduler
File : TaskEditDialog.java
explain : 学习 类
**/
package com.jewelry.taskscheduler.ui;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.infrastructure.scheduler.CronCompat;
import org.quartz.CronExpression;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.text.SimpleDateFormat;
/**
任务编辑对话框(MVC View):新增/编辑任务,带 cron 校验与下次执行时间预览
*/
public class TaskEditDialog extends JDialog {
private final Task task;
private final java.util.function.Consumer<Task> onSave;
private final JTextField txtName = new JTextField(24);
private final JTextField txtCron = new JTextField(24);
private final JTextField txtCommand = new JTextField(24);
private final JTextField txtTimeout = new JTextField("30", 6);
private final JTextField txtMaxRetry = new JTextField("0", 6);
private final JTextField txtRetryGap = new JTextField("5", 6);
private final JTextArea txtRemark = new JTextArea(2, 22);
private final JCheckBox chkEnabled = new JCheckBox("启用", true);
private final JLabel lblNext = new JLabel("");
public TaskEditDialog(Window owner, Task task, java.util.function.Consumer<Task> onSave) {
super(owner instanceof java.awt.Frame ? (java.awt.Frame) owner : null,
task == null || task.getId() <= 0 ? "新增任务" : "编辑任务", true);
this.task = task;
this.onSave = onSave;
setSize(560, 520);
setLocationRelativeTo(owner);
buildForm();
if (task != null && task.getId() > 0) {
txtName.setText(task.getName());
txtCron.setText(task.getCronExpr());
txtCommand.setText(task.getCommand());
txtTimeout.setText(String.valueOf(task.getTimeoutSec()));
txtMaxRetry.setText(String.valueOf(task.getMaxRetry()));
txtRetryGap.setText(String.valueOf(task.getRetryGapSec()));
txtRemark.setText(task.getRemark());
chkEnabled.setSelected(task.isEnabled());
}
previewCron();
}
private void buildForm() {
JPanel form = new JPanel(new GridBagLayout());
GridBagConstraints g = new GridBagConstraints();
g.insets = new Insets(6, 10, 6, 10);
g.anchor = GridBagConstraints.WEST;
int row = 0;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("任务名称:*"), g);
g.gridx = 1;
form.add(txtName, g);
row++;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("Cron表达式(5字段):*"), g);
g.gridx = 1;
form.add(txtCron, g);
row++;
g.gridx = 0; g.gridy = row;
JButton btnPreview = new JButton("校验并预览下次执行");
btnPreview.addActionListener(e -> previewCron());
form.add(btnPreview, g);
g.gridx = 1;
form.add(lblNext, g);
row++;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("命令(cmd):*"), g);
g.gridx = 1;
form.add(txtCommand, g);
row++;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("超时(秒):"), g);
g.gridx = 1;
form.add(txtTimeout, g);
row++;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("失败重试次数:"), g);
g.gridx = 1;
form.add(txtMaxRetry, g);
row++;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("重试间隔(秒):"), g);
g.gridx = 1;
form.add(txtRetryGap, g);
row++;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("备注(含\"一次性\"=仅执行一次后自动禁用):"), g);
g.gridx = 1;
form.add(new JScrollPane(txtRemark), g);
row++;
g.gridx = 0; g.gridy = row;
form.add(new JLabel("启用:"), g);
g.gridx = 1;
form.add(chkEnabled, g);
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 8));
JButton ok = new JButton("保存");
JButton cancel = new JButton("取消");
buttons.add(ok);
buttons.add(cancel);
ok.addActionListener(e -> {
try {
if (txtName.getText().trim().isEmpty() || txtCron.getText().trim().isEmpty()
|| txtCommand.getText().trim().isEmpty()) {
throw new IllegalArgumentException("名称、Cron、命令均为必填");
}
Task t = task != null && task.getId() > 0 ? task : new Task();
t.setName(txtName.getText().trim());
t.setCronExpr(txtCron.getText().trim());
t.setCommand(txtCommand.getText().trim());
t.setTimeoutSec(Integer.parseInt(txtTimeout.getText().trim()));
t.setMaxRetry(Integer.parseInt(txtMaxRetry.getText().trim()));
t.setRetryGapSec(Integer.parseInt(txtRetryGap.getText().trim()));
t.setRemark(txtRemark.getText());
t.setEnabled(chkEnabled.isSelected());
t.setStatus(com.jewelry.taskscheduler.domain.model.TaskStatus.IDLE);
onSave.accept(t);
dispose();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "保存失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
});
cancel.addActionListener(e -> dispose());
setLayout(new BorderLayout());
add(form, BorderLayout.CENTER);
add(buttons, BorderLayout.SOUTH);
}
/** cron 校验 + 显示下一次执行时间 */
private void previewCron() {
try {
String cron = txtCron.getText().trim();
if (cron.isEmpty()) {
lblNext.setText("请输入 cron 后再预览");
return;
}
CronCompat.validate(cron);
String q = CronCompat.toQuartz(cron);
CronExpression expr = new CronExpression(q);
java.util.Date next = expr.getNextValidTimeAfter(new java.util.Date());
lblNext.setText("有效,下次执行: " + (next == null ? "无" : new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(next)));
lblNext.setForeground(new java.awt.Color(46, 125, 50));
} catch (Exception e) {
lblNext.setText("无效: " + e.getMessage());
lblNext.setForeground(new java.awt.Color(198, 40, 40));
}
}
}
调用:
java
/**
* encoding: utf-8
* 版权所有 2026 ©涂聚文有限公司 ®
* 许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎
* 描述:Tasks cheduler mvn 和 java 21 已在 PATH 中。脚本会先 mvn clean package 构建,再 java -jar target\taskscheduler-1.0.0-shaded.jar 启动 Swing 界面。
* Author : geovindu,Geovin Du 涂聚文.
* IDE : IntelliJ IDEA 2024.3.6 Java 21
* # database : Oracle21c,MySQL 9.0,SQL Server 2019,PostgreSQL 17.1 Neo4j
* # OS : window10
* Datetime : 2026 - 2026/9/5 - 21:08
* User : geovindu
* Product : IntelliJ IDEA
* Project : Taskscheduler
* File : Application.java
* explain : 学习 类
**/
package com.jewelry.taskscheduler;
import com.jewelry.taskscheduler.config.AppConfig;
import com.jewelry.taskscheduler.domain.model.Task;
import com.jewelry.taskscheduler.domain.repository.TaskRepository;
import com.jewelry.taskscheduler.infrastructure.db.Db;
import com.jewelry.taskscheduler.infrastructure.redis.RedisSupport;
import com.jewelry.taskscheduler.infrastructure.repository.TaskRepositoryJdbc;
import com.jewelry.taskscheduler.infrastructure.scheduler.QuartzSchedulerService;
import com.jewelry.taskscheduler.ui.MainController;
import com.jewelry.taskscheduler.ui.MainWindow;
import javax.swing.SwingUtilities;
import java.util.List;
/**
Java 任务调度系统主入口
启动顺序:加载配置 → 初始化日志 → H2 初始化 → Redis 启动 → Quartz 启动 → 种子数据 → Swing 主窗口
JDK 21 + Swing + DDD + MVC + Quartz + Redis(可选) + EventBus + DAG
*/
public class Application {
public static void main(String[] args) {
// 1. 配置与日志
AppConfig.load("app.properties");
AppConfig.AppLog.init(AppConfig.INSTANCE.logDir, AppConfig.INSTANCE.logMaxSizeMb);
AppConfig.AppLog.info("========== Java 任务调度系统启动 ==========");
AppConfig.AppLog.info("运行环境: JDK " + System.getProperty("java.version") + " / OS " + System.getProperty("os.name"));
// 2. H2 数据库
Db.init();
// 3. Redis(可选)
RedisSupport.instance().start();
// 4. Quartz 调度器
QuartzSchedulerService.instance().start();
// 5. 种子数据(首次运行空库时插入示例任务)
seedIfEmpty();
// 6. Swing 主窗口(EDT)
SwingUtilities.invokeLater(() -> {
try {
MainController controller = new MainController();
MainWindow window = new MainWindow(controller);
controller.setWindow(window);
window.setVisible(true);
} catch (Exception e) {
AppConfig.AppLog.error("Swing 界面启动失败: " + e);
e.printStackTrace();
}
});
}
/** 首次运行(任务表为空)时插入示例任务 */
private static void seedIfEmpty() {
TaskRepository repo = new TaskRepositoryJdbc();
if (repo.count() > 0) {
return;
}
AppConfig.AppLog.info("检测到空库,插入示例任务...");
// 示例1:每分钟健康检查(cron 5字段:分 时 日 月 周)
seed(repo, "每分钟健康检查", "* * * * *",
"echo [%date% %time%] health check ok >> logs\\health.log",
"定时健康检查示例");
// 示例2:每天 8 点执行数据库备份
seed(repo, "每日8点备份数据库", "0 8 * * *",
"mkdir db_backup 2>nul & copy data\\task_scheduler.mv.db db_backup\\task_scheduler_%date:~0,4%%date:~5,2%%date:~8,2%.mv.db",
"备份 H2 数据库文件");
// 示例3:一次性任务(备注含"一次性",执行成功自动禁用)
seed(repo, "一次性初始化数据", "0 0 * * *",
"echo init done >> logs\\init.log",
"一次性:执行成功后自动禁用");
}
private static void seed(TaskRepository repo, String name, String cron, String command, String remark) {
Task t = new Task();
t.setName(name);
t.setCronExpr(cron);
t.setCommand(command);
t.setTimeoutSec(60);
t.setMaxRetry(0);
t.setRetryGapSec(5);
t.setEnabled(true);
t.setRemark(remark);
repo.save(t);
try {
QuartzSchedulerService.instance().register(t);
} catch (Exception e) {
AppConfig.AppLog.warn("种子任务注册失败: " + name + ": " + e.getMessage());
}
}
}
输出:
