SpringBoot + 动态 Cron 表达式 + Web 界面修改:运营人员可自助调整任务时间

问题背景

在传统的Spring Boot应用中,定时任务通常使用@Scheduled注解来实现,Cron表达式直接硬编码在代码中。这种方式有以下几个问题:

  1. 修改不便:每次调整任务执行时间都需要修改代码,重新部署应用
  2. 运营依赖开发:运营人员无法自主调整任务时间,需要依赖开发人员
  3. 缺乏灵活性:无法根据业务需求动态调整任务执行计划
  4. 缺乏监控:任务执行状态和日志难以管理和查看

这些问题在业务快速变化的场景下尤为突出,比如促销活动期间需要临时调整任务执行时间,或者根据业务量的变化调整任务执行频率等。

核心概念

1. 动态任务调度

动态任务调度是指在应用运行过程中,能够动态地添加、修改、删除定时任务,而不需要重启应用。Spring Boot提供了TaskScheduler接口,支持动态任务调度。

2. Cron表达式

Cron表达式是一种用于指定定时任务执行时间的字符串格式,由6或7个字段组成,分别表示秒、分、时、日、月、周、年(可选)。例如:

  • 0 0 2 * * ?:每天凌晨2点执行
  • 0 0 */3 * * ?:每3小时执行一次
  • 0 0 0 * * ?:每天零点执行

3. 任务配置管理

将任务配置(包括任务名称、描述、Cron表达式、状态等)存储在数据库中,便于动态管理和修改。

4. Web界面管理

提供直观的Web界面,让运营人员可以自助调整任务时间,查看任务执行状态和日志。

实现方案

1. 技术栈选择

  • Spring Boot 2.7.5:提供基础框架支持
  • Spring Web:提供REST API
  • Spring Data JPA:操作数据库
  • Spring Security:权限控制
  • MySQL 8.0:存储任务配置和执行日志
  • Lombok:简化代码

2. 数据库设计

任务配置表(task_config)

字段名数据类型描述
idbigint(20)主键ID
task_namevarchar(100)任务名称(唯一)
task_descriptionvarchar(255)任务描述
cron_expressionvarchar(50)Cron表达式
statustinyint(4)状态:0-禁用,1-启用
create_timedatetime创建时间
update_timedatetime更新时间

任务执行日志表(task_log)

字段名数据类型描述
idbigint(20)主键ID
task_namevarchar(100)任务名称
statustinyint(4)执行状态:0-失败,1-成功
start_timedatetime开始时间
end_timedatetime结束时间
durationbigint(20)执行时长(毫秒)
error_messagetext错误信息

3. 核心代码实现

3.1 任务配置实体类

@Entity
@Table(name = "task_config")
public class TaskConfig {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "task_name", nullable = false, unique = true, length = 100)
    private String taskName;
    
    @Column(name = "task_description", length = 255)
    private String taskDescription;
    
    @Column(name = "cron_expression", nullable = false, length = 50)
    private String cronExpression;
    
    @Column(name = "status", nullable = false)
    private Integer status;
    
    @Column(name = "create_time", nullable = false, updatable = false)
    private LocalDateTime createTime;
    
    @Column(name = "update_time", nullable = false)
    private LocalDateTime updateTime;
    
    // getter/setter 方法
}

3.2 动态任务调度服务

@Service
public class DynamicTaskScheduler {
    
    @Autowired
    private TaskScheduler taskScheduler;
    
    @Autowired
    private TaskConfigRepository taskConfigRepository;
    
    @Autowired
    private TaskLogRepository taskLogRepository;
    
    // 存储任务调度器的引用
    private final Map<String, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
    
    // 存储任务执行器
    private final Map<String, Runnable> taskRunners = new ConcurrentHashMap<>();
    
    /**
     * 初始化所有任务
     */
    @PostConstruct
    public void init() {
        log.info("开始初始化动态任务调度器");
        
        // 查询所有启用的任务配置
        List<TaskConfig> taskConfigs = taskConfigRepository.findByStatus(1);
        
        // 初始化每个任务
        for (TaskConfig taskConfig : taskConfigs) {
            scheduleTask(taskConfig);
        }
        
        log.info("动态任务调度器初始化完成,共初始化 {} 个任务", taskConfigs.size());
    }
    
    /**
     * 调度任务
     */
    public void scheduleTask(TaskConfig taskConfig) {
        try {
            String taskName = taskConfig.getTaskName();
            String cronExpression = taskConfig.getCronExpression();
            
            // 取消已存在的任务
            cancelTask(taskName);
            
            // 创建任务执行器
            Runnable taskRunner = createTaskRunner(taskName);
            taskRunners.put(taskName, taskRunner);
            
            // 创建Cron触发器
            CronTrigger cronTrigger = new CronTrigger(cronExpression);
            
            // 调度任务
            ScheduledFuture<?> scheduledFuture = taskScheduler.schedule(taskRunner, cronTrigger);
            scheduledTasks.put(taskName, scheduledFuture);
            
            log.info("任务 [{}] 调度成功,cron表达式:{}", taskName, cronExpression);
        } catch (Exception e) {
            log.error("调度任务失败", e);
            throw new RuntimeException("调度任务失败:" + e.getMessage());
        }
    }
    
    /**
     * 更新任务Cron表达式
     */
    public void updateTaskCron(String taskName, String cronExpression) {
        // 查询任务配置
        TaskConfig taskConfig = taskConfigRepository.findByTaskName(taskName);
        if (taskConfig == null) {
            throw new RuntimeException("任务不存在:" + taskName);
        }
        
        // 更新Cron表达式
        taskConfig.setCronExpression(cronExpression);
        taskConfigRepository.save(taskConfig);
        
        // 重新调度任务
        scheduleTask(taskConfig);
        
        log.info("任务 [{}] 的Cron表达式已更新为:{}", taskName, cronExpression);
    }
    
    // 其他方法:cancelTask、enableTask、disableTask、getTaskStatus等
}

3.3 任务管理控制器

@RestController
@RequestMapping("/tasks")
public class TaskController {
    
    @Autowired
    private TaskConfigRepository taskConfigRepository;
    
    @Autowired
    private TaskLogRepository taskLogRepository;
    
    @Autowired
    private DynamicTaskScheduler dynamicTaskScheduler;
    
    /**
     * 获取所有任务配置
     */
    @GetMapping
    public ResponseEntity<Result> getAllTasks() {
        try {
            List<TaskConfig> tasks = taskConfigRepository.findAll();
            return ResponseEntity.ok(Result.success(tasks));
        } catch (Exception e) {
            log.error("获取任务配置失败", e);
            return ResponseEntity.ok(Result.error("获取任务配置失败:" + e.getMessage()));
        }
    }
    
    /**
     * 更新任务Cron表达式
     */
    @PutMapping("/{taskName}/cron")
    public ResponseEntity<Result> updateTaskCron(@PathVariable String taskName, @RequestBody CronUpdateRequest request) {
        try {
            String cronExpression = request.getCronExpression();
            
            // 验证Cron表达式
            validateCronExpression(cronExpression);
            
            // 更新任务Cron表达式
            dynamicTaskScheduler.updateTaskCron(taskName, cronExpression);
            
            return ResponseEntity.ok(Result.success("更新任务Cron表达式成功"));
        } catch (Exception e) {
            log.error("更新任务Cron表达式失败", e);
            return ResponseEntity.ok(Result.error("更新任务Cron表达式失败:" + e.getMessage()));
        }
    }
    
    // 其他方法:getTask、enableTask、disableTask、getTaskLogs、getTaskStatus等
}

3.4 Web界面

Web界面使用HTML、CSS和JavaScript实现,包括任务列表展示、编辑Cron表达式、启用/禁用任务、查看执行日志等功能。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>任务管理</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css">
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<body>
    <div class="container mt-5">
        <h1 class="mb-4">任务管理</h1>
        
        <div class="card">
            <div class="card-body">
                <table class="table table-striped">
                    <thead>
                        <tr>
                            <th>任务名称</th>
                            <th>任务描述</th>
                            <th>Cron表达式</th>
                            <th>状态</th>
                            <th>操作</th>
                        </tr>
                    </thead>
                    <tbody id="taskTableBody">
                        <!-- 任务列表将通过JavaScript动态生成 -->
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    
    <!-- 编辑Cron表达式模态框 -->
    <div class="modal fade" id="editCronModal" tabindex="-1" aria-labelledby="editCronModalLabel" aria-hidden="true">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="editCronModalLabel">编辑Cron表达式</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <input type="hidden" id="currentTaskName">
                    <div class="mb-3">
                        <label for="cronExpression" class="form-label">Cron表达式</label>
                        <input type="text" class="form-control" id="cronExpression" placeholder="例如:0 0 2 * * ?">
                        <div class="form-text">Cron表达式格式:秒 分 时 日 月 周 年(可选)</div>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-primary" id="saveCronBtn">保存</button>
                </div>
            </div>
        </div>
    </div>
    
    <!-- 任务执行日志模态框 -->
    <div class="modal fade" id="taskLogModal" tabindex="-1" aria-labelledby="taskLogModalLabel" aria-hidden="true">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title" id="taskLogModalLabel">任务执行日志</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <div id="taskLogContent">
                        <!-- 日志内容将通过JavaScript动态生成 -->
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        // 页面加载完成后获取任务列表
        $(document).ready(function() {
            loadTasks();
        });
        
        // 加载任务列表
        function loadTasks() {
            $.ajax({
                url: '/api/tasks',
                type: 'GET',
                success: function(response) {
                    if (response.code === 200) {
                        var tasks = response.data;
                        var tbody = $('#taskTableBody');
                        tbody.empty();
                        
                        tasks.forEach(function(task) {
                            var statusText = task.status === 1 ? '启用' : '禁用';
                            var statusClass = task.status === 1 ? 'text-success' : 'text-danger';
                            var actionButtons = '';
                            
                            if (task.status === 1) {
                                actionButtons = '<button class="btn btn-sm btn-warning" onclick="disableTask(\'' + task.taskName + '\')">禁用</button>';
                            } else {
                                actionButtons = '<button class="btn btn-sm btn-success" onclick="enableTask(\'' + task.taskName + '\')">启用</button>';
                            }
                            
                            var row = '<tr>' +
                                '<td>' + task.taskName + '</td>' +
                                '<td>' + task.taskDescription + '</td>' +
                                '<td>' + task.cronExpression + '</td>' +
                                '<td class="' + statusClass + '">' + statusText + '</td>' +
                                '<td>' +
                                    '<button class="btn btn-sm btn-primary me-2" onclick="editCron(\'' + task.taskName + '\', \'' + task.cronExpression + '\')">编辑Cron</button>' +
                                    '<button class="btn btn-sm btn-info me-2" onclick="viewLogs(\'' + task.taskName + '\')">查看日志</button>' +
                                    actionButtons +
                                '</td>' +
                            '</tr>';
                            
                            tbody.append(row);
                        });
                    } else {
                        alert('获取任务列表失败:' + response.message);
                    }
                },
                error: function() {
                    alert('获取任务列表失败');
                }
            });
        }
        
        // 编辑Cron表达式
        function editCron(taskName, cronExpression) {
            $('#currentTaskName').val(taskName);
            $('#cronExpression').val(cronExpression);
            $('#editCronModal').modal('show');
        }
        
        // 保存Cron表达式
        $('#saveCronBtn').click(function() {
            var taskName = $('#currentTaskName').val();
            var cronExpression = $('#cronExpression').val();
            
            $.ajax({
                url: '/api/tasks/' + taskName + '/cron',
                type: 'PUT',
                contentType: 'application/json',
                data: JSON.stringify({ cronExpression: cronExpression }),
                success: function(response) {
                    if (response.code === 200) {
                        alert('更新Cron表达式成功');
                        $('#editCronModal').modal('hide');
                        loadTasks();
                    } else {
                        alert('更新Cron表达式失败:' + response.message);
                    }
                },
                error: function() {
                    alert('更新Cron表达式失败');
                }
            });
        });
        
        // 其他方法:enableTask、disableTask、viewLogs等
    </script>
</body>
</html>

最佳实践

1. 任务设计

  • 职责分离:将任务逻辑与调度逻辑分离,便于维护
  • 任务粒度:任务粒度要适中,避免任务过大或过小
  • 错误处理:妥善处理任务执行过程中的异常,确保系统稳定

2. Cron表达式管理

  • 标准化:使用标准的Cron表达式格式,便于理解和维护
  • 文档化:为每个任务的Cron表达式添加注释,说明其执行时间
  • 验证:在修改Cron表达式时,进行格式验证,确保表达式有效

3. 监控与告警

  • 日志记录:记录详细的任务执行日志,便于排查问题
  • 监控:监控任务执行状态,及时发现和解决问题
  • 告警:当任务执行失败或超时,发送告警通知

4. 权限管理

  • 角色分配:根据角色分配任务管理权限,确保系统安全
  • 操作审计:记录任务配置的修改记录,便于追溯

5. 性能优化

  • 任务执行时间:优化任务执行逻辑,避免任务执行时间过长
  • 并发控制:合理设置任务调度线程池大小,避免并发冲突
  • 资源管理:及时释放任务执行过程中使用的资源

总结与展望

总结

本方案通过Spring Boot + 动态Cron表达式 + Web界面的组合,实现了运营人员可自助调整任务时间的功能。主要优势包括:

  1. 灵活性:运营人员可以根据业务需求动态调整任务执行时间
  2. 便捷性:通过Web界面操作,无需修改代码和重启应用
  3. 可监控性:提供详细的任务执行日志和状态监控
  4. 可扩展性:易于添加新的任务和功能

展望

  1. 分布式任务调度:对于分布式部署的应用,可以集成XXL-Job、Elastic-Job等分布式任务调度框架
  2. 任务依赖管理:支持任务之间的依赖关系,确保任务按顺序执行
  3. 任务参数化:支持任务参数的动态配置,提高任务的灵活性
  4. 智能化调度:基于业务数据和系统负载,智能调整任务执行计划
  5. 可视化配置:提供更直观的Cron表达式生成器,降低运营人员的使用门槛

示例工程

本方案的完整示例工程已提供,包括:

  • 完整的项目结构和代码
  • 数据库建表脚本和示例数据
  • Web界面和API接口
  • 详细的使用说明文档

关注公众号奥,回复"动态任务调度",获取完整的代码示例和实现方案。

结语

动态任务调度是现代应用中常见的需求,尤其是在业务快速变化的场景下。通过本方案的实现,我们可以让运营人员自主调整任务时间,提高了系统的灵活性和可维护性。同时,通过完善的监控和管理功能,确保了任务执行的可靠性和稳定性。

如果您有任何问题或建议,欢迎在评论区留言讨论。


标题:SpringBoot + 动态 Cron 表达式 + Web 界面修改:运营人员可自助调整任务时间
作者:jiangyi
地址:http://jiangyi.space/articles/2026/02/28/1772115018645.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消