Spring+Quartz实现动态定制定时任务并适配各种数据库

在这边记录一下关于使用quartz框架的一些问题和解决方案,其中关于quartz的一些基本使用方式和基本配置都不会提到,网上有关资料特别多。

首先使用quartz有如下几种方式:

1.使用内存机制,采用默认的配置文件,直接在代码当中配置定时任务的执行类以及对应的执行时间。
2.使用数据库,需要新建配置文件quartz.properties配置数据库连接,同时其余如上。
3.结合spring,在xml文件中配置SchedulerFactoryBean,CronTriggerFactoryBean,MethodInvokingJobDetailFactoryBean,外加一个任务执行类的容器,合计4个Bean,就能够完成一个定时任务的配置,无需任何代码片段,除过执行类。
4.结合spring,但是任务内容和执行时间都由用户动态制定,此时在配置文件中配置一个SchedulerFactoryBean即可,然后在动态获取这个容器,往其中按照方式1的方法添加内容。
其实往大的方向说,quartz有两种模式:集群和非集群。只不过集群模式必须使用数据库,同时需要在配置文件中的org.quartz.jobStore.isClustered = true,这样即可。
我参与的项目重点使用了3和4,3主要完成一些比较常规的定时任务,比如定时更新数据库类似的任务。而4就是完成用户自己的需求。
现在开始讲解整个过程:
1.基本配置
首先看一下applicationContext-quartz.xml

    <!-- schedulerBean -->
    <bean id="chedulerFactoryBean"
        class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <!-- 指定quartz配置文件 -->
        <property name="configLocation" value="classpath:quartz.properties" />
         <!--必QuartzScheduler 延时启动,应用启动完后 QuartzScheduler 再启动,单位秒 -->    
        <property name="startupDelay" value="3" />    
        <!-- 设置自动启动 -->    
        <property name="autoStartup" value="true" />  
        <property name="applicationContextSchedulerContextKey" value="applicationContextKey" />
        <property name="schedulerName" value="DefaultQuartzScheduler" />
        <property name="dataSource" ref="dataSource"/>
    </bean>

其中的参数都比较简单,就是关于dataSource的话,使用的是spring的配置,而不是在配置文件quartz.properties的配置,所以配置文件中就不需要配置了。下面是dataSource的配置:


    <!-- 数据源配置, 使用Tomcat JDBC连接池 -->
    <bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource" destroy-method="close">
        <property name="driverClassName" value="${jdbc.driver}" />
        <property name="url" value="${jdbc.url}" />
        <property name="username" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />

        <property name="maxActive" value="${jdbc.pool.maxActive}" />
        <property name="maxIdle" value="${jdbc.pool.maxIdle}" />
        <property name="minIdle" value="0" />
        <property name="maxWait" value="${jdbc.pool.maxWait}" />
        <property name="defaultAutoCommit" value="true" />
    </bean>

接下来看一下quartz.properties中的其中一点

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT

这个参数和往常不太一样(正常应该是org.quartz.impl.jdbcjobstore.JobStoreTX),如果你使用了spring的dataSource,必须这样配置,因为spring默认使用的jobStore类是LocalDataSourceJobStore,而这个类继承的是JobStoreCMT,而不是JobStoreTX,至于这两者之间的区别,网上有比较多的讲解。
2. 定时任务的一些工具类
前面我们只定义了Scheduler的一个工厂Bean,所以接下来需要定义管理Scheduler的一些工具类及其比较重要的方法。

public class SchedulerUtil {
    @Autowired
    private SchedulerFactoryBean schedulerFactoryBean;
    public static final Class<JobExecutor>  DEFAULT_CLASS=JobExecutor.class;

    public Scheduler getScheduler(){
        return schedulerFactoryBean.getScheduler();
    }
    public void saveJob (TimerTaskPO timerTaskPO) throws Exception{
        JobKey key=new JobKey(timerTaskPO.getPk(),Scheduler.DEFAULT_GROUP);
        if (getScheduler().getJobDetail(key)!=null) {
            getScheduler().deleteJob(key);
        }
        if(timerTaskPO.getTimerItemList()!=null&&timerTaskPO.getTimerItemList().size()>0){
            JobDetail jobDetail=createJobDetail(timerTaskPO);
            CronTrigger trigger=createTrigger(timerTaskPO);
            getScheduler().scheduleJob(jobDetail, trigger);
        }   
    }
    public JobDetail createJobDetail(TimerTaskPO timerTaskPO) throws Exception{
        JobDetailImpl jobDetail=new JobDetailImpl();
        jobDetail.setName(timerTaskPO.getPk());
        jobDetail.setGroup(Scheduler.DEFAULT_GROUP);
        jobDetail.setJobClass(DEFAULT_CLASS);
        JobDataMap map=new JobDataMap();
        List<JobDataPO> list=createJobData(timerTaskPO);
        map.put("data", list);
        jobDetail.setJobDataMap(map);
        return jobDetail;
    }
    public  List<JobDataPO> createJobData(TimerTaskPO timerTaskPO) throws Exception{

    }
    public CronTrigger createTrigger(TimerTaskPO timerTaskPO) throws ParseException{
        CronTriggerImpl trigger = new CronTriggerImpl(timerTaskPO.getPk(),Scheduler.DEFAULT_GROUP);
        trigger.setCronExpression(timerTaskPO.getExpression());
        return trigger;
    }
        public void delJob(String name) throws SchedulerException{
        JobKey key=new JobKey(name,Scheduler.DEFAULT_GROUP);
        if (getScheduler().getJobDetail(key)!=null) {
            getScheduler().deleteJob(key);
        }
    }

    public void pauseJob(String name) throws SchedulerException{
        JobKey key=new JobKey(name,Scheduler.DEFAULT_GROUP);
        if (getScheduler().getJobDetail(key)!=null) {
            getScheduler().pauseJob(key);
        }
    }

    public void resumeJob(String name) throws SchedulerException{
        JobKey key=new JobKey(name,Scheduler.DEFAULT_GROUP);
        if (getScheduler().getJobDetail(key)!=null) {
            getScheduler().resumeJob(key);
        }
    }

    public void executeJob(String name) throws SchedulerException{
        JobKey key=new JobKey(name,Scheduler.DEFAULT_GROUP);
        if (getScheduler().getJobDetail(key)!=null) {
            getScheduler().triggerJob(key);
        }
    }

这里面包含了一些对定时任务的一些基本操作,比如:保存、暂停、恢复和立即执行一次。比较重要的就是createJobDetail方法。这个方法去构建定时任务所需要的所有数据,包括在定任务在执行期间所需要的所有数据,这些数据是需要序列化进入数据库的,我这边是在createJobData方法总创建的,而TimerTaskPO对象就是由用户创建的定时任务的对象。JobDataPO对象是定时任务执行的需要的所有数据。JobExecutor类是我定时任务的执行类:

public class JobExecutor implements StatefulJob{
    protected  final Logger logger = LoggerFactory.getLogger(JobExecutor.class);
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException{
        JobDataMap jobDataMap=context.getMergedJobDataMap();
        JobDataPO jobDataPO=null;
            List<JobDataPO> list= (List<JobDataPO>)jobDataMap.get("data");
        //这块就是业务逻辑了
    }

这边会把所有数据拿出来去执行定时任务。
到这边,quartz的架构已经搭建完成了,其余的可能就是和业务相关了,镶嵌进去即可。
3.适配多种数据库
这个时候需要看一下配置文件中的另外一个配置了:

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

这边配置的是数据库真正执行的代理类,一般情况下,是不需要修改的,我在使用的过程中,使用mysql以及oracle数据的时候,是没有任何问题的。但使用其余数据库就会报错。
postgres的报错信息如下:

Caused by: org.quartz.JobPersistenceException: Couldn't retrieve trigger: 不良的类型值 long : \x [See nested exception: org.postgresql.util.PSQLException: 不良的类型值 long : \x]

上网查询之后,quartz其实已经提供了十几种的代理类,使用哪种数据库就使用对应的代理类就行,我参与的项目支持用户使用五种数据库:mysql,oracle,hsql,postgres以及sqlserver。针对这种,我觉得有3种解决方案:
(1)用户使用哪种数据库,写一份简单说明,让用户手动去改。
(2)用户在初始化数据库的时候,由代码去判断用户使用的数据库类型,同时去修改quartz配置文件中的代理类。
(3)代码当中动态适配所有数据库,根据数据库类型采用相应的代理类,不修改配置文件。
方案1:最简单直接,如果是自己项目使用,完全ok。但如果是一件产品,大量用户使用的话,体验太差,不能采用。
方案2:体验好,用户也感觉不到。当时否定的原因在于:自己技术不行,担心用户在初始化之前quartz已经读取了配置文件,这样就会导致用户在第一次启动时候出现问题。最后请教了前辈,我们产品初始化数据库是第一个操作,spring注入在其后,所以,这样做是没有任何问题的。比较好的方案。
方案3:最麻烦也最困难的方案,需要对spring-quartz的初始化过程比较了解,但因为已经否定了方案2,所以自己就选择了这个,最终也实现了。

其实要是没有sqlserver的问题的话,方案3也挺简单的。
我去查看了代理类之间的不同,就我使用的五种数据库,他们的区别就只是读取二进制文件的方式不同而已,重写了两个方法而已。解决方案也很简单,自己写一个数据库代理类,在静态区去判断数据库类型。再读取二进制数据时,把quartz本身提供的读取方法搬过来,调用一下对应的即可。我自己的代理类如下:

public class SQLDelegate extends StdJDBCDelegate {
    public static String PgString="postgresql";
    public static String MsString="sqlserver";
    public static String HsString="hsqldb";
    public static boolean PgFlag=false;
    public static boolean MsFlag=false;
    public static boolean HsFlag=false;


    static{
        WebApplicationContext  applicationContext = null;
        //获取上下文
        DataSource dataSource=applicationContext.getBean(DataSource.class);
        String className=dataSource.getDriverClassName();
        PgFlag=className.contains(PgString);
        if (!PgFlag) {
            MsFlag=className.contains(MsString);
            if (!MsFlag) {
                HsFlag=className.contains(HsString);
            }
        }
    }

    @Override           
    protected Object getObjectFromBlob(ResultSet rs, String colName)
        throws ClassNotFoundException, IOException, SQLException {
        if (PgFlag) {
            return getObjectFromBlobByPg(rs, colName);
        }else if(MsFlag){
            return getObjectFromBlobByMS(rs, colName);
        }else if(HsFlag){
            return getObjectFromBlobByHs(rs, colName);
        }
        return super.getObjectFromBlob(rs, colName);
    }
    @Override           
    protected Object getJobDataFromBlob(ResultSet rs, String colName)
        throws ClassNotFoundException, IOException, SQLException {
        if (PgFlag) {
            return getJobDataFromBlobByPg(rs, colName);
        }else if(MsFlag){
            return getJobDataFromBlobByMS(rs, colName);
        }else if(HsFlag){
            return getJobDataFromBlobByHs(rs, colName);
        }
        return super.getJobDataFromBlob(rs, colName);
        }
        protected Object getObjectFromBlobByPg(ResultSet rs, String colName)
            throws ClassNotFoundException, IOException, SQLException {
            //pg的getObjectFromBlob方法
        }

    protected Object getJobDataFromBlobByPg(ResultSet rs, String colName)
        throws ClassNotFoundException, IOException, SQLException {
        //pg的getJobDataFromBlob方法
    }

    protected Object getObjectFromBlobByMS(ResultSet rs, String colName)
            throws ClassNotFoundException, IOException, SQLException {
           //SlqServer的getObjectFromBlob方法
        }

    protected Object getJobDataFromBlobByMS(ResultSet rs, String colName)
            throws ClassNotFoundException, IOException, SQLException {
           //SlqServer的getJobDataFromBlob方法
        }


    protected Object getObjectFromBlobByHs(ResultSet rs, String colName)
        throws ClassNotFoundException, IOException, SQLException {
        //hsql的getObjectFromBlob方法
    }

    protected Object getJobDataFromBlobByHs(ResultSet rs, String colName)
        throws ClassNotFoundException, IOException, SQLException {
        //hsql的getJobDataFromBlob方法
    }

思路也很简单,代码一目了然。上面注释的部分,源码中都可以找到的。
当初以为做这边就行了,但前面说了,sqlserver的问题远远不止如此,pg和hsq这样做了之后,发现问题解决了。但是sqlserver并没有。之后我把代理类换成了org.quartz.impl.jdbcjobstore.MSSQLDelegate,结果没有报错。显然这并不仅仅是那两个方法的问题。对了sqlserver的报错信息是:

Failure obtaining db row lock: 第 1 行: 只有 DECLARE CURSOR 才允许使用 FOR UPDATE 子句。

之后我根据错误堆栈,去找到了报错的地方,执行关于数据库锁的sql,在JobStoreCMT类的executeInLock方法中:

transOwner = getLockHandler().obtainLock(conn, lockName);

之后我比较了两种情况下不同sql语句的区别,果然不一样,就说明当采用sqlserver的代理类的时候,有个地方修改了这句话sql语句,然后我又去跟代码找到了那句sql语句的注入部分,一直找,最终找到了,一下子恍然大悟,在JobStoreSupport类的initialize方法中,看一下这边的代码:

 if (getLockHandler() == null) {

      // If the user hasn't specified an explicit lock handler, 
      // then we *must* use DB locks with clustering
      if (isClustered()) {
          setUseDBLocks(true);
      }

      if (getUseDBLocks()) {
          if(getDriverDelegateClass() != null && getDriverDelegateClass().equals(MSSQLDelegate.class.getName())) {
              if(getSelectWithLockSQL() == null) {
                  String msSqlDflt = "SELECT * FROM {0}LOCKS WITH (UPDLOCK,ROWLOCK) WHERE " + COL_SCHEDULER_NAME + " = {1} AND LOCK_NAME = ?";
                  getLog().info("Detected usage of MSSQLDelegate class - defaulting 'selectWithLockSQL' to '" + msSqlDflt + "'.");
                  setSelectWithLockSQL(msSqlDflt);
              }
          }
          getLog().info("Using db table-based data access locking (synchronization).");
          setLockHandler(new StdRowLockSemaphore(getTablePrefix(), getInstanceName(), getSelectWithLockSQL()));
      } else {
          getLog().info(
              "Using thread monitor-based data access locking (synchronization).");
          setLockHandler(new SimpleSemaphore());
      }
  }

如果锁处理器为空,就去初始化,然后判断是不是集群模式,如果是,设置使用数据库锁标志为ture,然后如果使用数据库锁,就使用StdRowLockSemaphore这个处理器,否则使用SimpleSemaphore的处理器。但最关键的地方在于,使用数据库锁的时候,它判断了使用的数据库代理类的名称,如果是MSSQLDelegate的话,就去修改注入新的sql语句,否则使用StdRowLockSemaphore的默认值。显然就是对于sqlserver做了特殊处理,如果对数据库了解比较深入的话,对这些特殊处理我想也很好理解。
所以,到这里了,问题找到了,那么我该怎么办呢???
想来想去,就只有一个办法,重写这块的逻辑代码,quartz判断的是代理类的名称,但我已经采用统一的数据库代理类了,所以我需要去判断数据库的驱动类型,如果是sqlserver的话,我就去得仿照quartz一样重新设置一下sql语句。但问题又来了,我该怎么去重写,这个是JobStoreSupport类,quartz的最基本的类。而且我是通过spring去连接quartz的,所以我需要重点从spring那边出发。
只要能够找到jobStoreSupport的最底层的子类,然后我再去继承重写就可以了,跟踪之后发现,spring使用的是LocalDataSourceJobStore这个类,是的,我需要重写这个类即可:

public class MyLocalDataSourceJobStore extends LocalDataSourceJobStore{
    @Override
    public void initialize(ClassLoadHelper loadHelper,
            SchedulerSignaler signaler) throws SchedulerConfigException {
        super.initialize(loadHelper,signaler);
        boolean flag;
        //这边写判断逻辑
        if (flag) {
            String msSqlDflt = "SELECT * FROM {0}LOCKS WITH (UPDLOCK,ROWLOCK) WHERE " + COL_SCHEDULER_NAME + " = {1} AND LOCK_NAME = ?";
            StdRowLockSemaphore semaphore =(StdRowLockSemaphore) getLockHandler();
            semaphore.setSelectWithLockSQL(msSqlDflt);
        }
    }
}

重写完了,问题又来了,我怎么才能让spring使用我的这个类呢?
SchedulerFactoryBean类在initSchedulerFactory方法中初始化了一些参数,包括”org.quartz.jobStore.class”类的设置,然后把这些参数交给StdSchedulerFactory实例去初始化。

    private void initSchedulerFactory(SchedulerFactory schedulerFactory) throws SchedulerException, IOException {
        //省略其余的
        if (this.dataSource != null) {
            mergedProps.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName());
        }

        //省略
        ((StdSchedulerFactory) schedulerFactory).initialize(mergedProps);
    }

显然我的思路就比较清晰了,我必须在它设置”org.quartz.jobStore.class”值之后,而在StdSchedulerFactory实例调用initialize方法之前把它设置的给覆盖了就行了。我先去重写StdSchedulerFactory类的initialize方法:

public class MyStdSchedulerFactory extends StdSchedulerFactory{
    @Override
    public void initialize(Properties props) throws SchedulerException {
        props.put(StdSchedulerFactory.PROP_JOB_STORE_CLASS, MyLocalDataSourceJobStore.class.getName());
        super.initialize(props);
    }

因为又重写了一个类,必须要让SchedulerFactoryBean使用MyStdSchedulerFactory的实例,再重写一下:

public class MySchedulerFactoryBean extends SchedulerFactoryBean{

    @Override
    public void afterPropertiesSet() throws Exception {
        setSchedulerFactoryClass(MyStdSchedulerFactory.class);
        super.afterPropertiesSet();
    }
}

重写了3个类,才完成了一个对sqlserver的兼容,感觉挺不划算的,当时至少用了一天时间才完成的吧。需要理清楚其中的调用顺序。
我重写了SchedulerFactoryBean,所以在配置文件中也需要用自己的类了,

    <!-- schedulerBean -->
    <bean id="chedulerFactoryBean"
        class="***.***.timer.MySchedulerFactoryBean">
        <!-- 省略 -->
    </bean>
</beans>

到此就结束了。使用quartz也比较容易,两个配置文件,一个工具类,兼容其余数据库的集体代理类,为了兼容sqlserver重写的3个类。自己在使用的过程当中,其实很大一部分时间花费了在了和项目业务相关的内容上面,quartz框架好搭,关键在于怎么把自己的业务结合起来,包括定时任务的监督以及记录日志等方面都需要考虑。

我这边quartz使用的是2.2.1版本。spring是4.3。

猜你喜欢

转载自blog.csdn.net/ywg_1994/article/details/80032529