quartz.md 13.0 KB
Newer Older
沉默王二's avatar
沉默王二 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13
---
title: Spring Boot 整合Quartz实现编程喵定时发布文章
shortTitle: 整合Quartz
category:
  - Java企业级开发
tag:
  - Spring Boot
---

### 前言

编程喵🐱实战项目中需要做一个定时发布文章的功能,于是我就很自然地想到了 Quartz,这是一个老而弥坚的开源任务调度框架。

沉默王二's avatar
沉默王二 已提交
14
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-39e31fbf-5546-4627-9d49-651beeb961c1.png)
沉默王二's avatar
沉默王二 已提交
15 16 17 18 19 20


记得我在 14 年开发大宗期货交易平台的时候就用到了它,每天凌晨定时需要统计一波交易数据,生成日报报表,「配合 Cron 表达式」(上一节有讲)用起来非常自洽。

可惜后来平台稳定了,新的政策出来了,直接把大宗期货交易灭了。于是我发财的机会也随着破灭了。想想都觉得可惜,哈哈哈。

沉默王二's avatar
沉默王二 已提交
21
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-573fa3b6-551b-418d-9616-3066bb4f75d2.png)
沉默王二's avatar
沉默王二 已提交
22 23 24 25 26 27 28

时光荏苒,Quartz 发展到现在,已经可以和 Spring Boot 项目无缝衔接了,用起来也比之前在 Spring 项目中更丝滑。

### 关于 Quartz

Quartz 是一款功能强大的开源的任务调度框架,在 GitHub 上已经累计有 5k+ 的 star 了。小到单机应用,大到分布式,都可以整合 Quartz。

沉默王二's avatar
沉默王二 已提交
29
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-ea2d0b63-4b99-4654-a03d-45023a741e88.png)
沉默王二's avatar
沉默王二 已提交
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91

在使用 Quartz 之前,让我们先来搞清楚 4 个核心概念:

- Job:任务,要执行的具体内容。
- JobDetail:任务详情,Job 是它要执行的内容,同时包含了这个任务调度的策略和方案。
- Trigger:触发器,可以通过 Cron 表达式来指定任务执行的时间。
- Scheduler:调度器,可以注册多个 JobDetail 和 Trigger,用来调度、暂停和删除任务。

### 整合 Quartz

Quartz 存储任务的方式有两种,一种是使用内存,另外一种是使用数据库。内存在程序重启后就丢失了,所以我们这次使用数据库的方式来进行任务的持久化。

第一步,在 pom.xml 文件中添加 Quartz 的 starter。

```
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>2.6.7</version>
</dependency>
```

第二步,在 application.yml 添加 Quartz 相关配置,配置说明直接看注释。

```
spring:
  quartz:
    job-store-type: jdbc # 默认为内存 memory 的方式,这里我们使用数据库的形式
    wait-for-jobs-to-complete-on-shutdown: true # 关闭时等待任务完成
    overwrite-existing-jobs: true # 可以覆盖已有的任务
    jdbc:
      initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构
    properties: # quartz原生配置
      org:
        quartz:
          scheduler:
            instanceName: scheduler # 调度器实例名称
            instanceId: AUTO # 调度器实例ID自动生成
          # JobStore 相关配置
          jobStore:
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 使用完全兼容JDBC的驱动
            tablePrefix: QRTZ_ # Quartz 表前缀
            useProperties: false # 是否将JobDataMap中的属性转为字符串存储
          # 线程池相关配置
          threadPool:
            threadCount: 25 # 线程池大小。默认为 10 。
            threadPriority: 5 # 线程优先级
            class: org.quartz.simpl.SimpleThreadPool # 指定线程池实现类,对调度器提供固定大小的线程池
```

Quartz 默认使用的是内存的方式来存储任务,为了持久化,我们这里改为 JDBC 的形式,并且指定 `spring.quartz.jdbc.initialize-schema=never`,这样我们可以手动创建数据表。因为该值的另外两个选项ALWAYS和EMBEDDED都不太符合我们的要求:

- ALWAYS:每次都初始化
- EMBEDDED:只初始化嵌入式数据库,比如说 H2、HSQL

那手动创建数据表的 SQL 语句去哪里找呢?

>GitHub 地址:[https://github.com/quartz-scheduler/quartz/tree/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore](https://github.com/quartz-scheduler/quartz/tree/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore)

为了方便小伙伴们下载,我把它放在了本教程的源码里面了:

沉默王二's avatar
沉默王二 已提交
92
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-407d9133-7487-444e-83dd-d11524bfd748.png)
沉默王二's avatar
沉默王二 已提交
93 94 95 96


如果使用 Intellij IDEA 旗舰版的话,首次打开 SQL 文件的时候会提示你指定数据源。在上图中,我配置了本地的 MySQL 数据库,导入成功后可以在数据库中查看到以下数据表:

沉默王二's avatar
沉默王二 已提交
97
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-277dc414-4d2d-4a68-91d5-10332998c8bf.png)
沉默王二's avatar
沉默王二 已提交
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252

Quartz数据库核心表如下:

Table Name |	Description
--- | ---
QRTZ_CALENDARS |	存储Quartz的Calendar信息
QRTZ_CRON_TRIGGERS |	存储CronTrigger,包括Cron表达式和时区信息
QRTZ_FIRED_TRIGGERS |	存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
QRTZ_PAUSED_TRIGGER_GRPS |	存储已暂停的Trigger组的信息
QRTZ_SCHEDULER_STATE |	存储少量的有关Scheduler的状态信息,和别的Scheduler实例
QRTZ_LOCKS |	存储程序的悲观锁的信息
QRTZ_JOB_DETAILS |	存储每一个已配置的Job的详细信息
QRTZ_JOB_LISTENERS |	存储有关已配置的JobListener的信息
QRTZ_SIMPLE_TRIGGERS |	存储简单的Trigger,包括重复次数、间隔、以及已触的次数
QRTZ_BLOG_TRIGGERS |	Trigger作为Blob类型存储
QRTZ_TRIGGER_LISTENERS |	存储已配置的TriggerListener的信息
QRTZ_TRIGGERS |	存储已配置的Trigger的信息

剩下的就是对 Quartz 的 scheduler、jobStore 和 threadPool 配置。

第三步,创建任务调度的接口 IScheduleService,定义三个方法,分别是通过 Cron 表达式来调度任务、指定时间来调度任务,以及取消任务。

```java
public interface IScheduleService {
    /**
     * 通过 Cron 表达式来调度任务
     */
    String scheduleJob(Class<? extends Job> jobBeanClass, String cron, String data);

    /**
     * 指定时间来调度任务
     */
    String scheduleFixTimeJob(Class<? extends Job> jobBeanClass, Date startTime, String data);

    /**
     * 取消定时任务
     */
    Boolean cancelScheduleJob(String jobName);
}
```

第四步,创建任务调度业务实现类 ScheduleServiceImpl,通过Scheduler、CronTrigger、JobDetail的API来实现对应的方法。

```java
@Slf4j
@Service
public class ScheduleServiceImpl implements IScheduleService {
    private String defaultGroup = "default_group";

    @Autowired
    private Scheduler scheduler;
    @Override
    public String scheduleJob(Class<? extends Job> jobBeanClass, String cron, String data) {
        String jobName = UUID.fastUUID().toString();
        JobDetail jobDetail = JobBuilder.newJob(jobBeanClass)
                .withIdentity(jobName, defaultGroup)
                .usingJobData("data", data)
                .build();
        //创建触发器,指定任务执行时间
        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .withIdentity(jobName, defaultGroup)
                .withSchedule(CronScheduleBuilder.cronSchedule(cron))
                .build();
        // 调度器进行任务调度
        try {
            scheduler.scheduleJob(jobDetail, cronTrigger);
        } catch (SchedulerException e) {
            log.error("任务调度执行失败{}", e.getMessage());
        }
        return jobName;
    }

    @Override
    public String scheduleFixTimeJob(Class<? extends Job> jobBeanClass, Date startTime, String data) {
        //日期转CRON表达式
        String startCron = String.format("%d %d %d %d %d ? %d",
                DateUtil.second(startTime),
                DateUtil.minute(startTime),
                DateUtil.hour(startTime, true),
                DateUtil.dayOfMonth(startTime),
                DateUtil.month(startTime) + 1,
                DateUtil.year(startTime));
        return scheduleJob(jobBeanClass, startCron, data);
    }

    @Override
    public Boolean cancelScheduleJob(String jobName) {
        boolean success = false;
        try {
            // 暂停触发器
            scheduler.pauseTrigger(new TriggerKey(jobName, defaultGroup));
            // 移除触发器中的任务
            scheduler.unscheduleJob(new TriggerKey(jobName, defaultGroup));
            // 删除任务
            scheduler.deleteJob(new JobKey(jobName, defaultGroup));
            success = true;
        } catch (SchedulerException e) {
            log.error("任务取消失败{}", e.getMessage());
        }
        return success;
    }
}
```

第五步,定义好要执行的任务,继承 QuartzJobBean 类,实现 
executeInternal 方法,这里只定义一个定时发布文章的任务。

```java
@Slf4j
@Component
public class PublishPostJob extends QuartzJobBean {
    @Autowired
    private IScheduleService scheduleService;
    @Autowired
    private IPostsService postsService;

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        Trigger trigger = jobExecutionContext.getTrigger();
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        Long data = jobDataMap.getLong("data");
        log.debug("定时发布文章操作:{}",data);

        // 获取文章的 ID后获取文章,更新文章为发布的状态,还有发布的时间
        boolean success = postsService.updatePostByScheduler(data);

        //完成后删除触发器和任务
        if (success) {
            log.debug("定时任务执行成功,开始清除定时任务");
            scheduleService.cancelScheduleJob(trigger.getKey().getName());
        }
    }
}
```

第六步,发布文章的接口里 PostsServiceImpl 添加定时发布的任务调度方法。

```java
@Service
public class PostsServiceImpl extends ServiceImpl<PostsMapper, Posts> implements IPostsService {

    private void handleScheduledAfter(Posts posts) {
        // 文章已经保存为草稿了,并且拿到了文章 ID
        // 调用定时任务
        String jobName = scheduleService.scheduleFixTimeJob(PublishPostJob.class, posts.getPostDate(), posts.getPostsId().toString());
        LOGGER.debug("定时任务{}开始执行", jobName);
    }

}
```

好,我们现在启动服务,通过Swagger 来测试一下,注意设置文章的定时发布时间。


沉默王二's avatar
沉默王二 已提交
253
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-12f8b138-33db-4faa-b31f-1d1b3fa5afe9.png)
沉默王二's avatar
沉默王二 已提交
254 255 256 257

查看 Quartz 的数据表 qrtz_cron_triggers,发现任务已经添加进来了。


沉默王二's avatar
沉默王二 已提交
258
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-0acacdc6-3cf3-4042-a784-388bb10f0368.png)
沉默王二's avatar
沉默王二 已提交
259 260 261

qrtz_job_details 表里也可以查看具体的任务详情。

沉默王二's avatar
沉默王二 已提交
262
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-ef1bd1b0-0f13-4dde-a84b-a1ae20b78430.png)
沉默王二's avatar
沉默王二 已提交
263 264 265 266

文章定时发布的时间到了之后,在日志里也可以看到 Quartz 的执行日志。


沉默王二's avatar
沉默王二 已提交
267
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/springboot/quartz-45abe530-05fe-498b-b32a-0d2bc5bd6996.png)
沉默王二's avatar
沉默王二 已提交
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284

再次查看 Quartz 数据表 qrtz_cron_triggers 和 qrtz_job_details 的时候,也会发现定时任务已经清除了。

整体上来说,Spring Boot 整合 Quartz还是非常丝滑的,配置少,步骤清晰,比 Spring Task 更强大,既能针对内存也能持久化,所以大家在遇到定时任务的时候完全可以尝试一把。

完整的功能在编程喵实战项目中已经实现了,可以把编程喵导入到本地尝试一下。

### 业务梳理

简单来梳理一下编程喵定时发布文章的业务。

1)用户在发布文章的时候可以选择定时发布,如果选择定时发布,那么就要设置定时发布的时间,暂时规定至少十分钟以后可以定时。

2)当管理端用户选择了定时发布,那么在保存文章的时候,文章状态要先设置为草稿状态,对前端用户是不可见的状态。

----

沉默王二's avatar
沉默王二 已提交
285
更多内容,只针对《二哥的Java进阶之路》星球用户开放,需要的小伙伴可以[戳链接🔗](https://javabetter.cn/zhishixingqiu/)加入我们的星球,一起学习,一起卷。。**编程喵**🐱是一个 Spring Boot+Vue 的前后端分离项目,融合了市面上绝大多数流行的技术要点。通过学习实战项目,你可以将所学的知识通过实践进行检验、你可以拓宽自己的技术边界,你可以掌握一个真正的实战项目是如何从 0 到 1 的。
沉默王二's avatar
沉默王二 已提交
286 287 288 289 290 291 292 293 294


### 源码路径

> - 编程喵:[https://github.com/itwanger/coding-more](https://github.com/itwanger/coding-more)
> - codingmore-quartz:[https://github.com/itwanger/codingmore-learning](https://github.com/itwanger/codingmore-learning/tree/main/codingmore-quartz)

---

沉默王二's avatar
沉默王二 已提交
295
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png)