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

spring-boot-2.0.3之quartz集成,最佳实践

程序员文章站 2022-11-10 22:30:15
前言 开心一刻 快过年了,大街上,爷爷在给孙子示范摔炮怎么放,嘴里还不停念叨:要像这样,用劲甩才能响。示范了一个,两个,三个... 孙子终于忍不住了,抱着爷爷的腿哭起来:爷呀,你给我剩个吧! 新的一年祝大家:健健康康,快快乐乐! github:https://github.com/youzhibin ......

前言

  开心一刻

    快过年了,大街上,爷爷在给孙子示范摔炮怎么放,嘴里还不停念叨:要像这样,用劲甩才能响。示范了一个,两个,三个... 孙子终于忍不住了,抱着爷爷的腿哭起来:爷呀,你给我剩个吧!

spring-boot-2.0.3之quartz集成,最佳实践

  新的一年祝大家:健健康康,快快乐乐!

  github:

  码云(gitee):

前情回顾与问题

   讲到了quartz的基本概念,以及springboot与quartz的集成;集成非常简单,引入相关依赖即可,此时我们job存储方式采用的是jdbc。

   讲到了quartz的数据源问题,如果我们没有@quartzdatasource修饰的数据源,那么默认情况下就是我们的工程数据源,springboot会将工程数据源设置给quartz;为什么需要数据源,因为我们的job不会空跑,往往会进行数据库的操作,那么就会用到数据库连接,而获取数据库连接最常用的的方式就是从数据源获取。

  后续使用过程中,发现了一些问题:

    1、spring注入,job到底能不能注入到spring容器,job中能不能自动注入我们的mapper(spring的autowired);

    2、job存储方式,到底用jdbc还是memory,最佳实践是什么

    3、调度失准,没有严格按照我们的cron配置进行

spring注入

  中我还分析的井井有条,并很自信的得出结论:job不能注入到spring,也不能享受spring的自动注入

spring-boot-2.0.3之quartz集成,最佳实践

  那时候采用的是从quartz数据源中获取connection,然后进行jdbc编程,发现jdbc用起来真的不舒服(不是说有问题,mybatis、spring jdbctemplate等底层也是jdbc),此时我就有了一个疑问:quartz job真的不能注入到spring、不能享受spring的自动注入吗? 结论可想而知:能!

spring-boot-2.0.3之quartz集成,最佳实践

打的真疼

  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,如下所示

spring-boot-2.0.3之quartz集成,最佳实践
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

spring-boot-2.0.3之quartz集成,最佳实践

  实现

    pom.xml

spring-boot-2.0.3之quartz集成,最佳实践
<?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.yml

spring-boot-2.0.3之quartz集成,最佳实践
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

spring-boot-2.0.3之quartz集成,最佳实践
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的,完整代码请看:

  job实例化过程源码解析

    还记得schedulerfactorybean的创建吗,可以看看,我们从schedulerfactorybean开始

    quartzschedulerthread线程的启动

      quartzschedulerthread声明如下

spring-boot-2.0.3之quartz集成,最佳实践view code

      负责触发quartzscheduler注册的triggers,可以理解成quartz的主线程(守护线程)。我们从schedulerfactorybean的afterpropertiesset()开始

spring-boot-2.0.3之quartz集成,最佳实践

      quartzschedulerthread继承了thread,通过defaultthreadexecutor的execute()启动了quartzschedulerthread线程

    jobfactory的创建与替换

spring-boot-2.0.3之quartz集成,最佳实践

      autowirecapablebeanjobfactory实例后续会赋值给quartz,作为quartz job的工厂,具体在哪赋值给quartz的了,我们往下看

spring-boot-2.0.3之quartz集成,最佳实践

      当quartz scheduler创建完成后,将scheduler的jobfactory替换成了autowirecapablebeanjobfactory。

    job的创建与执行

      quartzschedulerthread在上面已经启动了,autowirecapablebeanjobfactory也已经赋值给了scheduler;我们来看看quartzschedulerthread的run(),里面有job的创建与执行

spring-boot-2.0.3之quartz集成,最佳实践

      最终会调用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,通过名字我们也能猜到这两者之间的区别与优缺点

spring-boot-2.0.3之quartz集成,最佳实践

  上述两种jobstore对应到springboot就是:memory、jdbc

spring-boot-2.0.3之quartz集成,最佳实践
/*
 * 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 * * * ?,日志输入如下

spring-boot-2.0.3之quartz集成,最佳实践

  秒数会有不对,但这影响比较小,我们还能接受,可是时间间隔有时候却由1分钟变成2分钟,甚至3分钟,这个就有点接受不了。具体原因我还没有查明,个人觉得可能和数据库持久化有关。

  当存储方式是ramjobstore时,调度很准,还未发现调度失准的情况,cron表达式:3 */1 * * * ?,日志输入如下

spring-boot-2.0.3之quartz集成,最佳实践

总结

  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