前言
开心一刻
着火了, 他报警说: 119 吗, 我家发生火灾了.
119 问: 在哪里?
他说: 在我家.
119 问: 具体点.
他说: 在我家的厨房里.
119 问: 我说你现在的位置.
他说: 我趴在桌子底下.
119: 我们怎样才能到你家?
他说: 你们不是有消防车吗?
119 说: 烧死你个傻 B 算了.
路漫漫其修远兮, 吾将上下而求索!
GitHub: https://github.com/youzhibing
码云(gitee): https://gitee.com/youzhibing
前情回顾
上篇博客中, 讲到了 springboot 与 quartz 的集成, 非常简单, pow.xml 中引入 spring-boot-starter-quartz 依赖即可, 工程中就可以通过
- @Override
- private Scheduler scheduler;
自动注入 quartz 调度器, 然后我们就可以通过调度器对 quartz 组件: Trigger,JobDetail 进行添加与删除等操作, 实现对任务的调度.
结果也如我们预期一样, 每隔 10s 我们的 MyJob 的 executeinternal 方法就被调用, 打印一条信息: MyJob...
似乎一切都是那么顺利, 感觉集成 quartz 就是这么简单!
数据源问题
产生背景
如果定时任务不服务于业务, 那将毫无意义; 我们不能让定时任务只是空跑(或者打印一句: MyJob...), 如果是, 那么相信我, 把这个定时任务删了吧, 不要有任何留恋!
既然是服务于我们的业务, 那么很大程度上就会操作数据库; 我的业务需求就是凌晨某个时间点进行一次数据统计, 既要从数据库查数据, 也要将统计后的数据插入到数据库. 那么问题来了, 业务 job 中如何操作数据库?
业务 job 示例
- package com.lee.quartz.job;
- import org.quartz.JobExecutionContext;
- import org.quartz.JobExecutionException;
- import org.springframework.scheduling.quartz.QuartzJobBean;
- public class MyJob extends QuartzJobBean {
- private static final Logger LOGGER = LoggerFactory.getLogger(MyJob.class);
- @Override
- protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
- // TODO 如何进行数据库的操作
- System.out.println("MyJob...")
- }
- }
可以从 4 个方面来考虑(业务 job 中如何操作数据库):
1, 既然是 springboot 与 quartz 的集成, 那么我们能不能用 spring 的注入功能, 将我们的 mapper(集成了 mybatis)注入到业务 job 中了?
2, 利用 JobDetail 的 jobDataMap, 将我们的 mapper 传到业务 job 中
3,quartz 不是有它自己的 11 张表吗, 那它肯定有对数据库进行操作, 我们参考 quartz 是如何操作数据库的
4, 实在是不行, 我们自己创建数据库连接总行了吧
我们来逐个分析下以上 4 种方案
方案 4, 个人不推荐, 个人比较推荐连接池的方式来管理数据库连接, 但个人实现数据库连接池已经是个不小的挑战了, 没必要; 不到万不得已不采用此方案.
方案 1, 这个听起来好像很不错, 连接交由 spring 的数据源管理, 我们只需要用其中的连接操作数据库即可. 但看上面的 MyJob,spring 管理的 bean 能注入进来吗, 显然不能, 因为 MyJob 实例不受 spring 管理; 有小伙伴可能会认为这很简单, MyJob 实例让 spring 管理起来不就 OK 了! ok, 问题又来了, spring 管理的 MyJob 实例能用到 quartz 中吗, 不能! quartz 如何获取 MyJob 实例? 我们把 MyJob 的类全路径: com.lee.quartz.job.MyJob 传给了 quartz, 那么很显然 quartz 会根据这个类全路径, 然后通过反射来实例化 MyJob(这也是为什么业务 Job 一定要有无参构造方法的原因), 也就是 quartz 会重新创建 MyJob 实例, 与 spring 管理 MyJob 实例没有任何关系. 显然通过 spring 注入的方式是行不通的.
方案 2, 我们知道可以通过 JobDetail 进行参数的传递, 但有要求: 传递的参数必须能序列化(实现 Serializable); 我没测试此方案, 不过我想实现起来会有点麻烦.
方案 3, 这个好像可行, 我们可以看看 quartz 是如何进行数据库操作的, 我们把 quartz 的那套拿过来用是不是就行了呢?
说了这么多, 方案总结下:
1, 如何利用 quartz 的数据源 (或者数据库连接) 进行数据库操作
2, 引申下, 能不能将 quart 的数据源设置成我们应用的数据源, 让 quartz 与应用共用一个数据源, 方便统一管理?
源码探究
1,quartz 自身是如何操作数据库的
我们通过暂停任务来跟下源代码, 如下图所示
发现获取 connection 的方式如下所示:
conn = DBConnectionManager.getInstance().getConnection(getDataSource());
很明显, DBConnectionManager 是单例的, 通过 DBConnectionManager 从数据源中获取数据库连接 (conn), 既然都拿到 conn 了, 那操作数据库也就简单了. 注意: getDataSource() 获取的是数据源的名称, 不是数据源!
接下来我们再看看数据源是什么数据源, druid? 还是 quartz 自己的数据源?
数据源还是用的我们应用的数据源(druid 数据源),springboot 自动将我们应用的数据源配置给了 quartz.
至此, 该问题也就清晰了, 总结下: springboot 会自动将我们的应用数据源 (druid 数据源) 配置给 quartz,quartz 操作数据库的时候从数据源中获取数据库连接, 然后通过数据库连接对数据库进行操作.
2,springboot 是如何设置 quartz 数据源的
凡是涉及到 springboot 自动配置的, 去找 spring-boot-autoconfigure-2.0.3.RELEASE.jar 中 spring.factories 就对了, 如下所示
关于 spring.factories 文件内容的读取, 大家查阅此篇博文; 关于 springboot 的自动配置, 我的 springboot 启动源码系列篇中还没有讲到. 大家姑且先这样认为:
当在类路径下能找到 Scheduler.class, SchedulerFactoryBean.class,PlatformTransactionManager.class 时(只要 pom.xml 有 spring-boot-starter-quartz 依赖, 这些类就能在类路径下找到),QuartzAutoConfiguration 就会被 springboot 当成配置类进行自动配置.
将 quartz 的配置属性设置给 SchedulerFactoryBean; 将数据源设置给 SchedulerFactoryBean: 如果有 @QuartzDataSource 修饰的数据源, 则将 @QuartzDataSource 修饰的数据源设置给 SchedulerFactoryBean, 否则将应用的数据源 (druid 数据源) 设置给 SchedulerFactoryBean, 显然我们的应用中没有 @QuartzDataSource 修饰的数据源, 那么 SchedulerFactoryBean 中的数据源就是应用的数据源; 将事务管理器设置给 SchedulerFactoryBean.
SchedulerFactoryBean,Scheduler 的工程 bean, 负责创建和配置 quartz Scheduler; 它实现了 FactoryBean,InitializingBean,FactoryBean 的 getObject 方法实现的很简单, 如下
- @Override
- @Nullable
- public Scheduler getObject() {
- return this.scheduler;
- }
就是返回 scheduler 实例, 注册到 spring 容器中, 那么 scheduler 是在哪里实例化的呢, 就是在 afterPropertiesSet 中完成的, 关于 FactoryBean,InitializingBean 本文不做过多的讲解, 不了解的可以先去查阅下资料 (注意: InitializingBean 的 afterPropertiesSet() 先于 FactoryBean 的 getObject()执行). 接下来我们仔细看看 SchedulerFactoryBean 实现 InitializingBean 的 afterPropertiesSet 方法
- @Override
- public void afterPropertiesSet() throws Exception {
- if (this.dataSource == null && this.nonTransactionalDataSource != null) {
- this.dataSource = this.nonTransactionalDataSource;
- }
- if (this.applicationContext != null && this.resourceLoader == null) {
- this.resourceLoader = this.applicationContext;
- }
- // Initialize the Scheduler instance... 初始化 Scheduler 实例
- this.scheduler = prepareScheduler(prepareSchedulerFactory());
- try {
- registerListeners(); // 注册 Scheduler 相关监听器, 一般没有
- registerJobsAndTriggers(); // 注册 jobs 和 triggers, 一般没有
- }
- catch (Exception ex) {
- try {
- this.scheduler.shutdown(true);
- }
- catch (Exception ex2) {
- logger.debug("Scheduler shutdown exception after registration failure", ex2);
- }
- throw ex;
- }
- }
- View Code
我们来重点跟下: this.scheduler = prepareScheduler(prepareSchedulerFactory());
可以看到我们通过 org.quartz.jobStore.dataSource 设置的 dsName(quartzDs)最后会被替换成 springTxDataSource. 加 scheduler 实例名 (我们的应用中是: springTxDataSource.quartzScheduler), 这也就是为什么我们通过 DBConnectionManager.getInstance().getConnection("quartzDs") 报以下错误的原因
- java.sql.SQLException: There is no DataSource named 'quartzDs'
- at org.quartz.utils.DBConnectionManager.getConnection(DBConnectionManager.java:104)
- at com.lee.quartz.job.FetchDataJob.executeInternal(FetchDataJob.java:24)
- at org.springframework.scheduling.quartz.QuartzJobBean.execute(QuartzJobBean.java:75)
- at org.quartz.core.JobRunShell.run(JobRunShell.java:202)
- at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:573)
- View Code
LocalDataSourceJobStore 的 initialize 内容如下
- @Override
- public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException {
- // Absolutely needs thread-bound DataSource to initialize.
- this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource();
- if (this.dataSource == null) {
- throw new SchedulerConfigException("No local DataSource found for configuration -" +
- "'dataSource' property must be set on SchedulerFactoryBean");
- }
- // Configure transactional connection settings for Quartz.
- setDataSource(TX_DATA_SOURCE_PREFIX + getInstanceName());
- setDontSetAutoCommitFalse(true);
- // Register transactional ConnectionProvider for Quartz.
- DBConnectionManager.getInstance().addConnectionProvider(
- TX_DATA_SOURCE_PREFIX + getInstanceName(),
- new ConnectionProvider() {
- @Override
- public Connection getConnection() throws SQLException {
- // Return a transactional Connection, if any.
- return DataSourceUtils.doGetConnection(dataSource);
- }
- @Override
- public void shutdown() {
- // Do nothing - a Spring-managed DataSource has its own lifecycle.
- }
- /* Quartz 2.2 initialize method */
- public void initialize() {
- // Do nothing - a Spring-managed DataSource has its own lifecycle.
- }
- }
- );
- // Non-transactional DataSource is optional: fall back to default
- // DataSource if not explicitly specified.
- DataSource nonTxDataSource = SchedulerFactoryBean.getConfigTimeNonTransactionalDataSource();
- final DataSource nonTxDataSourceToUse = (nonTxDataSource != null ? nonTxDataSource : this.dataSource);
- // Configure non-transactional connection settings for Quartz.
- setNonManagedTXDataSource(NON_TX_DATA_SOURCE_PREFIX + getInstanceName());
- // Register non-transactional ConnectionProvider for Quartz.
- DBConnectionManager.getInstance().addConnectionProvider(
- NON_TX_DATA_SOURCE_PREFIX + getInstanceName(),
- new ConnectionProvider() {
- @Override
- public Connection getConnection() throws SQLException {
- // Always return a non-transactional Connection.
- return nonTxDataSourceToUse.getConnection();
- }
- @Override
- public void shutdown() {
- // Do nothing - a Spring-managed DataSource has its own lifecycle.
- }
- /* Quartz 2.2 initialize method */
- public void initialize() {
- // Do nothing - a Spring-managed DataSource has its own lifecycle.
- }
- }
- );
- // No, if HSQL is the platform, we really don't want to use locks...
- try {
- String productName = JdbcUtils.extractDatabaseMetaData(this.dataSource, "getDatabaseProductName");
- productName = JdbcUtils.commonDatabaseName(productName);
- if (productName != null && productName.toLowerCase().contains("hsql")) {
- setUseDBLocks(false);
- setLockHandler(new SimpleSemaphore());
- }
- }
- catch (MetaDataAccessException ex) {
- logWarnIfNonZero(1, "Could not detect database type. Assuming locks can be taken.");
- }
- super.initialize(loadHelper, signaler);
- }
- View Code
注册两个 ConnectionProvider 给 quartz: 一个 dsName 叫 springTxDataSource.quartzScheduler, 有事务; 一个 dsName 叫 springNonTxDataSource.quartzScheduler, 没事务; 所以我们通过 DBConnectionManager 获取 connection 时, 通过指定 dsName 就能获取支持事务或不支持事务的 connection.
另外, SchedulerFactoryBean 实现了 SmartLifecycle, 会在 ApplicationContext refresh 的时候启动 Schedule,ApplicationContext shutdown 的时候停止 Schedule.
总结
1,springboot 集成 quartz, 应用启动过程中会自动调用 schedule 的 start 方法来启动调度器, 也就相当于启动了 quartz, 原因是 SchedulerFactoryBean 实现了 SmartLifecycle 接口;
2,springboot 会自动将我们应用的数据源配置给 quartz, 在我们示例应用中数据源是 druid 数据源, 应用和 quartz 都是用的此数据源;
3, 通过 org.quartz.jobStore.dataSource 设置的数据源名会被覆盖掉, 当我们通过 quartz 的 DBConnectionManager 获取 connection 时, 默认情况 dbName 给 springTxDataSource.quartzScheduler 或者 springNonTxDataSource.quartzScheduler, 一个支持事务, 一个不支持事务; 至于怎样自定义 dsName, 我还没去尝试, 有兴趣的小伙伴可以自己试试;
4,springboot 集成 quartz, 只是将 quartz 的一些通用配置给配置好了, 如果我们对 quartz 十分熟悉, 那么就很好理解, 但如果对 quartz 不熟悉(楼主对 quartz 就不熟悉), 那么很多时候出了问题就无从下手了, 所以建议大家先熟悉 quartz;
来源: https://www.cnblogs.com/youzhibing/p/10056696.html