欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

spring-boot-2.0.3之quartz集成,数据源问题,源码探究

程序员文章站 2022-11-30 14:05:24
前言 开心一刻 着火了,他报警说:119吗,我家发生火灾了。 119问:在哪里? 他说:在我家。 119问:具体点。 他说:在我家的厨房里。 119问:我说你现在的位置。 他说:我趴在桌子底下。 119:我们怎样才能到你家? 他说:你们不是有消防车吗? 119说:烧死你个傻B算了。 路漫漫其修远兮, ......

前言

  开心一刻  

    着火了,他报警说:119吗,我家发生火灾了。
    119问:在哪里?
    他说:在我家。
    119问:具体点。
    他说:在我家的厨房里。
    119问:我说你现在的位置。
    他说:我趴在桌子底下。
    119:我们怎样才能到你家?
    他说:你们不是有消防车吗?
    119说:烧死你个傻b算了。

  路漫漫其修远兮,吾将上下而求索!
  github:
  码云(gitee):

前情回顾

  中,讲到了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自身是如何操作数据库的

      我们通过暂停任务来跟下源代码,如下图所示

spring-boot-2.0.3之quartz集成,数据源问题,源码探究

      发现获取connection的方式如下所示:

conn = dbconnectionmanager.getinstance().getconnection(getdatasource());

      很明显,dbconnectionmanager是单例的,通过dbconnectionmanager从数据源中获取数据库连接(conn),既然都拿到conn了,那操作数据库也就简单了。注意:getdatasource()获取的是数据源的名称,不是数据源!

      接下来我们再看看数据源是什么数据源,druid?还是quartz自己的数据源?

spring-boot-2.0.3之quartz集成,数据源问题,源码探究

      数据源还是用的我们应用的数据源(druid数据源),springboot自动将我们应用的数据源配置给了quartz。

      至此,该问题也就清晰了,总结下:springboot会自动将我们的应用数据源(druid数据源)配置给quartz,quartz操作数据库的时候从数据源中获取数据库连接,然后通过数据库连接对数据库进行操作。

    2、springboot是如何设置quartz数据源的

      凡是涉及到springboot自动配置的,去找spring-boot-autoconfigure-2.0.3.release.jar中spring.factories就对了,如下所示

spring-boot-2.0.3之quartz集成,数据源问题,源码探究

      关于spring.factories文件内容的读取,大家查阅;关于springboot的自动配置,我的springboot启动源码系列篇中还没有讲到。大家姑且先这样认为:

        当在类路径下能找到scheduler.class, schedulerfactorybean.class,platformtransactionmanager.class时(只要pom.xml有spring-boot-starter-quartz依赖,这些类就能在类路径下找到),quartzautoconfiguration就会被springboot当成配置类进行自动配置。

spring-boot-2.0.3之quartz集成,数据源问题,源码探究

      将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方法

spring-boot-2.0.3之quartz集成,数据源问题,源码探究
@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());

spring-boot-2.0.3之quartz集成,数据源问题,源码探究

      可以看到我们通过org.quartz.jobstore.datasource设置的dsname(quartzds)最后会被替换成springtxdatasource.加scheduler实例名(我们的应用中是:springtxdatasource.quartzscheduler),这也就是为什么我们通过dbconnectionmanager.getinstance().getconnection("quartzds")报以下错误的原因

spring-boot-2.0.3之quartz集成,数据源问题,源码探究
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内容如下

spring-boot-2.0.3之quartz集成,数据源问题,源码探究
@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;