Java定时任务框架:Elastic-job、Quartz、XXL-JOB 学习

Java定时任务:Elastic-job、Quartz 、XXL-JOB 学习

一、Elastic-job分布式定时任务框架学习

一、Simple类型的任务
快速开启一个simple任务的步骤

1.创建一个maven工程

2.导入elastic-job的依赖,使用的是最新版本2.1.5

    <dependency>
      <groupId>com.dangdang</groupId>
      <artifactId>elastic-job-lite-spring</artifactId>
      <version>2.1.5</version>
    </dependency>

3.创建一个类来实现SimpleJob,实现里面的execute()方法,这个方法中的代码是我们实际要进行的逻辑代码

//实现SimpleJob接口,实现里面的execute方法,这个方法里面的逻辑是实际去执行的任务
public class MySimpleJob implements SimpleJob {
    @Override
    public void execute(ShardingContext shardingContext) {
        LocalTime localTime=LocalTime.now();
      //这里是做了简单的测试,简单的打印了一下执行的时间以及当前这个服务拿到的分片,以及这个定时任务一共几个分片
        System.out.println(localTime+",当前的分片项是:"+shardingContext.getShardingItem()+",一共有"+shardingContext.getShardingTotalCount()+"个分片");
    }
}

4、在主启动类中配置一下关键信息

​ 1)、配置注册中心,elastic-job使用zookeeper注册中心,所以前提是你要自己先启动一个zookeeper注册中心,这里就不详细讲解如何启动一个zookeeper注册中心了

    //配置注册中心
    public static CoordinatorRegistryCenter zkCenter(){
        /**
         *     配置zookeeper的配置信息
         *     参数说明
         *     String serverLists:zookeeper的地址 String namespace:命名空间
         */
        
        ZookeeperConfiguration zkConfig = new ZookeeperConfiguration("xxx.xxx.xx.xxxx:2181", "simple-job");
        //创建zookeeper注册中心
        ZookeeperRegistryCenter zkCenter = new ZookeeperRegistryCenter(zkConfig);
        //一定记得初始化
        zkCenter.init();
        return zkCenter;
    }

​ 2)、配置任务job需要的配置

    /**
     * job配置
     *
     * @return
     */
    public static LiteJobConfiguration configuration() {
        /**
         * job核心配置
         * 参数说明
         * final String jobName:任务的名字, final String cron:定时任务什么时候启动的cron表达式, final int shardingTotalCount:将这个定时任务分成几片
         */
        JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("simple-job", "0/10 * * * * ?", 2).build();

        /**
         * job类型配置
         * 参数说明
         * JobCoreConfiguration coreConfig:job的核心配置, String jobClass:实现Simple的那个类的全类名
         */
        JobTypeConfiguration jobTypeConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, "com.lyq.job.MySimpleJob");

        //job根配置
        LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration)
                  .overwrite(true) //开启重新功能,意思就是我们更改了cron表达式后重启任务立即生效,不开启这个不会立即生效
                  .build();
        return liteJobConfiguration;
    }

​ 3)、在main函数中启动我们的定时任务

    public static void main(String[] args) {
        System.out.println("Hello World!");
        //通过JobScheduler启动定时任务,一定记得初始化
        new JobScheduler(zkCenter(), configuration()).init();
    }
二、DataFlow类型的任务
在这里我要对流式任务进行一个补充:Dataflow任务的执行流程是,当我们的时间到达我们设置的cron之后会触发去执行,会去执行抓取数据,然后再去执行处理数据,然后再去执行抓取数据,再去执行处理数据,形成了一个循环。
那么如果当我们在执行抓取数据和处理处理的时候到了我们设置的时间之后怎么办呢?
如果到了我们设置的定时时间后Dataflow发现任务没有处理完就不会去再次执行。
那它是如何知道任务没有完成的呢?
Dataflow使用维护了一个全局状态,这个状态中包括分片数,已经完成的分片数,每个分片的状态等信息。当到达我们设置的定时时间后去查看是否有未完成的分片,如果有未完成的分片那就不会执行,否则则会再次执行这个定时任务。
快速开启一个dataflow类型任务的步骤

​ 1)、创建一个类,实现Dataflow接口

public class MyDataflowJob implements DataflowJob<Order> {
    List<Order> orderList = new ArrayList<>();

    //为了模拟实际的业务,在这里创建100条订单记录
    {
        for (int i = 0; i < 100; i++) {
            Order order = new Order();
            order.setOrderId(i + 1);
            order.setOrderStatus(0);
            orderList.add(order);
        }
    }


    //抓取数据的逻辑
    @Override
    public List<Order> fetchData(ShardingContext shardingContext) {
        //抓取的规则是,只抓取未支付的订单,并且 orderId % 分片数量==当前的分片数, 每次抓取10个数据
        List<Order> list = orderList.stream().filter(orderItem -> orderItem.getOrderStatus() == 0)
                .filter(orderItem -> orderItem.getOrderId() % shardingContext.getShardingTotalCount() == shardingContext.getShardingItem())
                .collect(Collectors.toList());
        //分割成10个一组
        List<Order> subList = list.subList(0, 10);
        List<Order> res=new ArrayList<>();
        if(subList!=null && subList.size()>0){
            res.addAll(subList);
        }
        LocalTime time=LocalTime.now();
        System.out.println(time+",我是分片项:"+shardingContext.getShardingItem()+",我正在抓取数据...");
        System.out.println("抓取到的数据为:"+res);
        try {
            //每次抓取模拟处理三秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return res;
    }

    //处理数据的逻辑
    @Override
    public void processData(ShardingContext shardingContext, List<Order> data) {
        for (Order order : data) {
            order.setOrderStatus(1);
        }
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LocalTime time=LocalTime.now();
        System.out.println(time+",我是分片项:"+shardingContext.getShardingItem()+",我正在处理数据...");
    }
}

​ 2)、编写配置类

    public static LiteJobConfiguration DataflowConfiguration(){
        //job核心配置
        JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("DataflowJob", "0/5 * * * * ?", 2).build();
        /**
         * job类型配置
         * 参数说明
         * JobCoreConfiguration coreConfig:job核心配置, String jobClass:执行具体业务逻辑的代码的全类名,
         * boolean streamingProcess:是否开启流式处理,如果不开启抓取数据和处理数据只会执行一次和simple任务没有区别了,开启了之后会循环执行抓取数据和处理数据的逻辑
         */
        JobTypeConfiguration jobTypeConfiguration=new DataflowJobConfiguration(jobCoreConfiguration,"com.lyq.job.MyDataflowJob",true);
        //job根配置
        LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).overwrite(true).build();
        return liteJobConfiguration;
    }

其他的配置和之前差不多,稍微改动一点地方就可以了

3、启动类代码

    public static void main(String[] args) {
        System.out.println("Hello World!");
        //通过JobScheduler启动定时任务,一定记得初始化
        new JobScheduler(zkCenter(), DataflowConfiguration()).init();
    }
三、Script类型的任务
快速开启一个Script类型的任务的步骤

​ 1)、自己写一个脚本

​ 2)、编写配置类

    public static LiteJobConfiguration ScriptConfiguration(){
        //job核心配置
        JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder("ScriprJob", "0/10 * * * * ?", 2).build();
        //job类型配置
        JobTypeConfiguration jobTypeConfiguration=new ScriptJobConfiguration(jobCoreConfiguration,"脚本的位置");
        //job根配置
        LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).build();
        return liteJobConfiguration;
    }
四、Spring整合Simple任务
spring整合Simple

​ 1)、创建一个maven工程

​ 2)、创建一个spring-config.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:reg="http://www.dangdang.com/schema/ddframe/reg"
       xmlns:job="http://www.dangdang.com/schema/ddframe/job"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.dangdang.com/schema/ddframe/reg
       http://www.dangdang.com/schema/ddframe/reg/reg.xsd
       http://www.dangdang.com/schema/ddframe/job
       http://www.dangdang.com/schema/ddframe/job/job.xsd">
    <!--        zookeeper注册中心配置-->
    <reg:zookeeper id="zkCenter" server-lists="112.126.80.104:2181" namespace="spring-simpleJob"/>

    <!--    job配置-->
    <job:simple id="simplejob" registry-center-ref="zkCenter" cron="0/5 * * * * ?" sharding-total-count="2"
                class="com.lyq.job.MySimpleJob" overwrite="true"/>
</beans>

​ 3)、修改web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>spring-elasticJob</display-name>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:spring-config.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
</web-app>

​ 4)、添加maven,截取了部分

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <spring.version>5.1.5.RELEASE</spring.version>
        <elasticjob.version>2.1.5</elasticjob.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>com.dangdang</groupId>
            <artifactId>elastic-job-lite-core</artifactId>
            <version>${elasticjob.version}</version>
        </dependency>
        <dependency>
            <groupId>com.dangdang</groupId>
            <artifactId>elastic-job-lite-spring</artifactId>
            <version>${elasticjob.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.6</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.26</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>spring-elasticjob</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <executions>
                    <execution>
                        <id>tomcat-run</id>
                        <goals>
                            <goal>exec-war-only</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <path>/</path>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

​ 5)、创建定时任务逻辑

@Slf4j
public class MySimpleJob implements SimpleJob {
    @Override
    public void execute(ShardingContext shardingContext) {
        LocalTime time = LocalTime.now();
        System.out.println(time + "我的分片项是:" + shardingContext.getShardingItem());
    }
}
五、Spring boot 整合elastic-job

因为官方并没有提供elastic-job的springboot的starter,所有我们需要手动写一个starter

手写一个elastic-job 的Spring boot的starer 的步骤

​ 1)、首先创建一个包,这个包一定不要和主启动类在一个包下

​ 2)、包中创建属性类

//对应于application.properties中的属性的前缀是什么
@ConfigurationProperties(prefix = "elasticjob.zookeeper")
@Getter@Setter
public class ZookeeperProperties {
    //zookeeper的服务器地址
    private String serverLists;
    //zookeeper的命名空间
    private String namespace;
}

​ 3)、创建自动配置类

@Configuration
//条件注解,只有当我们的配置文件中有下面的属性的时候才会执行这个类
@ConditionalOnProperty("elasticjob.zookeeper.server-lists")
//将这个类和对应的属性类进行一个绑定
@EnableConfigurationProperties(ZookeeperProperties.class)
public class ZookeeperAutoConfig {
    @Autowired
    private ZookeeperProperties zookeeperProperties;

    @Bean(initMethod = "init")
    public CoordinatorRegistryCenter zkCenter(){
        ZookeeperConfiguration zookeeperConfiguration = new ZookeeperConfiguration(zookeeperProperties.getServerLists(), zookeeperProperties.getNamespace());
        ZookeeperRegistryCenter center = new ZookeeperRegistryCenter(zookeeperConfiguration);
        return center;
    }
}

​ 4)、在resources目录下创建一个META-INF文件夹

​ 5)、在META-INF文件夹下创建一个spring.factories文件

#配置自动配置类的信息
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lyq.autoConfig.ZookeeperAutoConfig

​ 6)、在application.properties配置文件中配置属性就可以了

elasticjob.zookeeper.server-lists=xxx.xxx.xxx.xxx:2181
elasticjob.zookeeper.namespace=springboot-elasticJob

​ 7)、写一个注解,用这个注解中的属性来注册simple任务

//自定义的Simple任务注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ElasticJobSimpleJob {
    //下面的这些都是注解里面的属性
    String jobName() default "";
    String cron() default "";
    int shardingTotalCount() default 1;
    boolean overwrite() default false;
}

​ 8)、创建一个实现simple接口的类

@Slf4j
@ElasticJobSimpleJob(jobName = "simpleJob",cron = "0/5 * * * * ?",shardingTotalCount = 2,overwrite = true)
public class MySimpleJob implements SimpleJob {
    @Override
    public void execute(ShardingContext shardingContext) {
      log.info("我是分片项:"+shardingContext.getShardingItem()+",总的分片项为:"+shardingContext.getShardingTotalCount());
    }
}

​ 9)、创建针对ElasticJobSimpleJob注解的自动配置类

//注意因为此时的这个配置类的位置不和主启动在一个包下,所以即使添加了@Configuration注解,也不会扫描到,我们要把这个类配置到spring.factories中
@Configuration
//当注册中心一加载知乎就来执行这个自动配置类
@ConditionalOnBean(CoordinatorRegistryCenter.class)
//保证在注册中心成功加载之后这个类才进行一个加载
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class SimpleJobAutoConfig {
    //注入上下文
    @Autowired
    private ApplicationContext applicationContext;
    //注入注册中心
    @Autowired
    private CoordinatorRegistryCenter zkCenter;

    @PostConstruct
    public void initSimple(){
        //获取使用了@ElasticJobSimpleJob注解的实例
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobSimpleJob.class);
        //遍历所有的实例
        for (Map.Entry<String, Object> entry:beans.entrySet()){
            Object instance = entry.getValue();
            //通过反射获得每个实例的接口,必须要实现了Simple接口才可以,主要是保险
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                //判断这个接口是不是Simple接口
                if(superInterface== SimpleJob.class){
                    //获取注解中的信息
                    ElasticJobSimpleJob annotation = instance.getClass().getAnnotation(ElasticJobSimpleJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    //将simple任务注册到zookeeper中

                    //job核心配置
                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    //job类型配置
                    JobTypeConfiguration jobTypeConfiguration=new SimpleJobConfiguration(jobCoreConfiguration,instance.getClass().getCanonicalName());
                    //job根配置
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).overwrite(overwrite).build();
                    new JobScheduler(zkCenter,liteJobConfiguration).init();
                }
            }
        }
    }
}

​ 10)、spring.factories中需要添加一些配置信息

#配置自动配置类的信息
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lyq.autoConfig.ZookeeperAutoConfig,\
com.lyq.autoConfig.SimpleJobAutoConfig

​ 11)、启动测试

spring boot整合elastic-job 的Dataflow任务

基本的流程和上面整合Simple任务差不多,主要记录一下不一样的地方

​ 1)、创建一个用于Dataflow任务的注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ElasticJobDataflowJob {
    //下面的这些都是注解里面的属性
    String jobName() default "";
    String cron() default "";
    int shardingTotalCount() default 1;
    boolean overwrite() default false;
    boolean streamingProcess() default false;
}

​ 2)、创建一个类来实现Dataflow接口

@Slf4j
@ElasticJobDataflowJob(jobName = "DataflowJob",cron = "0/5 * * * * ?",shardingTotalCount = 2,overwrite = true,streamingProcess = true)
public class MyDataflowJob implements DataflowJob<Integer> {
    List<Integer>  orders=new ArrayList<>();
    {
        for (int i = 0; i < 10; i++) {
            orders.add(i+1);
        }
    }
    @Override
    public List<Integer> fetchData(ShardingContext shardingContext) {
        //抓取数据的逻辑
        List<Integer> list=new ArrayList<>();
        for (Integer order : orders) {
            if(order%shardingContext.getShardingTotalCount()==shardingContext.getShardingItem()){
                list.add(order);
                break;
            }
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("我是分片项:"+shardingContext.getShardingItem()+",我抓取到的数据为:"+list);
        return list;
    }

    @Override
    public void processData(ShardingContext shardingContext, List<Integer> data) {
        //处理数据的逻辑
       orders.removeAll(data);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("我是分片项:"+shardingContext.getShardingItem()+",我正在处理的数据是"+data);
    }
}

​ 3)、配置上面注解的自动配置类

@Configuration
@ConditionalOnBean(CoordinatorRegistryCenter.class)
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class DataflowJobAutoConfig {
    @Autowired
    private ApplicationContext applicationContext;
    @Resource
    private CoordinatorRegistryCenter zkCenter;

    @PostConstruct
    public void dataFlowJobInit(){
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobDataflowJob.class);
        for (Map.Entry<String, Object> bean:beans.entrySet()){
            Object instance = bean.getValue();
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                if(superInterface== DataflowJob.class){
                    ElasticJobDataflowJob annotation = instance.getClass().getAnnotation(ElasticJobDataflowJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    boolean streamingProcess = annotation.streamingProcess();

                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    JobTypeConfiguration jobTypeConfiguration=new DataflowJobConfiguration(jobCoreConfiguration,instance.getClass().getCanonicalName(),streamingProcess);
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).overwrite(overwrite).build();

                    new JobScheduler(zkCenter,liteJobConfiguration).init();
                }
            }
        }
    }
}

​ 4)、一定记得将上面这个自动配置类,添加到spring.factories中,因为组件扫描的时候扫不到这个包

#配置自动配置类的信息
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lyq.autoConfig.ZookeeperAutoConfig,\
com.lyq.autoConfig.SimpleJobAutoConfig,\
com.lyq.autoConfig.DataflowJobAutoConfig
六、实际业务场景,定时关单实操
案例1:我们模拟一个定时关单的功能,每次扫描之前没有支付的订单,把这些订单给取消了。

​ 1)、创建一个数据库名字叫做elasticjob

​ 2)、创建表

DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `amount` decimal(10, 2) NOT NULL COMMENT '订单金额',
  `status` int NOT NULL DEFAULT 0 COMMENT '订单状态 0:未支付 1:已支付 2:已取消',
  `receive_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '收件人姓名',
  `receive_address` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '收件人地址',
  `create_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '创建人姓名',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '修改人姓名',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

​ 3)、在之前的spring boot项目中添加如下的maven依赖

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.32</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>com.dangdang</groupId>
            <artifactId>elastic-job-lite-spring</artifactId>
            <version>2.1.5</version>
        </dependency>

		 <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.2</version>
                <dependencies>
                    <dependency>
                        <groupId>com.mysql</groupId>
                        <artifactId>mysql-connector-j</artifactId>
                        <version>8.0.32</version>
                    </dependency>
                </dependencies>
            </plugin>

​ 4)、使用mybatis generator插件来快速生成一个逆向工程

​ 5)、在resources目录下创建一个文件generatorConfig.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <context id="MysqlTables" targetRuntime="MyBatis3">
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/elasticjob?serverTimezone=UTC&amp;useSSL=false"
                        userId="root"
                        password="123456">
            <property name="nullCatalogMeansCurrent" value="true" />
        </jdbcConnection>

        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <javaModelGenerator targetPackage="com.lyq.springbootelasticjob.entity" targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <sqlMapGenerator targetPackage="mapper"  targetProject="src\main\resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.lyq.springbootelasticjob.mapper"  targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <table schema="elasticjob" tableName="t_order" domainObjectName="Order" >
        </table>

    </context>
</generatorConfiguration>

​ 6)、根据里面的实体类、mapper、xml文件生成位置自己在自己的项目中创建好对应的包和文件夹

​ 7)、创建service 和对应的impl类,这里就省略了,因为很简单

​ 8)、创建一个Simple类型的定时任务,每隔五秒去创建10条订单

@Slf4j
@ElasticJobSimpleJob(jobName = "createOrderSimpleJob",cron = "0/5 * * * * ?",shardingTotalCount = 1,overwrite = true)
public class MySimpleJob implements SimpleJob {
    @Autowired
    private OrderService orderService;
    @Override
    public void execute(ShardingContext shardingContext) {
//      log.info("我是分片项:"+shardingContext.getShardingItem()+",总的分片项为:"+shardingContext.getShardingTotalCount());

        //定时五秒钟创建10个订单,让一个分片项来处理就好了
        for (int i = 0; i < 10; i++) {
            Order order = new Order();
            order.setAmount(new BigDecimal(100));
            order.setStatus(0);
            order.setReceiveName("王五"+(i+1));
            order.setReceiveAddress("北京市海淀区");
            order.setCreateName("EyeDrop");
            order.setCreateTime(new Date());
            order.setUpdateName("EyeDrop");
            order.setUpdateTime(new Date());
            orderService.insertOrder(order);
        }
    }
}

​ 9)、修改一下之前创建的自动配置类

//注意因为此时的这个配置类的位置不和主启动在一个包下,所以即使添加了@Configuration注解,也不会扫描到,我们要把这个类配置到spring.factories中
@Configuration
//当注册中心一加载知乎就来执行这个自动配置类
@ConditionalOnBean(CoordinatorRegistryCenter.class)
//保证在注册中心成功加载之后这个类才进行一个加载
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class SimpleJobAutoConfig {
    //注入上下文
    @Autowired
    private ApplicationContext applicationContext;
    //注入注册中心
    @Resource
    private CoordinatorRegistryCenter zkCenter;

    @PostConstruct
    public void initSimple(){
        //获取使用了@ElasticJobSimpleJob注解的实例
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobSimpleJob.class);
        //遍历所有的实例
        for (Map.Entry<String, Object> entry:beans.entrySet()){
            Object instance = entry.getValue();
            //通过反射获得每个实例的接口,必须要实现了Simple接口才可以,主要是保险
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                //判断这个接口是不是Simple接口
                if(superInterface== SimpleJob.class){
                    //获取注解中的信息
                    ElasticJobSimpleJob annotation = instance.getClass().getAnnotation(ElasticJobSimpleJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    //将simple任务注册到zookeeper中

                    //job核心配置
                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    //job类型配置
                    JobTypeConfiguration jobTypeConfiguration=new SimpleJobConfiguration(jobCoreConfiguration,instance.getClass().getCanonicalName());
                    //job根配置
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).overwrite(overwrite).build();
                    //这里使用了SpringJobScheduler而不是之前的JobScheduler了
                    new SpringJobScheduler((ElasticJob) instance,zkCenter,liteJobConfiguration).init();
                }
            }
        }
    }
}
案例2:模拟商家定时从多个第三方平台将自己商铺的订单抓取过来

​ 1)、创建对应的表


DROP TABLE IF EXISTS `jd_order`;
CREATE TABLE `jd_order`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `status` int NOT NULL DEFAULT 0 COMMENT '0:未抓取 1:已抓取',
  `amount` decimal(10, 2) NOT NULL COMMENT '订单金额',
  `create_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `tcat_order`;
CREATE TABLE `tcat_order`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `order_status` int NOT NULL DEFAULT 0 COMMENT '0:未抓取 1:已抓取',
  `money` decimal(10, 2) NOT NULL COMMENT '订单金额',
  `create_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

DROP TABLE IF EXISTS `all_order`;
CREATE TABLE `all_order`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `third_order_id` int NOT NULL COMMENT '第三方订单的id',
  `type` int NOT NULL COMMENT '0:京东 1:天猫',
  `total_amount` decimal(10, 2) NOT NULL COMMENT '订单金额',
  `create_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

​ 2)、通过mybatis generator自动生成对应的mapper、ebtity、xml文件,这里就省略了,和之前的步骤一样

​ 3)、创建一个Simple类型的定时任务来模拟定时生产订单的功能,每五秒钟生产五个订单

@ElasticJobSimpleJob(jobName = "createOrderJob",cron = "0/5 * * * * ?",shardingTotalCount = 1,overwrite = true)
public class ThirdOrderCreateJob implements SimpleJob {
    @Autowired
    private OrderService orderService;
    @Override
    public void execute(ShardingContext shardingContext) {
        orderService.createOrder();
    }
}

下面是对应的createOrder()的实现


    @Override
    public void createOrder() {
        //每五秒钟创建五个订单,这五个订单是随机的天猫和京东的订单,相当于一秒钟创建一个订单了
        for (int i = 0; i < 5; i++) {
            Random random = new Random();
            int index = random.nextInt(2);
            if (index == 1) {
                //创建京东订单
                log.info("创建京东订单");
                JdOrder jdOrder = new JdOrder();
                jdOrder.setStatus(0);
                jdOrder.setAmount(new BigDecimal(100));
                jdOrder.setCreateName("JdSystem");
                jdOrder.setCreateTime(new Date());
                jdOrder.setUpdateName("JdSystem");
                jdOrder.setUpdateTime(new Date());
                jdOrderMapper.insertSelective(jdOrder);
            } else {
                //创建天猫订单
                log.info("创建天猫订单");
                TcatOrder tcatOrder = new TcatOrder();
                tcatOrder.setOrderStatus(0);
                tcatOrder.setMoney(new BigDecimal(100));
                tcatOrder.setCreateName("TcatSystem");
                tcatOrder.setCreateTime(new Date());
                tcatOrder.setUpdateName("TcatSystem");
                tcatOrder.setUpdateTime(new Date());
                tcatOrderMapper.insertSelective(tcatOrder);
            }
        }
    }

​ 4)、使用Dataflow类型的任务来抓取京东和天猫平台的没有被抓取的订单,因为模拟的是订单一直产生,所以使用Dataflow类型的任务可以一直进行抓取和处理。抓取的处理主要包括两大步:1.将抓取到的订单插入到自己的订单表中 2、更新对应的订单的状态为已抓取

@Slf4j
//@ElasticJobDataflowJob(jobName = "thirdOrderDataflowJob", cron = "0/5 * * * * ?", shardingTotalCount = 2, overwrite = true, streamingProcess = true)
public class ThirdOrderDataflowJob implements DataflowJob<Object> {
    @Resource
    private JdOrderMapper jdOrderMapper;
    @Resource
    private TcatOrderMapper tcatOrderMapper;
    @Resource
    private OrderService orderService;

    @Override
    public List<Object> fetchData(ShardingContext shardingContext) {
        if (shardingContext.getShardingItem() == 0) {
            //抓取京东中没有被抓取的订单
            List<JdOrder> jdOrders = jdOrderMapper.getNoFetchOrders(5);
            if (jdOrders.size() > 0 && !jdOrders.isEmpty()) {
                List<Object> jdOrderList = jdOrders.stream().map(item -> ((Object) item)).collect(Collectors.toList());
                return jdOrderList;
            }
        }else{
            //抓取天猫中没有被抓取的订单
            List<TcatOrder> tcatOrders = tcatOrderMapper.getNotFetchOrders(5);
            if (!tcatOrders.isEmpty() && tcatOrders.size() > 0) {
                List<Object> tcatOrderList = tcatOrders.stream().map(item -> ((Object) item)).collect(Collectors.toList());
                return tcatOrderList;
            }
        }
        return null;
    }

    @Override
    public void processData(ShardingContext shardingContext, List<Object> data) {
        if (shardingContext.getShardingItem() == 0) {
            //处理京东订单
            log.info("处理京东订单");
            List<JdOrder> jdOrders = data.stream().map(item -> ((JdOrder) item)).collect(Collectors.toList());
            if (!jdOrders.isEmpty() && jdOrders.size() > 0) {
                for (JdOrder jdOrder : jdOrders) {
                    AllOrder allOrder = new AllOrder();
                    allOrder.setThirdOrderId(jdOrder.getId());
                    allOrder.setType(0);
                    allOrder.setTotalAmount(jdOrder.getAmount());
                    allOrder.setCreateName("System");
                    allOrder.setCreateTime(new Date());
                    allOrder.setUpdateName("System");
                    allOrder.setUpdateTime(new Date());
                    orderService.processJdOrder(allOrder);
                }
            }
        } else {
            //处理天猫订单
            log.info("处理天猫订单");
            List<TcatOrder> tcatOrders = data.stream().map(item -> ((TcatOrder) item)).collect(Collectors.toList());
            if (!tcatOrders.isEmpty() && tcatOrders.size() > 0) {
                for (TcatOrder tcatOrder : tcatOrders) {
                    AllOrder allOrder = new AllOrder();
                    allOrder.setThirdOrderId(tcatOrder.getId());
                    allOrder.setType(1);
                    allOrder.setTotalAmount(tcatOrder.getMoney());
                    allOrder.setCreateName("System");
                    allOrder.setCreateTime(new Date());
                    allOrder.setUpdateName("System");
                    allOrder.setUpdateTime(new Date());
                    orderService.processTcatOrder(allOrder);
                }
            }
        }
    }
}

​ 5)、注意,这里需要将我们之前的Dataflow类型的任务的自动配置类进行更改,如果不更改会出现错误。(我在这里卡了2个小时)

@Configuration
@ConditionalOnBean(CoordinatorRegistryCenter.class)
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class DataflowJobAutoConfig {
    @Autowired
    private ApplicationContext applicationContext;
    @Resource
    private CoordinatorRegistryCenter zkCenter;

    @PostConstruct
    public void dataFlowJobInit(){
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobDataflowJob.class);
        for (Map.Entry<String, Object> bean:beans.entrySet()){
            Object instance = bean.getValue();
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                if(superInterface== DataflowJob.class){
                    ElasticJobDataflowJob annotation = instance.getClass().getAnnotation(ElasticJobDataflowJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    boolean streamingProcess = annotation.streamingProcess();

                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    JobTypeConfiguration jobTypeConfiguration=new DataflowJobConfiguration(jobCoreConfiguration,instance.getClass().getCanonicalName(),streamingProcess);
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).overwrite(overwrite).build();
					//主要就是改这里,一定改成SpringJobScheduler
                    new SpringJobScheduler((ElasticJob) instance,zkCenter,liteJobConfiguration).init();
                }
            }
        }
    }
}
七、Elastic-job的高级玩法:自定义分片策略、事件追踪、作业监听器
一、自定义分片策略

Elastic-job默认的分片策略有三种,我来介绍一下

第一种:AverageAllocationJobShardingStrategy:基于平均分配算法的分片策略

举个栗子:如果有一个作业分成了9份,有三台服务器,那么这三台服务器的分配如下 第一台[0,1,2] 第二台[3,4,5] 第三台[6,7,8]

如果分成了10份,有三台服务器的话,会将多出来的先分给小的,第一台[0,1,2,9] 第二台[3,4,5] 第三台[6,7,8]

第二种:OdevitySortByNameJobShardingStrategy:根据作业名的哈希值的奇偶数决定ip升降序算法的分片策略

作业名的哈希值如果是奇数的话,按照ip升序。如果作业名的哈希值是偶数的话,按照ip降序。

举个栗子:如果有三台服务器,有两个分片,如果作业名的哈希值为奇数,那么分片是这样的第一台[0] 第二台[1] 第三台[]

如果作业名的哈希值是偶数的话,分片是这样的:第一台[] 第二台[1] 第三台[0]

第三种:RotateServerByNameJobShardingStrategy:根据作业名的哈希值对服务器进行轮转的分片策略

服务器构成一个环形,根据作业名的哈希值来确定第一台服务器,然后在这个环中轮转分配分片

下面说一下我们自定义分片策略的步骤,这里我选择的是轮询的分片策略,每个服务器依次拿走一个分片直到分片没有了为止

​ 1)、创建自定义分片策略类,需要实现JobShardingStrategy接口

public class MyShardingStrategy implements JobShardingStrategy {
    @Override
    public Map<JobInstance, List<Integer>> sharding(List<JobInstance> jobInstances, String jobName, int shardingTotalCount) {
        Map<JobInstance,List<Integer>> res=new HashMap<>();
        ArrayDeque<Integer> queue=new ArrayDeque<>();//创建一个队列来存放分片项
        for (int i = 0; i < shardingTotalCount; i++) {
            queue.add(i);//将分片项加入到队列中去
        }
        while(!queue.isEmpty()){
            //如果队列中海尔有分片项的话就要取出来,直到没有为止
            for (JobInstance jobInstance : jobInstances) {
                if(!queue.isEmpty()){
                    Integer shardingItem = queue.pop();//取出一个分片项
                    List<Integer> integers = res.get(jobInstance);//得到当前实例的所有分片项
                    if(integers!=null && integers.size()>0){
                        integers.add(shardingItem);//如果分片项列表不为空的话就将这个分片项加入到这个列表中去
                    }else{
                        //如果分片项列表为空的话,将这个分片项加入到一个列表,然后再将这个列表放到这个map中去
                        List<Integer> shardingItems=new ArrayList<>();
                        shardingItems.add(shardingItem);
                        res.put(jobInstance,shardingItems);
                    }
                }
            }
        }
        return res;
    }
    //这里我们自定义的分片策略是轮询,比如有10个分片项,2个服务,这两个服务轮流从分片项中去一个 比如A取0 B取1 A取2 B取3.。。。

}

​ 2)、在我们的注解类里面,我们应该添加上我们使用的分片策略,这样会让我们自己写的注解更加健壮

//自定义的Simple任务注解,Dataflow类型的一样的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ElasticJobSimpleJob {
    //下面的这些都是注解里面的属性
    String jobName() default "";
    String cron() default "";
    int shardingTotalCount() default 1;
    boolean overwrite() default false;
    Class<? extends JobShardingStrategy> jobShardingStrategyClass() default AverageAllocationJobShardingStrategy.class;
}

​ 3)、然后还需要更新一下我么的任务的自动配置类

//注意因为此时的这个配置类的位置不和主启动在一个包下,所以即使添加了@Configuration注解,也不会扫描到,我们要把这个类配置到spring.factories中
@Configuration
//当注册中心一加载知乎就来执行这个自动配置类
@ConditionalOnBean(CoordinatorRegistryCenter.class)
//保证在注册中心成功加载之后这个类才进行一个加载
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class SimpleJobAutoConfig {
    //注入上下文
    @Autowired
    private ApplicationContext applicationContext;
    //注入注册中心
    @Resource
    private CoordinatorRegistryCenter zkCenter;

    @PostConstruct
    public void initSimple(){
        //获取使用了@ElasticJobSimpleJob注解的实例
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobSimpleJob.class);
        //遍历所有的实例
        for (Map.Entry<String, Object> entry:beans.entrySet()){
            Object instance = entry.getValue();
            //通过反射获得每个实例的接口,必须要实现了Simple接口才可以,主要是保险
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                //判断这个接口是不是Simple接口
                if(superInterface== SimpleJob.class){
                    //获取注解中的信息
                    ElasticJobSimpleJob annotation = instance.getClass().getAnnotation(ElasticJobSimpleJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                          //获得注解中的分片策略属性
                    Class<? extends JobShardingStrategy> jobShardingStrategyClass = annotation.jobShardingStrategyClass();
                    //将simple任务注册到zookeeper中

                    //job核心配置
                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    //job类型配置
                    JobTypeConfiguration jobTypeConfiguration=new SimpleJobConfiguration(jobCoreConfiguration,instance.getClass().getCanonicalName());
                    //job根配置
   LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).jobShardingStrategyClass(jobShardingStrategyClass.getCanonicalName()).overwrite(overwrite).build();
                  
                    new SpringJobScheduler((ElasticJob) instance,zkCenter,liteJobConfiguration).init();
                }
            }
        }
    }
}
@Configuration
@ConditionalOnBean(CoordinatorRegistryCenter.class)
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class DataflowJobAutoConfig {
    @Autowired
    private ApplicationContext applicationContext;
    @Resource
    private CoordinatorRegistryCenter zkCenter;

    @PostConstruct
    public void dataFlowJobInit(){
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobDataflowJob.class);
        for (Map.Entry<String, Object> bean:beans.entrySet()){
            Object instance = bean.getValue();
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                if(superInterface== DataflowJob.class){
                    ElasticJobDataflowJob annotation = instance.getClass().getAnnotation(ElasticJobDataflowJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    boolean streamingProcess = annotation.streamingProcess();
                      //获得注解中的分片策略属性
                    Class<? extends JobShardingStrategy> jobShardingStrategyClass = annotation.jobShardingStrategyClass();

                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    JobTypeConfiguration jobTypeConfiguration=new DataflowJobConfiguration(jobCoreConfiguration,instance.getClass().getCanonicalName(),streamingProcess);
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).jobShardingStrategyClass(jobShardingStrategyClass.getCanonicalName()).overwrite(overwrite).build();
					
                    new SpringJobScheduler((ElasticJob) instance,zkCenter,liteJobConfiguration).init();
                }
            }
        }
    }
}
二、开启事件追踪

开启事件追踪后,会将我们的每个任务都生成对应的两个数据表中的记录,一个表是job_execution_log,另一个表是job_status_trace_log

开始事件追踪也很简单,只需要配置一个数据源就可以了,它会自动在数据库中创建这两个表,并将对应的事件添加到这个表中

下面来说一下步骤

​ 1)、为了我们结构的严谨,我们应该在自己写的两个注解中添加是否开启事件追踪功能,如果开启了我们才配置事件追踪,如果没有哦开启的话我们是不需要进行开启事件追踪的

//自定义的Simple任务注解,Dataflow类型的任务是一样的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ElasticJobSimpleJob {
    //下面的这些都是注解里面的属性
    String jobName() default "";
    String cron() default "";
    int shardingTotalCount() default 1;
    boolean overwrite() default false;
    Class<? extends JobShardingStrategy> jobShardingStrategyClass() default AverageAllocationJobShardingStrategy.class;
    //是否开启事件追踪功能
    boolean isStartJobEvent() default false;
}

​ 2)、在Simple类型任务的自动配置类和Dataflow类型任务的自动配置类中配置数据源

//注意因为此时的这个配置类的位置不和主启动在一个包下,所以即使添加了@Configuration注解,也不会扫描到,我们要把这个类配置到spring.factories中
@Configuration
//当注册中心一加载知乎就来执行这个自动配置类
@ConditionalOnBean(CoordinatorRegistryCenter.class)
//保证在注册中心成功加载之后这个类才进行一个加载
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class SimpleJobAutoConfig {
    //注入上下文
    @Autowired
    private ApplicationContext applicationContext;
    //注入注册中心
    @Resource
    private CoordinatorRegistryCenter zkCenter;
    //注入数据源信息
    @Autowired
    private DataSource dataSource;

    @PostConstruct
    public void initSimple() throws IllegalAccessException, InstantiationException {
        //获取使用了@ElasticJobSimpleJob注解的实例
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobSimpleJob.class);
        //遍历所有的实例
        for (Map.Entry<String, Object> entry : beans.entrySet()) {
            Object instance = entry.getValue();
            //通过反射获得每个实例的接口,必须要实现了Simple接口才可以,主要是保险
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                //判断这个接口是不是Simple接口
                if (superInterface == SimpleJob.class) {
                    //获取注解中的信息
                    ElasticJobSimpleJob annotation = instance.getClass().getAnnotation(ElasticJobSimpleJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    //获得注解中的分片策略属性
                    Class<? extends JobShardingStrategy> jobShardingStrategyClass = annotation.jobShardingStrategyClass();
                    //是否开启事件追踪功能
                    boolean isStartJobEvent = annotation.isStartJobEvent();
                    //将simple任务注册到zookeeper中

                    //job核心配置
                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    //job类型配置
                    JobTypeConfiguration jobTypeConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, instance.getClass().getCanonicalName());

                    //开始事件追踪功能,需要配置数据源信息
                    JobEventConfiguration jobEventConfiguration = new JobEventRdbConfiguration(dataSource);

                    //job根配置
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).jobShardingStrategyClass(jobShardingStrategyClass.getCanonicalName()).overwrite(overwrite).build();
                    if (isStartJobEvent) {
                      
                       new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobEventConfiguration).init();
                        } 
                   new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration).init();
                    }
                }
            }
        }
    }
}
@Configuration
@ConditionalOnBean(CoordinatorRegistryCenter.class)
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class DataflowJobAutoConfig {
    @Autowired
    private ApplicationContext applicationContext;
    @Resource
    private CoordinatorRegistryCenter zkCenter;
    @Autowired
    private DataSource dataSource;

    @PostConstruct
    public void dataFlowJobInit() throws IllegalAccessException, InstantiationException {
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobDataflowJob.class);
        for (Map.Entry<String, Object> bean : beans.entrySet()) {
            Object instance = bean.getValue();
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                if (superInterface == DataflowJob.class) {
                    ElasticJobDataflowJob annotation = instance.getClass().getAnnotation(ElasticJobDataflowJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    boolean streamingProcess = annotation.streamingProcess();
                    Class<? extends JobShardingStrategy> jobShardingStrategyClass = annotation.jobShardingStrategyClass();
                    boolean isStartJobEvent = annotation.isStartJobEvent();
                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    JobTypeConfiguration jobTypeConfiguration = new DataflowJobConfiguration(jobCoreConfiguration, instance.getClass().getCanonicalName(), streamingProcess);
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).jobShardingStrategyClass(jobShardingStrategyClass.getCanonicalName()).overwrite(overwrite).build();
                    JobEventConfiguration jobEventConfiguration = new JobEventRdbConfiguration(dataSource);
                    if (isStartJobEvent) {
                       
                     new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobEventConfiguration).init();
                        
                    } else {
                        new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobEventConfiguration).init(); 
                    }
                }
            }
        }
    }
}
三、作业监听器

Elastic-job的作业监听器是分为两种的,第一种就是在每一个任务执行前和执行后都执行监听器中对应的方法,这个是官方推荐的事件监听器。另一个是分布式的事件监听器,只会在最后一个任务执行前和执行后来执行监听器中的方法,但是有bug需要官方修复,就是如果并发创建服务器的时候会出现异常,这也是官方不推荐的方法。

下面主要来讲解第一种作业监听器的使用步骤

​ 1)、创建监听器类实现ElasticJobListener接口

@Slf4j
//如果我们这个监听器实现的是ElasticJobListener接口的话,就不是分布式的监听器,
// 每次到定时时间后执行那个任务之前会调用beforeJobExecuted(),执行完之后调用afterJobExecuted()
public class NormalJobListener implements ElasticJobListener {
    @Override
    public void beforeJobExecuted(ShardingContexts shardingContexts) {
        log.info(shardingContexts.getJobName()+"开始执行前");
    }

    @Override
    public void afterJobExecuted(ShardingContexts shardingContexts) {
        log.info(shardingContexts.getJobName()+"开始执行后");
    }
}

​ 2)、为了让代码更加健壮,我们应该在自己写的注解中添加使用的监听器列表

//自定义的Simple任务注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface ElasticJobSimpleJob {
    //下面的这些都是注解里面的属性
    String jobName() default "";
    String cron() default "";
    int shardingTotalCount() default 1;
    boolean overwrite() default false;
    Class<? extends JobShardingStrategy> jobShardingStrategyClass() default AverageAllocationJobShardingStrategy.class;
    //是否开启事件追踪功能
    boolean isStartJobEvent() default false;
    Class<? extends ElasticJobListener>[] normalJobListener() default {};
}

​ 3)、更新Simple类型的任务的自动配置类和Dataflow类型任务的自动配置类

//注意因为此时的这个配置类的位置不和主启动在一个包下,所以即使添加了@Configuration注解,也不会扫描到,我们要把这个类配置到spring.factories中
@Configuration
//当注册中心一加载知乎就来执行这个自动配置类
@ConditionalOnBean(CoordinatorRegistryCenter.class)
//保证在注册中心成功加载之后这个类才进行一个加载
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class SimpleJobAutoConfig {
    //注入上下文
    @Autowired
    private ApplicationContext applicationContext;
    //注入注册中心
    @Resource
    private CoordinatorRegistryCenter zkCenter;
    //注入数据源信息
    @Autowired
    private DataSource dataSource;

    @PostConstruct
    public void initSimple() throws IllegalAccessException, InstantiationException {
        //获取使用了@ElasticJobSimpleJob注解的实例
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobSimpleJob.class);
        //遍历所有的实例
        for (Map.Entry<String, Object> entry : beans.entrySet()) {
            Object instance = entry.getValue();
            //通过反射获得每个实例的接口,必须要实现了Simple接口才可以,主要是保险
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                //判断这个接口是不是Simple接口
                if (superInterface == SimpleJob.class) {
                    //获取注解中的信息
                    ElasticJobSimpleJob annotation = instance.getClass().getAnnotation(ElasticJobSimpleJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    //获得注解中的分片策略属性
                    Class<? extends JobShardingStrategy> jobShardingStrategyClass = annotation.jobShardingStrategyClass();
                    //是否开启事件追踪功能
                    boolean isStartJobEvent = annotation.isStartJobEvent();
                    //获取作业监听器列表
                    Class<? extends ElasticJobListener>[] listeners = annotation.normalJobListener();
                    ElasticJobListener[] jobListeners = null;
                    if (listeners.length > 0 && listeners != null) {
                        jobListeners = new ElasticJobListener[listeners.length];
                        int i = 0;
                        for (Class<? extends ElasticJobListener> listener : listeners) {
                            ElasticJobListener elasticJobListener = listener.newInstance();
                            jobListeners[i] = elasticJobListener;
                            i++;
                        }
                    }
                    //将simple任务注册到zookeeper中

                    //job核心配置
                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    //job类型配置
                    JobTypeConfiguration jobTypeConfiguration = new SimpleJobConfiguration(jobCoreConfiguration, instance.getClass().getCanonicalName());

                    //开始事件追踪功能,需要配置数据源信息
                    JobEventConfiguration jobEventConfiguration = new JobEventRdbConfiguration(dataSource);

                    //job根配置
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).jobShardingStrategyClass(jobShardingStrategyClass.getCanonicalName()).overwrite(overwrite).build();
                    if (isStartJobEvent) {
                        if (jobListeners.length > 0 && jobListeners != null) {
                            //配置开启事件追踪功能
                            new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobEventConfiguration, jobListeners).init();
                        } else {
                            //配置开启事件追踪功能
                            new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobEventConfiguration).init();
                        }
                    }
                    if (jobListeners.length > 0 && jobListeners != null) {
                        new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobListeners).init();
                    } else {
                        new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration).init();
                    }
                }
            }
        }
    }
}
@Configuration
@ConditionalOnBean(CoordinatorRegistryCenter.class)
@AutoConfigureAfter(ZookeeperAutoConfig.class)
public class DataflowJobAutoConfig {
    @Autowired
    private ApplicationContext applicationContext;
    @Resource
    private CoordinatorRegistryCenter zkCenter;
    @Autowired
    private DataSource dataSource;

    @PostConstruct
    public void dataFlowJobInit() throws IllegalAccessException, InstantiationException {
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElasticJobDataflowJob.class);
        for (Map.Entry<String, Object> bean : beans.entrySet()) {
            Object instance = bean.getValue();
            Class<?>[] interfaces = instance.getClass().getInterfaces();
            for (Class<?> superInterface : interfaces) {
                if (superInterface == DataflowJob.class) {
                    ElasticJobDataflowJob annotation = instance.getClass().getAnnotation(ElasticJobDataflowJob.class);
                    String jobName = annotation.jobName();
                    String cron = annotation.cron();
                    int shardingTotalCount = annotation.shardingTotalCount();
                    boolean overwrite = annotation.overwrite();
                    boolean streamingProcess = annotation.streamingProcess();
                    Class<? extends JobShardingStrategy> jobShardingStrategyClass = annotation.jobShardingStrategyClass();
                    boolean isStartJobEvent = annotation.isStartJobEvent();
                    Class<? extends ElasticJobListener>[] listeners = annotation.normalListeners();
                    ElasticJobListener[] jobListeners = null;
                    if (listeners != null && listeners.length > 0) {
                        jobListeners = new ElasticJobListener[listeners.length];
                        int i = 0;
                        for (Class<? extends ElasticJobListener> listener : listeners) {
                            ElasticJobListener elasticJobListener = listener.newInstance();
                            jobListeners[i] = elasticJobListener;
                            i++;
                        }
                    }
                    JobCoreConfiguration jobCoreConfiguration = JobCoreConfiguration.newBuilder(jobName, cron, shardingTotalCount).build();
                    JobTypeConfiguration jobTypeConfiguration = new DataflowJobConfiguration(jobCoreConfiguration, instance.getClass().getCanonicalName(), streamingProcess);
                    LiteJobConfiguration liteJobConfiguration = LiteJobConfiguration.newBuilder(jobTypeConfiguration).jobShardingStrategyClass(jobShardingStrategyClass.getCanonicalName()).overwrite(overwrite).build();
                    JobEventConfiguration jobEventConfiguration = new JobEventRdbConfiguration(dataSource);
                    if (isStartJobEvent) {
                        if (jobListeners != null && jobListeners.length > 0) {
                            new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobEventConfiguration, jobListeners).init();
                        } else {
                            new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobEventConfiguration).init();
                        }
                    } else {
                        if (jobListeners != null && jobListeners.length > 0) {
                            new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration, jobListeners).init();
                        } else {
                            new SpringJobScheduler((ElasticJob) instance, zkCenter, liteJobConfiguration).init();
                        }
                    }
                }
            }
        }
    }
}

至此,Elastic-job的学习就结束了。

二、Quartz定时任务框架的学习

一、快速开启一个简单的Quartz案例,来体验一下Quartz

步骤如下

​ 1)、创建一个maven项目

​ 2)、将quartz的依赖导入

  <dependency>
      <groupId>org.quartz-scheduler</groupId>
      <artifactId>quartz</artifactId>
      <version>2.3.1</version>
    </dependency>

​ 3)、创建Quartz的配置文件 quartz.properties

#给这个quartz起个名字
org.quartz.scheduler.instanceName=MyQuartz 
#设置quartz的线程池中的线程数,表示最大可以有多少任务可以同时执行
org.quartz.threadPool.threadCount=3
#配置quartz中的job存储方法,这里用的简单的内存存储,当然也可以存储在数据库中了
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore

​ 4)、创建一个job类,要实现Job接口

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        LocalTime now=LocalTime.now();
        System.out.println("正在执行定时任务"+now);
    }
}

​ 5)、创建一个启动类

public class DemoApplication {
    public static void main(String[] args) {
        try {
            //床架一个调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            //开启调度器
            scheduler.start();
            //创建一个作业的细节JobDetail
            JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                                //withIdentity是给这个设置一个唯一标识
                                .withIdentity("job1", "group1")
                                .build();
            //创建一个触发器
            SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1", "group1")
                    .startNow()
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                            .withIntervalInSeconds(10)//十秒间隔执行一次
                            .repeatForever() //一直执行下去
                    )
                    .build();
            //将jobDetail和Trigger放到调度器中,将两者联系起来
            scheduler.scheduleJob(jobDetail,simpleTrigger);
            try {
                Thread.sleep(600000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            scheduler.shutdown();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

​ 6)、启动main函数就可以看到效果了。

二、Job和JobDetail

我们自己写的Job类需要实现Job接口,然后实现里面的execute()方法,这个execute()方法里面定义了我们执行定时任务的逻辑。

但是需要注意的是:如果我们使用的是默认的JobFactory的时候,Job需要有一个无参构造器,每次执行这个定时任务的时候都会执行这个构造器去创建一个实例,当任务执行完的时候,将这个实例进行垃圾回收。这也就导致了一个弊端就是在Job类中不应该去定义有状态的属性,这个这个状态是不会保留的,每次执行完就会被丢弃了。

我们在创建JobDetail的时候,会指定这个JobDetail去执行那个Job,而且我们可以在JobDetail中通过使用JobDataMap来传递不受限制数量的参数,通过JobDataMap进行传递的参数是可以在Job中取出来并且使用的。

Job获取JobDataMap传递过来的参数的方法有两种,第一种比较简单,就是我们通过在Job类中声明和JobDataMap中传递过来的map中的key同名的参数,并结合Lombok来使用可以快速的获得这个传递过来的属性。

另一个方法的话就是通过jobExecutionContext 这个上下文来获得了

下面来看一下上述两种方法来取得JobDataMap传递过来的参数的代码。

//在JobDetail中使用JobDataMap来传递参数
public class DemoApplication {
    public static void main(String[] args) {
        try {
            //床架一个调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            //开启调度器
            scheduler.start();
            //创建一个作业的细节JobDetail
            JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                                //withIdentity是给这个设置一个唯一标识
                                .withIdentity("job1", "group1")
                                //使用JobDataMap来进行传递参数
                                .usingJobData("name","Lisa")
                                .build();
            //创建一个触发器
            SimpleTrigger simpleTrigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1", "group1")
                    .startNow()
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                            .withIntervalInSeconds(10)//十秒间隔执行一次
                            .repeatForever() //一直执行下去
                    )
                    .build();
            //将jobDetail和Trigger放到调度器中,将两者联系起来
            scheduler.scheduleJob(jobDetail,simpleTrigger);
            try {
                Thread.sleep(600000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            scheduler.shutdown();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}
public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        LocalTime now=LocalTime.now();
        //通过jobExecutionContext来获取的,主要要先获得jobDetail才可以
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        String name = jobDataMap.getString("name");
        System.out.println("name:"+name);
        System.out.println("正在执行定时任务"+now);
    }
}
@Data
public class MyJob implements Job {
    private String name;
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        LocalTime now=LocalTime.now();
        System.out.println("name:"+name);
        System.out.println("正在执行定时任务"+now);
    }
}

上面我们介绍了如果通过JobDetail中的JobDataMap进行参数传递,将参数传递给Job然后让Job来进行一个使用,那么如果我们execute()方法执行的过程中更改了传过来的参数的话,我们怎么给JobDataMap中传过去呢?

下面我们就来讲解一下

Quartz中可以通过@PersistJobDataAfterExecution注解,再结合jobExecutionContext.getJobDetail().getJobDataMap().put()方法来实现这个功能。

@Data
//@DisallowConcurrentExecution
@PersistJobDataAfterExecution
public class MyJob implements Job {
    private String name;
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        LocalTime now=LocalTime.now();
        System.out.println(now.toString()+",name:"+name);
        Random random=new Random();
        jobExecutionContext.getJobDetail().getJobDataMap().put("name","Jerry"+random.nextInt(10));
    }
}
打印的结果如下
21:05:43.016,name:Lisa
21:05:47.985,name:Jerry1
21:05:52.982,name:Jerry8
21:05:57.979,name:Jerry3
三、Quartz防止定时任务并发

先说一下什么情况下会发生定时任务的并发,假如我们的每个定时任务的执行过程需要7秒钟,但是每次调用定时任务的时间间隔为5秒钟,那么就会出现一个问题,就是我们的一个定时任务还没有执行完,另一个定时任务已经启动了,这样的话可能会导致我们执行的过程中出现重复的部分,从而出现问题。

为了避免这个问题,Quartz中可以使用@DisallowConcurrentExecution注解来防止定时任务并发。将这个注解添加在对应的我们自己写的实现了Job接口的类上。

@Data
@DisallowConcurrentExecution
public class MyJob implements Job {
    private String name;
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        LocalTime now=LocalTime.now();
//        System.out.println("name:"+name);
        System.out.println(Thread.currentThread().getName()+",正在执行定时任务"+now);
        try {
            Thread.sleep(7000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LocalTime now2=LocalTime.now();
        System.out.println(Thread.currentThread().getName()+",结束执行定时任务"+now2);
    }
}

执行后的结果如下

MyQuartz_Worker-1,正在执行定时任务20:49:34.099
MyQuartz_Worker-1,结束执行定时任务20:49:41.106
MyQuartz_Worker-2,正在执行定时任务20:49:41.106
MyQuartz_Worker-2,结束执行定时任务20:49:48.107
MyQuartz_Worker-3,正在执行定时任务20:49:48.107
MyQuartz_Worker-3,结束执行定时任务20:49:55.121
MyQuartz_Worker-1,正在执行定时任务20:49:59.079
MyQuartz_Worker-1,结束执行定时任务20:50:06.084
MyQuartz_Worker-2,正在执行定时任务20:50:06.084
MyQuartz_Worker-2,结束执行定时任务20:50:13.084
MyQuartz_Worker-3,正在执行定时任务20:50:13.084

不会出现两个任务同时执行的情况。

四、Trigger触发器

Trigger触发器的优先级

多个Trigger触发器在同一个时间一起执行的话,怎么来确定哪个Trigger先执行呢?

可以通过设置Trigger的优先级来确定哪个Trigger先执行,Trigger的优先级默认是5,优先级越大越先执行,下面来看一下代码。

public class DemoApplication {
    public static void main(String[] args) {
        try {
            //床架一个调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            //开启调度器
            scheduler.start();
            //创建一个作业的细节JobDetail
            JobDetail jobDetail1 = JobBuilder.newJob(MyJob.class)
                                //withIdentity是给这个设置一个唯一标识
                                .withIdentity("job1", "group1")
                                .build();

            JobDetail jobDetail2 = JobBuilder.newJob(MyJob.class)
                    //withIdentity是给这个设置一个唯一标识
                    .withIdentity("job2", "group2")
                    .build();
            Date futureDate = DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND);

            Trigger trigger1 = TriggerBuilder.newTrigger()
                    .startAt(futureDate)
                    .withPriority(1)
                    .withIdentity("trigger1", "group1")
                    .usingJobData("name","Trigger1")
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                            .withIntervalInSeconds(5)//十秒间隔执行一次
                            .repeatForever() //一直执行下去
                    )
                    .build();
            Trigger trigger2 = TriggerBuilder.newTrigger()
                    .startAt(futureDate)
                    .withPriority(9)
                    .withIdentity("trigger2", "group2")
                    .usingJobData("name","Trigger2")
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                            .withIntervalInSeconds(5)//十秒间隔执行一次
                            .repeatForever() //一直执行下去
                    )
                    .build();
            //将jobDetail和Trigger放到调度器中,将两者联系起来
            scheduler.scheduleJob(jobDetail1,trigger1);
            scheduler.scheduleJob(jobDetail2,trigger2);
            try {
                Thread.sleep(600000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            scheduler.shutdown();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}
#注意我们还要修改一下quartz的线程数,保证两个任务同时一个线程执行
#给这个quartz起个名字
org.quartz.scheduler.instanceName=MyQuartz
#设置quartz的线程池中的线程数,表示最大可以有多少任务可以同时执行
org.quartz.threadPool.threadCount=1
#配置quartz中的job存储方法,这里用的简单的内存存储,当然也可以存储在数据库中了
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore
执行的结果如下
22:07:55.944,我是:Trigger2
22:07:56.954,我是:Trigger1
22:08:00.942,我是:Trigger2
22:08:01.953,我是:Trigger1
22:08:05.942,我是:Trigger2
22:08:06.948,我是:Trigger1
五、Cron触发器

我们在前面的Elastic-job学习过Cron表达式,通过cron表达式来控制我们的任务执行的时间。那么我们的Quartz中有没有类似的机制的?

那就是使用Cron触发器,主要是修改一下Trigger的创建过程代码,代码如下

 Trigger trigger1 = TriggerBuilder.newTrigger()
                    .startAt(futureDate)
                    .withPriority(1)
                    .withIdentity("trigger1", "group1")
                    .usingJobData("name","Trigger1")
                    .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
                    )
                    .build();

六、Trigger的misFire机制

Trigger的misFire机制就是当我们的scheduler关闭了或者是我们的线程池中的线程数小于Trigger的数量的话,就会导致有的Trigger无法执行,就会导致错过执行,这也就是misFire。不同的Trigger有不同的misFire机制,如果我们不设置每个Trigger的misFire的话,会使用默认的smart-policy策略,会根据Trigger的类型和配置动态调整行为。

下面来说一个各个类型的Trigger的misFire机制

SimpleTrigger的misFire机制

1、MISFIRE_INSTRUCTION_FIRE_NOW

这个策略只适合用在Trigger只会触发一次的Trigger上,作用大概就是scheduler想要立即被激活去执行。大概就是当当前的这个任务执行完后会立即执行这个已经错过的任务。

2、MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY

这个策略的意思大概就是:会告诉Scheduler这个Trigger将不会为misFire情况进行一个评估。只是会简单的进行尝试执行只要是有可能执行,如果在合适的时间执行了的话,就会更新这个Trigger。

如果当一个Trigger使用了这个misFire机制,并且已经错过了几次预定的触发,那么当触发器试图去进行触发执行的时候,可能会发生多次的连续触发。举个栗子:如果我们一个SimpleTrigger每15秒进行一次触发,但是已经错过了5分钟,那么一旦有可能去执行的时候它会去执行20次快速的触发。

3、MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT

这个策略的意思大概是:指示Scheduler,如果这个Trigger发生了misFire的话,SimpleTrigger想在现在的下一个调度时间执行。并且会保持这个Trigger触发的次数不会发生改变。说人话大概就是:我错过这个时间,那么我想要在下一个调度时间去执行。

注意:使用这个策略的话,如果此时这个Trigger的结束时间已经到达了,会造成这个Trigger直接到达COMPLETE状态。

4、MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT

这个策略和上一个策略差不多,如果这次错过了触发,会在下一个调度时间进行一个触发。但是这个策略会将重复次数更新为剩余的执行次数

注意:这个策略如果我们的Triigger错过了所有的触发的话,会让这个Trigger直接变成COMPLETE状态。

5、MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

这个策略当Trigger错过了触发时间的时候会,SimpleTrigger希望重新调度到Now,保持原来的重复次数不变。但是如果now在结束之间之后的话就不会进行一个触发了。

这个策略会有一个问题:会让这个trigger忘了原来的开始时间和重复次数。

6、MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT

这个策略当Trigger错过了触发时间的时候会,SimpleTrigger希望重新调度到Now。但是如果now在结束之间之后的话就不会进行一个触发了。但是会将重复次数改成剩余的执行次数。

使用此指令会导致触发器“忘记”它最初设置的开始时间和重复计数。相反,触发器上的重复计数将被更改为剩余的重复计数

CronTrigger的misFire机制

1、MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY

前面已经讲过了

2、MISFIRE_INSTRUCTION_DO_NOTHING

这个策略是当CronTrigger错过了任务执行之后,会更新下一次的触发时间为当前时间的下一次调度时间。举个栗子:任务每隔10min执行一次。当前时间为10:10 执行了一个任务,此时另一个trigger错过了,那么错过的trigger会更新执行时间为10:20,会当10:20的时候去触发执行。

注意:在使用这个策略的时候我们要考虑到misfireThreshold,这个是misFire的一个阈值时间。默认是60s。打个比方,如果我们任务每5s执行一次,任务的执行过程是7s的话,当我们一个调度时间到的时候另一个任务还没有执行完,此时发生了misFirie,我们已经设置了MISFIRE_INSTRUCTION_DO_NOTHING策略,按理说应该会在下一个任务调度时间去执行,但是你会发现当这个7s的任务一执行完毕后这个任务就会立即执行,会变成MISFIRE_INSTRUCTION_FIRE_ONCE_NOW策略一样的效果,这是因为misFire的阈值时间太大了,容忍度太高了,我们要将这个阈值调小。这个阈值可以简单的理解为错过了多长时间才会被任务是misFire了。7-5=2 那么我们的这个阈值应该小于2才可以。

#单位是毫秒
org.quartz.jobStore.misfireThreshold=1000
public class DemoApplication {
    public static void main(String[] args) {
        try {
            //床架一个调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            //开启调度器
            scheduler.start();
            //创建一个作业的细节JobDetail
            JobDetail jobDetail1 = JobBuilder.newJob(MyJob.class)
                                //withIdentity是给这个设置一个唯一标识
                                .withIdentity("job1", "group1")
                                .build();
            Trigger trigger1 = TriggerBuilder.newTrigger()
                    .startNow()
                    .withPriority(1)
                    .withIdentity("trigger1", "group1")
                    .usingJobData("name","Trigger1")
                    .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?").withMisfireHandlingInstructionDoNothing())
                    .build();
            //将jobDetail和Trigger放到调度器中,将两者联系起来
            scheduler.scheduleJob(jobDetail1,trigger1);
            try {
                Thread.sleep(600000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            scheduler.shutdown();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

结果如下

16:46:00.028,我是:Trigger1
16:46:10.008,我是:Trigger1
16:46:20.002,我是:Trigger1

3、MISFIRE_INSTRUCTION_FIRE_ONCE_NOW

错过执行的话会去请求立即重新执行。会当一个任务执行完后这个错过的任务立即执行。

二、Quartz的三种整合方式
一、与Java API整合

上面的代码其实就是和Java API的整合,所以这里就不再赘述了

二、与Spirng 整合

整合的步骤如下

​ 1)、创建一个maven工程

​ 2)、导入依赖

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.1.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.1.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context-support</artifactId>
      <version>5.1.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.24</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.quartz-scheduler</groupId>
      <artifactId>quartz</artifactId>
      <version>2.3.1</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>5.1.6.RELEASE</version>
    </dependency>
  </dependencies>

  <build>
    <finalName>spring-quartz</finalName>

      <plugins>
        <plugin>
          <groupId>org.apache.tomcat.maven</groupId>
          <artifactId>tomcat7-maven-plugin</artifactId>
          <version>2.2</version>
          <executions>
            <execution>
              <id>tomcat-run</id>
              <goals>
                <goal>exec-war-only</goal>
              </goals>
              <phase>package</phase>
              <configuration>
                <path>foo</path>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>
  </build>
</project>

​ 3)、修改web.xml文件

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>spring-quartz</display-name>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath*:spring-config.xml</param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
</web-app>

​ 4)、在resources文件夹下创建spring-config.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <!--    这个jobDetail的配置是第一种方式,我们自己写的job类必须继承QuartzJobBean才可以-->
    <bean id="JobDetail1" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
        <property name="name" value="job1"/>
        <property name="group" value="group1"/>
        <property name="jobClass" value="com.lyq.job.MyJob"/>
    </bean>

    <bean id="Myjob2" class="com.lyq.job.MyJob2"></bean>

    <!--    这个是jobDetail配置的第二种方式,这种方式的话我们自己写的类不需要继承QuartzJobBean,我们自己写方法就可以了也没有啥限制-->
    <bean id="jobDetail2" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="name" value="job2"/>
        <property name="group" value="goup2"/>
        <!--        因为我们自己写的类中的方法不是static静态方法,所以我们必须使用targetObject来指定我们的目标对象-->
        <!--        如果使用的是targetClass来指定目标类的话,这个目标来中调用的方法必须是静态的-->
        <property name="targetObject" ref="Myjob2"/>
        <property name="targetMethod" value="execute"/>
    </bean>

    <!--    配置trigger,注意直接将jobDetail配置到这个trigger中-->
    <bean id="trigger1" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
        <property name="name" value="trigger1"/>
        <property name="group" value="group1"/>
        <!--        这里注意,因为我们引入的JobDetail已经配置成了一个Bean,所以我们使用ref来引用,而不能使用value-->
        <property name="jobDetail" ref="JobDetail1"/>
        <property name="repeatInterval" value="5000"/>
    </bean>

    <!--    配置了一个CronTrigger-->
    <bean id="trigger2" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="name" value="trigger2"/>
        <property name="group" value="group2"/>
        <property name="jobDetail" ref="jobDetail2"/>
        <property name="cronExpression" value="0/5 * * * * ?"/>
    </bean>

    <!--创建一个Scheduler,将trigger配置进来-->
    <bean id="scheduler1" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="trigger1"/>
                <ref bean="trigger2"/>
            </list>
        </property>
    </bean>
</beans>

​ 5)、创建我们自己写的job类

public class MyJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        LocalTime now=LocalTime.now();
        System.out.println(now.toString()+",正在执行任务...");
    }
}
public class MyJob2 {
    public void execute(){
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",我是MyJob2,我正在执行任务...");
    }
}

搞定了!!!

三、与Spring Boot整合

与Spring Boot整合的步骤如下

​ 1)、创建一个Spring Boot项目

​ 2)、导入依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

​ 3)、修改配置文件

#配置scheduler调度器的名字
spring.quartz.scheduler-name=mySpringBootQuartz
#配置job存储的方式
spring.quartz.job-store-type=memory
#配置对quartz的配置是不是每次更改后会覆盖之前的配置
spring.quartz.overwrite-existing-jobs=true
#当我们关闭auartz的时候,需要等待job执行完后才关闭
spring.quartz.wait-for-jobs-to-complete-on-shutdown=true

​ 4)、创建job类,继承QuartzJobBean

@Slf4j
public class MyJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        LocalTime now = LocalTime.now();
        log.info(now.toString()+",我是MyJob,我正在执行任务...");
    }
}

​ 5)、创建配置类,来配置JobDetail和Trigger

@Configuration
public class QuartzConfig {
    @Bean
    public JobDetail myJobDetail() {
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("jobDetail1", "group1")
                .storeDurably()//持久化存储
                .build();
        return jobDetail;

    }

    @Bean
    public Trigger myTrigger(){
        SimpleTrigger trigger = TriggerBuilder.newTrigger()
                .startNow()
                .withIdentity("trigger1", "group1")
                .forJob(myJobDetail())//jobDetail要配置在Trigger中
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(5)
                        .repeatForever()
                ).build();
        return trigger;
    }
}

​ 6)、启动

上面我们是使用的内存来存储job的,下面使用jdbc来存储job

步骤如下

​ 1)、修改之前的配置文件

#配置scheduler调度器的名字
spring.quartz.scheduler-name=mySpringBootQuartz
#配置job存储的方式
#spring.quartz.job-store-type=memory
spring.quartz.job-store-type=jdbc
#配置对quartz的配置是不是每次更改后会覆盖之前的配置
spring.quartz.overwrite-existing-jobs=true
#当我们关闭auartz的时候,需要等待job执行完后才关闭
spring.quartz.wait-for-jobs-to-complete-on-shutdown=true
#配置数据源的信息
spring.datasource.url=jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#配置quartz中的sql文件的注释的前缀
spring.quartz.jdbc.comment-prefix=#,--
#配置quartz使用jdbc存储的时候初始化的时候的数据库的执行规则,如果是always代表每次启动的时候都去执行建表,never代表不会去建表,embedded是嵌入
spring.quartz.jdbc.initialize-schema=always

其他d都不需要进行更改,注意以后的执行不要用always,而是用nerve否则会重建表,表中数据就没了

​ 2)、查看数据库就会发现创建了很多表,并且表中记录了trigger和jobDetail等等很多信息

三、Quartz的高级特性
一、TriggerListener

Java API整合TriggerListener的步骤

​ 1)、创建listener

public class MyTriggerListener extends TriggerListenerSupport {
    @Override
    public String getName() {
        return "MyTriggerListener";
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        super.triggerFired(trigger, context);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",trigger开始触发");
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
        super.triggerComplete(trigger, context, triggerInstructionCode);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Trigger结束触发");
    }
}

​ 2)、将triggerListener配置到scheduler中

   //在scheduler中配置triggerListener
            scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());

Spring整合TriggerListener

​ 1)、创建listener

public class MyTriggerListener extends TriggerListenerSupport {
    @Override
    public String getName() {
        return "MyTriggerListener";
    }
    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        super.triggerFired(trigger, context);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",trigger开始触发");
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
        super.triggerComplete(trigger, context, triggerInstructionCode);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Trigger结束触发");
    }
}

​ 2)、在spring-config.xml文件中配置这个triggerListener

   <bean id="myTriggerListener" class="com.lyq.listener.MyTriggerListener"/>
  <!--创建一个Scheduler,将trigger配置进来-->
    <bean id="scheduler1" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="trigger1"/>
                <ref bean="trigger2"/>
            </list>
        </property>
        <property name="globalTriggerListeners">

            <list>
               <ref bean="myTriggerListener"/>
            </list>
        </property>
    </bean>

实战一下:要求:所有以5结尾的事件点都不触发trigger

下面是我们的listener的代码,只需要在里面重写vetoJobExecution()方法就可以了,如果这个方法返回true就不会触发trigger,如果返回false则会触发trigger

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        Calendar calendar = Calendar.getInstance();
        int second = calendar.get(Calendar.SECOND);
        if(second%5==0){
            return true;
        }
        return false;
    }
二、JobListener

Java API整合JobListener,JobListener主要是在job运行前后进行一个监控

​ 1)、 创建自己的JobListener

public class MyJobListener extends JobListenerSupport {
    @Override
    public String getName() {
        return "myJobListener";
    }

    @Override
    //job开始时这是这个任务
    public void jobToBeExecuted(JobExecutionContext context) {
        super.jobToBeExecuted(context);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Job将要去运行了...");
    }

    @Override
    //这个方法只有当Trigger不去执行的才会执行
    public void jobExecutionVetoed(JobExecutionContext context) {
        super.jobExecutionVetoed(context);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Job因为Trigger不执行从而也不执行了...");
    }

    @Override
    //job结束后执行这个方法
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        super.jobWasExecuted(context, jobException);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Job运行结束了...");
    }
}

​ 2)、在scheduler中配置这个JobListener

            //在sheduler中配置jobListener
            scheduler.getListenerManager().addJobListener(new MyJobListener());

下面对JobListener进行一个实战

实战要求:记录每一个job任务执行的时间

​ 1)、加入下面的依赖

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-dbcp2</artifactId>
      <version>2.6.0</version>
    </dependency>
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <version>8.0.31</version>
    </dependency>

​ 2)、在数据库中创建一个表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for quartz_job_log
-- ----------------------------
DROP TABLE IF EXISTS `quartz_job_log`;
CREATE TABLE `quartz_job_log`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `job_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务名字',
  `type` int NOT NULL COMMENT '1:开始时间 2:结束时间',
  `time` time NOT NULL COMMENT 'job开始或者结束的时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

​ 3)、写一个JobListener类来定义我们的处理逻辑

public class MyJobListener extends JobListenerSupport {
    @Override
    public String getName() {
        return "myJobListener";
    }

    @SneakyThrows
    @Override
    //job开始时这是这个任务
    public void jobToBeExecuted(JobExecutionContext context) {
        super.jobToBeExecuted(context);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Job将要去运行了...");
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setUrl("jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC&useSSL=false");
        dataSource.setDriver(new Driver());
        DataSourceConnectionFactory connectionFactory = new DataSourceConnectionFactory(dataSource);
        Connection connection = connectionFactory.createConnection();
        PreparedStatement preparedStatement = connection.prepareStatement("insert  into quartz_job_log(job_name,type,time) values(?,?,?)");
        preparedStatement.setString(1,context.getJobDetail().getKey().toString());
        preparedStatement.setInt(2,1);
        preparedStatement.setTime(3,new Time(new Date().getTime()));
        preparedStatement.execute();
        preparedStatement.close();
        connection.close();
        dataSource.close();
    }


    @SneakyThrows
    @Override
    //job结束后执行这个方法
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        super.jobWasExecuted(context, jobException);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Job运行结束了...");
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setUrl("jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC&useSSL=false");
        dataSource.setDriver(new Driver());
        DataSourceConnectionFactory connectionFactory = new DataSourceConnectionFactory(dataSource);
        Connection connection = connectionFactory.createConnection();
        PreparedStatement preparedStatement = connection.prepareStatement("insert  into quartz_job_log(job_name,type,time) values(?,?,?)");
        preparedStatement.setString(1,context.getJobDetail().getKey().toString());
        preparedStatement.setInt(2,2);
        preparedStatement.setTime(3,new Time(new Date().getTime()));
        preparedStatement.execute();
        preparedStatement.close();
        connection.close();
        dataSource.close();
    }
}

下面再来说一下Spring中怎么使用上面的这个实战案例

​ 1)、添加下面的两个依赖

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-dbcp2</artifactId>
      <version>2.6.0</version>
    </dependency>
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <version>8.0.31</version>
    </dependency>

​ 2)、修改xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--    配置数据源的bean-->
    <bean id="datasource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
        <property name="url" value="jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC&amp;useSSL=false"/>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    </bean>

<!--    将我们自己的JobListener监听器加入到bean中,并且将datasource注入进去-->
    <bean id="MyJobListener" class="com.lyq.listener.MyJobListener">
        <property name="dataSource" ref="datasource"/>
    </bean>

    <!--    这个jobDetail的配置是第一种方式,我们自己写的job类必须继承QuartzJobBean才可以-->
    <bean id="JobDetail1" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
        <property name="name" value="job1"/>
        <property name="group" value="group1"/>
        <property name="jobClass" value="com.lyq.job.MyJob"/>
    </bean>

    <bean id="Myjob2" class="com.lyq.job.MyJob2"></bean>

    <!--    这个是jobDetail配置的第二种方式,这种方式的话我们自己写的类不需要继承QuartzJobBean,我们自己写方法就可以了也没有啥限制-->
    <bean id="jobDetail2" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="name" value="job2"/>
        <property name="group" value="goup2"/>
        <!--        因为我们自己写的类中的方法不是static静态方法,所以我们必须使用targetObject来指定我们的目标对象-->
        <!--        如果使用的是targetClass来指定目标类的话,这个目标来中调用的方法必须是静态的-->
        <property name="targetObject" ref="Myjob2"/>
        <property name="targetMethod" value="execute"/>
    </bean>

    <!--    配置trigger,注意直接将jobDetail配置到这个trigger中-->
    <bean id="trigger1" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
        <property name="name" value="trigger1"/>
        <property name="group" value="group1"/>
        <!--        这里注意,因为我们引入的JobDetail已经配置成了一个Bean,所以我们使用ref来引用,而不能使用value-->
        <property name="jobDetail" ref="JobDetail1"/>
        <property name="repeatInterval" value="5000"/>
    </bean>

    <!--    配置了一个CronTrigger-->
    <bean id="trigger2" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
        <property name="name" value="trigger2"/>
        <property name="group" value="group2"/>
        <property name="jobDetail" ref="jobDetail2"/>
        <property name="cronExpression" value="0/5 * * * * ?"/>
    </bean>

    <bean id="myTriggerListener" class="com.lyq.listener.MyTriggerListener"/>

    <!--创建一个Scheduler,将trigger配置进来-->
    <bean id="scheduler1" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="triggers">
            <list>
                <ref bean="trigger1"/>
                <ref bean="trigger2"/>
            </list>
        </property>
        <property name="globalTriggerListeners">
            <list>
               <ref bean="myTriggerListener"/>
            </list>
        </property>
        <property name="globalJobListeners">

            <list>
               <ref bean="MyJobListener"/>
            </list>
        </property>
    </bean>
</beans>

​ 3)、编写自定义的JobListener类

public class MyJobListener extends JobListenerSupport {
    private DataSource dataSource;

    public DataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public String getName() {
        return "myJobListener";
    }

    @Override
    //job开始时这是这个任务
    public void jobToBeExecuted(JobExecutionContext context) {
        super.jobToBeExecuted(context);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Job将要去运行了...");
        DataSourceConnectionFactory connectionFactory = new DataSourceConnectionFactory(dataSource);
        Connection connection = null;
        try {
            connection = connectionFactory.createConnection();
            PreparedStatement preparedStatement = connection.prepareStatement("insert  into quartz_job_log(job_name,type,time) values(?,?,?)");
            preparedStatement.setString(1,context.getJobDetail().getKey().toString());
            preparedStatement.setInt(2,1);
            preparedStatement.setTime(3,new Time(new Date().getTime()));
            preparedStatement.execute();
            preparedStatement.close();
            connection.close();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
    }


    @SneakyThrows
    @Override
    //job结束后执行这个方法
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        super.jobWasExecuted(context, jobException);
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",Job运行结束了...");
        DataSourceConnectionFactory connectionFactory = new DataSourceConnectionFactory(dataSource);
        Connection connection = connectionFactory.createConnection();
        PreparedStatement preparedStatement = connection.prepareStatement("insert  into quartz_job_log(job_name,type,time) values(?,?,?)");
        preparedStatement.setString(1,context.getJobDetail().getKey().toString());
        preparedStatement.setInt(2,2);
        preparedStatement.setTime(3,new Time(new Date().getTime()));
        preparedStatement.execute();
        preparedStatement.close();
        connection.close();
    }
    
}
三、SchedulerListener

Java API整合SchedulerListener的步骤

​ 1)、创建自定义的SchedulerListener类

public class MySchedulerListener extends SchedulerListenerSupport {

    @Override
    //这个方法是scheduler运行之前调用
    public void schedulerStarting() {
        super.schedulerStarting();
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+", schedulerStarting!!! ");
    }


    @Override
    //这个方法是scheduler运行之后调用
    public void schedulerShuttingdown() {
        super.schedulerShuttingdown();
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+", schedulerShuttingdown!!! ");
    }
}

​ 2)、将这个SchedulerListener注册到scheduler中

public class DemoApplication {
    public static void main(String[] args) {
        try {
            //床架一个调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            //schedulerListener的注册一定要在scheduler运行之前执行
            scheduler.getListenerManager().addSchedulerListener(new MySchedulerListener());
            //为了演示scheduler结束运行的时候可以看到效果,我们使用一个JVM的shutdown钩子,这个方法是当JVM关闭的时候去调用这个方法
            //我们让JVM关闭的时候去关闭scheduler
            Runtime.getRuntime().addShutdownHook(new Thread(){
                @Override
                public void run() {
                    super.run();
                    try {
                        scheduler.shutdown();
                    } catch (SchedulerException e) {
                        e.printStackTrace();
                    }
                }
            });
            //开启调度器
            scheduler.start();
            //创建一个作业的细节JobDetail
            JobDetail jobDetail1 = JobBuilder.newJob(MyJob.class)
                                //withIdentity是给这个设置一个唯一标识
                                .withIdentity("job1", "group1")
                                .build();
            Trigger trigger1 = TriggerBuilder.newTrigger()
                    .startNow()
                    .withPriority(1)
                    .withIdentity("trigger1", "group1")
                    .usingJobData("name","Trigger1")
                    .withSchedule(CronScheduleBuilder.cronSchedule("0/6 * * * * ?").withMisfireHandlingInstructionDoNothing())
                    .build();
            //在scheduler中配置triggerListener
            scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());
            //在sheduler中配置jobListener
            scheduler.getListenerManager().addJobListener(new MyJobListener());
            //将jobDetail和Trigger放到调度器中,将两者联系起来
            scheduler.scheduleJob(jobDetail1,trigger1);
            try {
                Thread.sleep(600000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            scheduler.shutdown();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}
四、使用Quartz完成一个实战

实战要求:使用Quartz集群完成订单按小时统计的功能

下面记录一下步骤

​ 1)、创建两个数据表,一个order表,一个订单统计表

DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
  `amount` decimal(10, 2) NOT NULL COMMENT '订单金额',
  `status` int NOT NULL DEFAULT 0 COMMENT '订单状态 0:未支付 1:已支付 2:已取消',
  `receive_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '收件人姓名',
  `receive_address` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '收件人地址',
  `create_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '创建人姓名',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_name` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '修改人姓名',
  `update_time` datetime NOT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 161 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = DYNAMIC;

SET FOREIGN_KEY_CHECKS = 1;

DROP TABLE IF EXISTS `t_order_statistics`;
CREATE TABLE `t_order_statistics`  (
  `id` int NOT NULL AUTO_INCREMENT COMMENT 'id',
  `time` timestamp NOT NULL COMMENT '订单时间',
  `order_count` int NOT NULL DEFAULT 0 COMMENT '订单数量',
  `create_user` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `update_user` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '更新人',
  `update_time` datetime NOT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

​ 2)、 导入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.4.2</version>
                <dependencies>
                    <dependency>
                        <groupId>com.mysql</groupId>
                        <artifactId>mysql-connector-j</artifactId>
                        <version>8.0.32</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

​ 3)、编写配置文件

#配置scheduler调度器的名字
spring.quartz.scheduler-name=mySpringBootQuartz
#配置job存储的方式
#spring.quartz.job-store-type=memory
#要注意使用Quartz集群必须要设置存储方式为jdbc,这样的话多个服务器才可以同步和共享数据
spring.quartz.job-store-type=jdbc
#配置对quartz的配置是不是每次更改后会覆盖之前的配置
spring.quartz.overwrite-existing-jobs=true
#当我们关闭auartz的时候,需要等待job执行完后才关闭
spring.quartz.wait-for-jobs-to-complete-on-shutdown=true
#配置数据源的信息
spring.datasource.url=jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#配置quartz中的sql文件的注释的前缀
spring.quartz.jdbc.comment-prefix=#,--
#配置quartz使用jdbc存储的时候初始化的时候的数据库的执行规则,如果是always代表每次启动的时候都去执行建表,never代表不会去建表,embedded是嵌入,注意第一次启动的时候设置为always,以后启动都要设置为never
spring.quartz.jdbc.initialize-schema=never
#配置mybatis对应的xml文件的位置
mybatis.mapper-locations=mapper/*.xml

​ 4)、在resources文件夹下创建mybatis generator 的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <classPathEntry location="/Program Files/IBM/SQLLIB/java/db2java.zip" />

    <context id="MysqlTables" targetRuntime="MyBatis3">
        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC&amp;useSSL=false"
                        userId="root"
                        password="123456">
            <property name="nullCatalogMeansCurrent" value="true" />
        </jdbcConnection>

        <javaTypeResolver >
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <javaModelGenerator targetPackage="com.lyq.springbootquartz.entity" targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <sqlMapGenerator targetPackage="mapper"  targetProject="src\main\resources">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <javaClientGenerator type="XMLMAPPER" targetPackage="com.lyq.springbootquartz.mapper"  targetProject="src\main\java">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>

        <table schema="quartz" tableName="t_order" domainObjectName="Order" ></table>
        <table schema="quartz" tableName="t_order_statistics" domainObjectName="OrderStatistics" ></table>

    </context>
</generatorConfiguration>

​ 5)、使用mybatis generator生成entity、mapper、xml等

​ 6)、创建一个job包,然后在这个包中编写自己的job类

public class OrderJob extends QuartzJobBean {
    @Autowired
    private OrderService orderService;
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",下单了....");
       orderService.createOrder();

    }
}
public class StatisticsOrderJob extends QuartzJobBean {
    @Autowired
    private OrderService orderService;
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        LocalTime now = LocalTime.now();
        System.out.println(now.toString()+",获取前一分钟的所有订单。。。");
        orderService.statisticsOrder();
    }
}

​ 7)、编写对应的service

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderStatisticsMapper statisticsMapper;
    public void createOrder(){
        Random random=new Random();
        int num = random.nextInt(5);
        Order order = new Order();
        order.setAmount(new BigDecimal(100));
        order.setStatus(0);
        order.setReceiveName("lyq");
        order.setReceiveAddress("xxxxxx");
        order.setCreateName("system");
        order.setCreateTime(new Date());
        order.setUpdateName("system");
        order.setUpdateTime(new Date());
        orderMapper.insertSelective(order);
    }

    public void statisticsOrder() {
        //获取一分钟前的所有订单
        Calendar now = Calendar.getInstance();
        //设置整秒的时候才会进行获取
        now.set(Calendar.SECOND,0);
        now.set(Calendar.MILLISECOND,0);

        Calendar startTime = Calendar.getInstance();
        //设置整秒的时候才会进行获取
        startTime.set(Calendar.SECOND,0);
        startTime.set(Calendar.MILLISECOND,0);
        //抓取一分钟前的数据
        startTime.add(Calendar.MINUTE,-1);
        List<Order> orders= orderMapper.selectOneMinuteOrders(now.getTime(),startTime.getTime());
        OrderStatistics statistics = new OrderStatistics();
        statistics.setCreateUser("system");
        statistics.setCreateTime(new Date());
        statistics.setUpdateUser("system");
        statistics.setUpdateTime(new Date());
        statistics.setOrderCount(orders.size());
        statistics.setTime(now.getTime());
        statisticsMapper.insertSelective(statistics);
    }
}
//补充一下上面用到的一个mapper中的方法的编写
    @Select("select * from t_order where create_time >#{start} and create_time <=#{now}")
    @ResultMap("BaseResultMap")
    List<Order> selectOneMinuteOrders(@Param("now") Date now, @Param("start") Date start);

​ 8)、编写对应的Quartz的配置类,配置JobDetail和Trigger

@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail jobDetail1() {
        JobDetail jobDetail = JobBuilder.newJob(OrderJob.class).withIdentity("order", "group2").storeDurably().build();
        return jobDetail;
    }

    @Bean
    public JobDetail jobDetail2(){
        JobDetail jobDetail = JobBuilder.newJob(StatisticsOrderJob.class).withIdentity("orderStatisticsJob", "group2").storeDurably().build();
        return jobDetail;
    }

    @Bean
    public Trigger trigger1() {
        CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity("orderTrigger", "group2").forJob(jobDetail1())
                .withSchedule(CronScheduleBuilder.cronSchedule("0/15 * * * * ?")).startNow().build();
        return trigger;
    }

    @Bean
    public Trigger trigger2(){
        CronTrigger trigger = TriggerBuilder.newTrigger().startNow().withIdentity("trigger2", "group2").forJob(jobDetail2())
                //每分钟的第五秒执行获取上一分钟订单的任务
                .withSchedule(CronScheduleBuilder.cronSchedule("5 0/1 * * * ?")).build();
        return trigger;
    }

}

​ 9)、启动测试

补充一下Quartz集群

Quartz集群必须使用jdbc来存储这样多个服务器之间才可以共享和同步对应的jobDetail和Trigger数据。

Quartz集群和elastic-job的区别:

elastic-job是支持分布式的,会将一个任务进行分片,分成多个任务分片,然后给不同的服务器执行

quartz中每个任务只能给一个服务器执行,执行的是整个任务,任务不能分片。quartz集群是可以支持故障转移的,如果一个服务器发生了故障那么这个任务给转给另一个服务器去执行。


三、XXL-JOB学习

写在前面:
如果单纯的去学习如何使用XXL-JOB是非常简单的,因为这个定时任务框架的开发初心就是简化开发复杂度。XXL-JOB分为两大模块,一个是调度中心,另一个是执行器。调度中心也就是xxl-job-admin,是个可视化的web页面,执行器简单理解就是我们的项目,我们实际开发中,可以对我们的项目进行简单的改造就可以让它变成一个执行器。调度中心通过发送指令来让执行器中的任务进行执行。
但是在学习工作或者面试中,我感觉XXL-JOB最值得学习的是它的源码,里面有很多的东西,包括自己用的xxl-rpc,以及很多其他的模块,我感觉这都很有学习的价值,而且面试肯定会问xxl-job的源码问题。
快速体验一下XXL-JOB
一、XXL-JOB的使用步骤

​ 1)、从GitHub或者gitee上下载项目的源码,然后在自己电脑上的编辑器中打开

​ 2)、在数据库中执行项目里面的数据库脚本

#
# XXL-JOB v2.4.0
# Copyright (c) 2015-present, xuxueli.

CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;

SET NAMES utf8mb4;

CREATE TABLE `xxl_job_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
  `job_desc` varchar(255) NOT NULL,
  `add_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `author` varchar(64) DEFAULT NULL COMMENT '作者',
  `alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
  `schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
  `schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
  `misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
  `executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
  `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
  `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
  `executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
  `executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
  `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
  `glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
  `glue_source` mediumtext COMMENT 'GLUE源代码',
  `glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
  `glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
  `child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
  `trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
  `trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
  `trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `job_group` int(11) NOT NULL COMMENT '执行器主键ID',
  `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
  `executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
  `executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
  `executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
  `executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
  `executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
  `trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
  `trigger_code` int(11) NOT NULL COMMENT '调度-结果',
  `trigger_msg` text COMMENT '调度-日志',
  `handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
  `handle_code` int(11) NOT NULL COMMENT '执行-状态',
  `handle_msg` text COMMENT '执行-日志',
  `alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
  PRIMARY KEY (`id`),
  KEY `I_trigger_time` (`trigger_time`),
  KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_log_report` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
  `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
  `suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
  `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_logglue` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `job_id` int(11) NOT NULL COMMENT '任务,主键ID',
  `glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
  `glue_source` mediumtext COMMENT 'GLUE源代码',
  `glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
  `add_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_registry` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `registry_group` varchar(50) NOT NULL,
  `registry_key` varchar(255) NOT NULL,
  `registry_value` varchar(255) NOT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_group` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
  `title` varchar(12) NOT NULL COMMENT '执行器名称',
  `address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
  `address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '账号',
  `password` varchar(50) NOT NULL COMMENT '密码',
  `role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
  `permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
  PRIMARY KEY (`id`),
  UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `xxl_job_lock` (
  `lock_name` varchar(50) NOT NULL COMMENT '锁名称',
  PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' );
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');

commit;


​ 3)、配置xxl-job-admin这个项目中的配置文件中的数据库连接信息

​ 4)、启动xxl-job-admin这个项目,会在8080端口上启动这个调度中心,我们在浏览器中访问http://localhost:8080/xxl-job-admin

	用户名是admin,密码是123456

​ 5)、然后创建一个spring boot项目作为执行器,也可以将自己的boot项目改造成执行器,这里我就创建了boot项目作为一个执行器

​ 6)、引入如下依赖

        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
            <version>2.4.0</version>
        </dependency>

​ 7)、修改配置信息

spring.application.name=xxljob-study
# 这个项目的web端口
server.port=8081
# no web
#spring.main.web-environment=false

# log日志的配置信息位置
logging.config=classpath:logback.xml


### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin

### xxl-job, access token
xxl.job.accessToken=default_token

### xxl-job executor appname
xxl.job.executor.appname=xxljob-study
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
xxl.job.executor.ip=
#默认执行器的端口,如果是单机多个服务的话,记得修改这个端口号,因为单机的话ip地址是一样的会发生冲突
xxl.job.executor.port=9999
### xxl-job executor log-path
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30

​ 8)、在resources下创建日志的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">

    <contextName>logback</contextName>
    <property name="log.path" value="/data/applogs/xxl-job/xxl-job-executor-sample-springboot.log"/>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>

</configuration>

​ 9)、创建配置类

@Configuration
public class XxlJobConfig {
    private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;

    @Value("${xxl.job.accessToken}")
    private String accessToken;

    @Value("${xxl.job.executor.appname}")
    private String appname;

    @Value("${xxl.job.executor.address}")
    private String address;

    @Value("${xxl.job.executor.ip}")
    private String ip;

    @Value("${xxl.job.executor.port}")
    private int port;

    @Value("${xxl.job.executor.logpath}")
    private String logPath;

    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;


    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        logger.info(">>>>>>>>>>> xxl-job config init.");
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);

        return xxlJobSpringExecutor;
    }

}

​ 10)、创建自己的任务类

@Component
@Slf4j
public class MyJob {
    //value里面写的是这个JobHandler的名字,这个名字需要在调度中心中使用
    @XxlJob(value = "myJob")
    public ReturnT execute() {
        log.info("Hello XXL-JOB");
        return ReturnT.SUCCESS;
    }
}

​ 11)、启动控制台和这个执行器

​ 12)、在控制台中添加一个执行器,appName就是我们在执行器项目中的配置文件中配置的那个

​ 名称我们可以自己起一个,注册方式使用自动注册,就好了

​ 13)、在任务管理中创建一个任务,执行器选择我们刚才创建的执行器,任务描述可以自定义,负责人可以自定义,调度类型一般就是CRON,然后写一个cron表达式,JobHandler要和我们自己写的job类中的@XxlJob中的value值对应起来

二、介绍一下XXL-JOB中的路由策略
1、第一个:顾名思义就是把任务分配给第一个服务器
2、最后一个:顾名思义就是把任务分配给最后一个服务器
3、轮询:就是所有的服务器轮流执行
4、随机:就是随机将任务分配给一个服务器
5、一致性Hash:根据一致性hash算法,一个任务分配给了一个服务器之后以后如果在服务器数量不变的情况下,都会给这个服务器
三、父子任务

我们可以创建父任务和子任务,一个父任务可以有多个子任务,每个父任务执行的时候会自动去调用子任务,即使子任务的调度时间没有到,但是反过来子任务调度的时候不会去执行父任务。

下面附上job的实现代码

@Slf4j
@Component
public class MyJob {

    @XxlJob(value = "parentJob")
    public ReturnT parentJob(){
        log.info("父任务执行了 。。。。");
        return ReturnT.SUCCESS;
    }

    @XxlJob(value = "childJob")
    public ReturnT childJob(){
        log.info("子任务执行了 。。。。");
        return ReturnT.SUCCESS;
    }

}

​ 我们需要在控制台中配置子任务的id,如果父任务中有多个子任务,子任务之间用逗号分隔

四、从调度器中接受参数

这个功能就是我们的执行器可以从调度器中接收传递过来的参数,然后根据这个参数进行对应的业务处理。

  @XxlJob(value = "paramJob")
    public ReturnT paramJob(){
        LocalTime now = LocalTime.now();
        String nowString = now.toString();
        //从调度器中接收参数
        String data = XxlJobHelper.getJobParam();
        if(!data.isEmpty()){
            nowString=data;
        }
        log.info("此时的时间为:"+nowString);
        return ReturnT.SUCCESS;
    }

然后我们在控制台中执行任务的时候我们可以设置参数,这样的话我们的执行器就可以接收到调度器传过来的参数了。

五、分片任务

XXL-JOB中有和elastic-job一样的分片功能,将一个任务进行分片,这样可以将一个复杂的任务分成很多小片,然后把分片给不同的服务器去执行,可以提高执行速度,也可以减轻一台服务器的压力。

使用这个功能我们只需要在控制台配置任务的时候将路由策略选择分片广播就可以了。

六、日志回调

前面我们说了执行器可以从调度器中接收参数,那么我们执行器可以给调度器传递参数吗?显然是可以的,比如日志回调。

我们执行器可以给调度器传递日志信息,然后就可以在控制台的调度日志中看到执行日志了。

下面附上任务的代码

    @XxlJob(value = "logJob")
    public ReturnT logJob() throws InterruptedException {
        XxlJobHelper.log("此时正在处理第{}行",156);
        Thread.sleep(3000);
        XxlJobHelper.log("此时正在处理第{}行",166);
        Thread.sleep(3000);
        XxlJobHelper.log("任务完成");
        return ReturnT.SUCCESS;
    }
七、任务的生命周期

前面我们的@Xxljob注解我们只使用了value这个属性,其实这个注解中有三个属性,分别是value、init、destroy

value:对应的就是jobhandler name

init:对应的就是jobHandler启动之前的调用哪个初始化方法,一般进行一个预处理

destroy:对应的就是jobHandler销毁的时候调用哪个结束方法,一般对应善后处理

下面附上代码

    @XxlJob(value = "lifeCycleJob",init = "init",destroy = "destory")
    public ReturnT lifeCycleJob() {
        System.out.println("任务启动");
        return ReturnT.SUCCESS;
    }

    public void init(){
        System.out.println("job执行前进行预处理...");
    }

    public void destory(){
        System.out.println("job执行结束后进行善后处理...");
    }
至此,这篇文章终于完整写完了。写于2023年4月18日22:00:09
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值