前言
开心一刻
快过年了, 大街上, 爷爷在给孙子示范摔炮怎么放, 嘴里还不停念叨: 要像这样, 用劲甩才能响. 示范了一个, 两个, 三个... 孙子终于忍不住了, 抱着爷爷的腿哭起来: 爷呀, 你给我剩个吧!
新的一年祝大家: 健健康康, 快快乐乐!
GitHub:https://github.com/youzhibing
码云(gitee):https://gitee.com/youzhibing
前情回顾与问题
spring-boot-2.0.3 之 quartz 集成, 不是你想的那样哦! 讲到了 quartz 的基本概念, 以及 springboot 与 quartz 的集成; 集成非常简单, 引入相关依赖即可, 此时我们 job 存储方式采用的是 jdbc.
spring-boot-2.0.3 之 quartz 集成, 数据源问题, 源码探究 讲到了 quartz 的数据源问题, 如果我们没有 @QuartzDataSource 修饰的数据源, 那么默认情况下就是我们的工程数据源, springboot 会将工程数据源设置给 quartz; 为什么需要数据源, 因为我们的 job 不会空跑, 往往会进行数据库的操作, 那么就会用到数据库连接, 而获取数据库连接最常用的的方式就是从数据源获取.
后续使用过程中, 发现了一些问题:
1,spring 注入, job 到底能不能注入到 spring 容器, job 中能不能自动注入我们的 mapper(spring 的 autowired);
2,job 存储方式, 到底用 JDBC 还是 MEMORY, 最佳实践是什么
3, 调度失准, 没有严格按照我们的 cron 配置进行
spring 注入
spring-boot-2.0.3 之 quartz 集成, 数据源问题, 源码探究中我还分析的井井有条, 并很自信的得出结论: job 不能注入到 spring, 也不能享受 spring 的自动注入
那时候采用的是从 quartz 数据源中获取 connection, 然后进行 jdbc 编程, 发现 jdbc 用起来真的不舒服(不是说有问题, mybatis,spring jdbcTemplate 等底层也是 jdbc), 此时我就有了一个疑问: quartz job 真的不能注入到 spring, 不能享受 spring 的自动注入吗? 结论可想而知: 能!
打的真疼
job 能不能注入到 spring 容器? 答案是可以的(各种注解:@Compoment,@Service,@Repository 等), 只是我们将 job 注入到 spring 容器有意义吗? 我们知道 quartz 是通过反射来实例化 job 的(具体实例化过程请往下看), 与 spring 中已存在的 job bean 没有任何关联, 我们将 job 注入到 spring 也只是使 spring 中多了一个没调用者的 bean 而已, 没有任何意义. 这个问题应该换个方式来问: job 有必要注入到 spring 容器中吗? 很显然没必要.
job 中能不能注入 spring 中的常规 bean 了? 答案是可以的. 我们先来看下 springboot 官网是如何描述的: job 可以定义 setter 来注入 data map 属性, 也可以以类似的方式注入常规 bean, 如下所示
- public class SampleJob extends QuartzJobBean {
- private MyService myService;
- private String name;
- // Inject "MyService" bean (注入 spring 常规 bean)
- public void setMyService(MyService myService) { ... }
- // Inject the "name" job data property (注入 job data 属性)
- public void setName(String name) { ... }
- @Override
- protected void executeInternal(JobExecutionContext context)
- throws JobExecutionException {
- ...
- }
- }
- View Code
实现
pom.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.lee</groupId>
- <artifactId>spring-boot-quartz</artifactId>
- <version>1.0-SNAPSHOT</version>
- <properties>
- <java.version>1.8</java.version>
- <maven.compiler.source>1.8</maven.compiler.source>
- <maven.compiler.target>1.8</maven.compiler.target>
- <druid.version>1.1.10</druid.version>
- <pagehelper.version>1.2.5</pagehelper.version>
- <druid.version>1.1.10</druid.version>
- </properties>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.0.3.RELEASE</version>
- </parent>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-thymeleaf</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-quartz</artifactId>
- </dependency>
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>druid-spring-boot-starter</artifactId>
- <version>${druid.version}</version>
- </dependency>
- <dependency>
- <groupId>MySQL</groupId>
- <artifactId>MySQL-connector-java</artifactId>
- </dependency>
- <dependency>
- <groupId>com.GitHub.pagehelper</groupId>
- <artifactId>pagehelper-spring-boot-starter</artifactId>
- <version>${pagehelper.version}</version>
- </dependency>
- <!-- 日志 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-logging</artifactId>
- <exclusions> <!-- 排除 spring-boot-starter-logging 中的全部依赖 -->
- <exclusion>
- <groupId>*</groupId>
- <artifactId>*</artifactId>
- </exclusion>
- </exclusions>
- <scope>test</scope> <!-- 打包的时候不打 spring-boot-starter-logging.jar -->
- </dependency>
- <dependency>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-classic</artifactId>
- </dependency>
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- <optional>true</optional>
- </dependency>
- </dependencies>
- <build>
- <finalName>spring-boot-quartz</finalName>
- <plugins>
- <!-- 打包项目 mvn clean package -->
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
- View Code
application.YAML
- server:
- port: 9001
- servlet:
- context-path: /quartz
- spring:
- thymeleaf:
- mode: html
- cache: false
- #连接池配置
- datasource:
- type: com.alibaba.druid.pool.DruidDataSource
- name: ownDataSource
- druid:
- driver-class-name: com.MySQL.jdbc.Driver
- url: jdbc:MySQL://localhost:3306/spring-boot-quartz?useSSL=false&useUnicode=true
- username: root
- password: 123456
- initial-size: 1 #连接池初始大小
- max-active: 20 #连接池中最大的活跃连接数
- min-idle: 1 #连接池中最小的活跃连接数
- max-wait: 60000 #配置获取连接等待超时的时间
- pool-prepared-statements: true #打开 PSCache, 并且指定每个连接上 PSCache 的大小
- max-pool-prepared-statement-per-connection-size: 20
- validation-query: SELECT 1 FROM DUAL
- validation-query-timeout: 30000
- test-on-borrow: false #是否在获得连接后检测其可用性
- test-on-return: false #是否在连接放回连接池后检测其可用性
- test-while-idle: true #是否在连接空闲一段时间后检测其可用性
- quartz:
- #相关属性配置
- properties:
- org:
- quartz:
- scheduler:
- instanceName: quartzScheduler
- instanceId: AUTO
- threadPool:
- class: org.quartz.simpl.SimpleThreadPool
- threadCount: 10
- threadPriority: 5
- threadsInheritContextClassLoaderOfInitializingThread: true
- #mybatis 配置
- mybatis:
- type-aliases-package: com.lee.quartz.entity
- mapper-locations: classpath:mybatis/mapper/*.xml
- # 分页配置, pageHelper 是物理分页插件
- pagehelper:
- #4.0.0 以后版本可以不设置该参数, 该示例中是 5.1.4
- helper-dialect: MySQL
- #启用合理化, 如果 pageNum<1 会查询第一页, 如果 pageNum>pages 会查询最后一页
- reasonable: true
- logging:
- level:
- com.lee.quartz.mapper: debug
- View Code
FetchDataJob.java
- package com.lee.quartz.job;
- import com.lee.quartz.entity.User;
- import com.lee.quartz.mapper.UserMapper;
- import org.quartz.JobExecutionContext;
- import org.quartz.JobExecutionException;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.scheduling.quartz.QuartzJobBean;
- import java.util.Random;
- import java.util.stream.IntStream;
- public class FetchDataJob extends QuartzJobBean {
- private static final Logger LOGGER = LoggerFactory.getLogger(FetchDataJob.class);
- @Autowired
- private UserMapper userMapper;
- @Override
- protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
- // TODO 业务处理
- Random random = new Random();
- IntStream intStream = random.ints(18, 100);
- int first = intStream.limit(1).findFirst().getAsInt();
- int count = userMapper.saveUser(new User("zhangsan" + first, first));
- if (count == 0) {
- LOGGER.error("用户保存失败!");
- return;
- }
- LOGGER.info("用户保存成功");
- }
- }
- View Code
如上, FetchDataJob 中是可以注入 userMapper 的, 完整代码请看: spring-boot-quartz-plus
job 实例化过程源码解析
还记得 SchedulerFactoryBean 的创建吗, 可以看看这里, 我们从 SchedulerFactoryBean 开始
QuartzSchedulerThread 线程的启动
QuartzSchedulerThread 声明如下
View Code
负责触发 QuartzScheduler 注册的 Triggers, 可以理解成 quartz 的主线程 (守护线程). 我们从 SchedulerFactoryBean 的 afterPropertiesSet() 开始
QuartzSchedulerThread 继承了 Thread, 通过 DefaultThreadExecutor 的 execute()启动了 QuartzSchedulerThread 线程
jobFactory 的创建与替换
AutowireCapableBeanJobFactory 实例后续会赋值给 quartz, 作为 quartz job 的工厂, 具体在哪赋值给 quartz 的了, 我们往下看
当 quartz scheduler 创建完成后, 将 scheduler 的 jobFactory 替换成了 AutowireCapableBeanJobFactory.
job 的创建与执行
QuartzSchedulerThread 在上面已经启动了, AutowireCapableBeanJobFactory 也已经赋值给了 scheduler; 我们来看看 QuartzSchedulerThread 的 run(), 里面有 job 的创建与执行
最终会调用 AutowireCapableBeanJobFactory 的 createJobInstance 方法, 通过反射创建了 job 实例, 还向 job 实例中填充了 job data map 属性和 spring 常规 bean. 具体 this.beanFactory.autowireBean(jobInstance); 是如何向 job 实例填充 spring 常规 bean 的, 需要大家自己去跟了. job 被封装成了 JobRunShell(实现了 Runnable), 然后从线程池中取第一个线程来执行 JobRunShell, 最终会执行到 FetchDataJob 的 executeInternal, 处理我们的业务; quartz 的线程实现与线程机制, 有兴趣的小伙伴自行去看.
小结下: 先启动 QuartzSchedulerThrea 线程, 然后将 quartz 的 jobFactory 替换成 AutowireCapableBeanJobFactory;QuartzSchedulerThread 是一个守护线程, 会按规则处理 trigger 和 job(要成对存在), 最终完成我们的定时业务.
job 存储方式
JobStore 是负责跟踪调度器 (scheduler) 中所有的工作数据: 作业任务, 触发器, 日历等. 我们无需在我们的代码中直接使用 JobStore 实例, 只需要通过配置信息告知 Quartz 该用哪个 JobStore 即可. quartz 的 JobStore 有两种: RAMJobStore,JDBCJobStore, 通过名字我们也能猜到这两者之间的区别与优缺点
上述两种 JobStore 对应到 springboot 就是: MEMORY,JDBC
- /*
- * Copyright 2012-2017 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.springframework.boot.autoconfigure.quartz;
- /**
- * Define the supported Quartz {@code JobStore}.
- *
- * @author Stephane Nicoll
- * @since 2.0.0
- */
- public enum JobStoreType {
- /**
- * Store jobs in memory.
- */
- MEMORY,
- /**
- * Store jobs in the database.
- */
- JDBC
- }
- View Code
至于选择哪种方式, 就看哪种方式更契合我们的业务需求, 没有绝对的选择谁与不选择谁, 只看哪种更合适. 据我的理解和工作中的应用, 内存方式用的更多; 实际应用中, 我们往往只是持久化我们自定义的基础 job(不是 quartz 的 job)到数据库, 应用启动的时候加载基础 job 到 quartz 中, 进行 quartz job 的初始化, quartz 的 job 相关信息全部存储在 RAM 中; 一旦应用停止, quartz 的 job 信息全部丢失, 但这影响不大, 可以通过我们的自定义 job 进行 quartz job 的恢复, 但是恢复的 quartz job 是原始状态, 如果需要实时保存 quartz job 的状态, 那就需要另外设计或者用 JDBC 方式了.
调度失准
当存储方式是 JDBCJobStore 时, 会出现调度失准的情况, 没有严格按照配置的 cron 表达式执行, 例如 cron 表达式: 1 */1 * * * ?, 日志输入如下
秒数会有不对, 但这影响比较小, 我们还能接受, 可是时间间隔有时候却由 1 分钟变成 2 分钟, 甚至 3 分钟, 这个就有点接受不了. 具体原因我还没有查明, 个人觉得可能和数据库持久化有关.
当存储方式是 RAMJobStore 时, 调度很准, 还未发现调度失准的情况, cron 表达式: 3 */1 * * * ?, 日志输入如下
总结
1,quartz job 无需注入到 spring 容器中(注入进去了也没用), 但 quartz job 中是可以注入 spring 容器中的常规 bean 的, 当然还可以注入 jab data map 中的属性值;
2, springboot 覆写了 quartz 的 jobFactory, 使得 quartz 在调用 jobFactory 创建 job 实例的时候, 能够将 spring 容器的 bean 注入到 job 中, AutowireCapableBeanJobFactory 中 createJobInstance 方法如下
- @Override
- protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
- Object jobInstance = super.createJobInstance(bundle); // 通过反射实例化 job, 并将 JobDataMap 中属性注入到 job 实例中
- this.beanFactory.autowireBean(jobInstance); // 注入 job 依赖的 spring 中的 bean
- this.beanFactory.initializeBean(jobInstance, null);
- return jobInstance;
- }
3, 最佳实践
JobStore 选择 RAMJobStore; 持久化我们自定义的 job, 应用启动的时候将我们自定义的 job 都加载给 quartz, 初始化 quartz job;quartz job 状态改变的时候, 分析清楚是否需要同步到我们自定义的 job 中, 有则同步改变自定义 job 状态.
参考
Quartz Scheduler
来源: https://www.cnblogs.com/youzhibing/p/10208056.html