Spring Task 任务调度可视化管理

简介

想必大家都清楚 Spring Scheduler 的好用和怎么用了,不过它没有一个像 XXL-Job 有个后台界面的,好像不太完整,于是笔者打算为 Spring Scheduler 提供一个可视化的操作界面,虽然比不上 XXL-Job 那么强大,但也算弥补其中缺失的一环。它支持在线监控执行的任务、支持动态修改任务状态、暂停/恢复任务,以及终止运行中任务。

另外本组件的特色就是非常简单,或者说"轻量级",只有两个主要的类和一个前端静态 html 组成,Java 的话总共不超过 500 行代码。

源码在这里

核心原理

我们先进入原理层面谈谈(如果读者觉得太难可以先略过)。

  • 核心ScheduleHandler类 ,连完整的注释才 180 行。其作用如下:
    • 主要围绕 Spring 核心原理,从加载机制中得到哪些是定时器的方法,收集起来以便统一管理
    • 如何对任务控制呢?通过ScheduledTaskScheduledFuture,可以扩展实现动态修改任务状态、暂停/恢复任务,以及终止运行中任务。ScheduledTask 表示所有被@Scheduled注解修饰的任务
    • 如何得到 ScheduledTask 对象呢?这涉及到 Spring 的BeanPostProcessor加载机制。众所周知,Spring 是一个开放系统,暴露了大量开放的接口供用户使用。其中原理我们不妨看看 ScheduleHandler源码就知道。
  • 控制器ScheduledController,这是提供 API 接口的。任务可以得到了,可是怎么对其管理呢?我们很自然地想到用数据库来进行 CRUD 的管理,但问题又来了,Spring 任务连个名称或者 id 都没有,怎么做数据库管理呢?笔者想了下,就是通过类名称和执行方法组成唯一的条件,就是一个独特的任务记录,可以进行入库和管理。这个类除了调用上述的 Spring ScheduledTask API 外,还有涉及的数据库的 CRUD 操作。其中一个怎么停止任务的地方,比较巧妙地说。
  • 前端 task.html,如下图所示,界面非常简单, 就一个 HTML,仅仅依赖 vue.js(CDN 加载),而且 js/css 全在 HTML 里面。前端页面源码在下面有。你要简单修改下接口地址什么的。比较遗憾的是相关的 CSS 源码找不到了~。

后端依赖的话,是整合在我的框架 AJAXJS里面,当然是非常轻量级的。如果你不打算依赖 AJAXJS,把相关的 JDBC 操作抠出来也是非常简单的。

使用配置

Spring 工程配置如下:

java 复制代码
// 配置线程池,这是必须的
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(10); // 核心线程数,根据预期并发量调整
    executor.setMaxPoolSize(20);  // 最大线程数,根据服务器资源调整
    executor.setQueueCapacity(100); // 队列大小,控制积压的任务数
    executor.setThreadNamePrefix("AsyncHttpProcessor-");
    // 设置拒绝策略,例如 DiscardPolicy 或 CallerRunsPolicy
    // executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();

    return executor;
}

为了让 Spring Boot 能够自动配置此组件,你必须在 YAML 配置文件中启用它:

yaml 复制代码
aj-framework:
  schedule_mgr: # 定时任务管理器
    enabled: true

启动成功在日志中显示:

编写一个定时器试试

首先启动类加入@EnableScheduling注解,然后在 Spring 组件中添加带@Scheduled注解的方法:

java 复制代码
/**
 * 添加定时任务
 */
@Scheduled(cron = "0/2 * * * * *") // cron 表达式,每2秒执行
public void doTask() {
    log.info("定时任务开始执行 at: {}", LocalDateTime.now());
}

使用限制

对于固定频率的,fixedRate,因为不能获取其类和方法,故不能加入到任务管理中

java 复制代码
//每隔2秒执行一次
@Scheduled(fixedRate = 2000)
public void testTasks() {
    System.out.println("定时任务执行时间:" + dateFormat.format(new Date()));
}

但幸运地,可以转化为 Cron 表达式的,

java 复制代码
@Scheduled(cron = "0/2 * * * * *") // cron 表达式,每5秒执行
public void doTask() {
    System.out.println("我是定时任务~" + ATOMIC_LONG.getAndIncrement());
}

前端代码

css 资源不保证一定有效,所以需要你进一步美化~

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8"/>
    <title>Job Mgr</title>
    <link rel="stylesheet" type="text/css" href="http://www.ajaxjs.com/public/common.css"/>
    <link rel="stylesheet" type="text/css" href="http://www.ajaxjs.com/public/admin.css"/>
    <script src="http://www.ajaxjs.com/public/common.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js"></script>
    <style>
        .btns a{
            display: inline-block;
            background-color:#008ef0;
            border-radius: 5px;
            color: white;
            padding: 3px 8px;
            height:21px;
            line-height:20px;
            margin-right: 10px;
            font-size:12px;
        }
    </style>
</head>
<body class="inner-page">
<h2>任务调度管理</h2>
<br/>
<br/>
<div id="vue">
    <table class="list-table">
        <thead>
        <tr>
            <th>id</th>
            <th>任务名称</th>
            <th>执行类</th>
            <th>执行方法</th>
            <th>表达式</th>
            <th>状态</th>
            <th>创建时间</th>
            <th>操作</th>
        </tr>
        </thead>
        <tr v-for="(item) in mapList">
            <td>{{item.id}}</td>
            <td>
                <span v-show="editingId != item.id">{{item.name}}</span>
                <input type="text" v-show="editingId == item.id" v-model="item.name"/>
            </td>
            <td>
                <span v-show="editingId != item.id">{{item.className}}</span>
                <input type="text" v-show="editingId == item.id" v-model="item.className"/>
            </td>
            <td>{{item.method}}()</td>
            <td>
                <span v-show="editingId != item.id">{{item.express}}</span>
                <input type="text" v-show="editingId == item.id" v-model="item.express"/>
            </td>
            <td>{{{0:'进行中',1:'已暂停',2:'已删除'}[item.status]}}</td>
            <td>{{item.createDate}}</td>
            <td class="btns">
                <a href="#" @click="trigger(item.id)" title="执行一次任务">▶ 执行</a>
                <a href="#" @click="del(item.id)">✖ 删除</a>
                <a href="#" @click="pause(item.id)" v-if="item.status == 0">❚❚ 暂停</a>
                <a href="#" @click="resume(item.id)" v-if="item.status != 0">⟳ 恢复</a>
            </td>
        </tr>
    </table>
</div>

<script>
new Vue({
    el: '#vue',
    data: {
        mapList: [],
        create: {},
        isShowCreate: false,
        editingId: 0
    },
    mounted() {
        aj.xhr.get('http://localhost:8301/scheduled', json => {
            this.mapList = json.data.rows;
        });
    },
    methods: {
      trigger(id) {
        aj.xhr.postForm('http://localhost:8301/scheduled/trigger/' + id, null, (json) => {
                if (json && json.status == 1) {
                    alert('执行成功');
                    location.reload();
                }
            });
        },
        del(id) {
            if (confirm('确定删除?')) {
                let url = "http://localhost:8301/scheduled/remove/" + id;
                aj.xhr.postForm(url, {}, (json) => {
                    if (json && json.status == 1) {
                        alert('删除成功');
                        location.reload();
                    }
                });
            }
        },
        pause(id) {
            aj.xhr.postForm('http://localhost:8301/scheduled/pause/' + id, {}, (json) => {
                if (json && json.status == 1) {
                    alert('暂停成功');
                    location.reload();
                }
            });
        },
        resume(id) {
            aj.xhr.postForm('http://localhost:8301/scheduled/resume/' + id, {}, (json) => {
                if (json && json.status == 1) {
                    alert('恢复成功');
                    location.reload();
                }
            });
        }
    }
});
</script>
</body>
</html>

改进了的界面:

附文:Spring Task/Spring Scheduler 傻傻分不清

这里做一下"名词解释",分清楚这两者的区别:

Spring Task

  • Spring Task 是 Spring 框架自带的一个任务调度模块,提供了基本的任务调度功能。
  • 它是通过 Java 的 Timer 和 TimerTask 类来实现的,这两个类提供了一种简单的方式来安排和执行重复性任务。
  • Spring Task 可以通过@Scheduled注解将方法标记为定时任务,并指定任务的触发条件、执行时间间隔等属性。
  • Spring Task 适用于简单的定时任务和重复性任务,但在处理复杂任务、并发任务或需要更高级功能的场景下有限。

Spring Scheduler

Spring Scheduler 也称为 Spring Scheduling。

  • Spring Scheduling 是 Spring 框架对任务调度的一种增强支持,建立在 Spring Task 基础上。
  • 它使用了一个更强大、灵活且可扩展的任务调度器接口,例如TaskSchedulerThreadPoolTaskScheduler
  • Spring Scheduling 提供了比 Spring Task 更多的特性和配置选项,如异步执行任务、并发控制、任务取消和动态调度等。
  • 它还提供了更多的任务触发选项和灵活的表达式语法,例如 Cron 表达式。

总结起来,Spring Task 是 Spring 框架自带的一个简单任务调度模块,提供了基本的定时任务功能;而 Spring Scheduling 是对任务调度的增强支持,提供了更多特性和配置选项,适用于更复杂的任务调度需求。如果您只需要简单的定时任务,可以使用 Spring Task;如果需要更丰富的任务调度功能,可以选择 Spring Scheduling。

参考

其他同类的界面参考:

相关推荐
q***76561 小时前
工作中常用springboot启动后执行的方法
java·spring boot·后端
黄俊懿2 小时前
【架构师从入门到进阶】第一章:架构设计基础——第二节:架构设计原则
分布式·后端·中间件·架构
小冷coding3 小时前
【项目】医疗数字化升级:基于Spring生态的合规化微服务架构实践
spring·微服务·架构
xj198603193 小时前
maven导入spring框架
数据库·spring·maven
代码N年归来仍是新手村成员3 小时前
OpenClaw本地部署 + AWS Bedrock Claude 4.5 模型
后端·云计算·aws
树獭叔叔3 小时前
深度解析 GRPO:DeepSeek R1 背后“悟道”的逻辑引擎
后端·aigc·openai
是店小二呀3 小时前
MySQL 深度实践:表的约束及其在数据完整性中的作用
后端
树獭叔叔3 小时前
Transformer 的稳健基石:残差连接与 Pre-LN 深度解析
后端·aigc·openai