Quartz任务调度(详细)

文章目录
一、Quartz概念
1. 基本介绍
2. Quartz运行环境
3. Quartz核心概念
4. Quartz的体系结构
二、Quart的使用
1、引入Quartz的jar包
2、入门案例
3、Job和JobDetail详解
4、JobExecutionContext
5、JobDataMap介绍
(1)使用Map获取
(2)使用 Setter 方法获取
6、有状态的Job和无状态的Job(@PersistJobDataAfterExecution)
7、Trigger
(1)SimpleTrigger触发器
(2)CronTrigger触发器
8、 SchedulerFactory
(1)StdSchedulerFactory
(2)DirectSchedulerFactory(了解)
9、Scheduler 常用方法
10、 Quartz.properties
三、Quartz监听器
1、概念
2、JobListener
3、TriggerListener
4、SchedulerListener
四、持久化到Mysql中
1. 下载sql文件
2. 引入依赖
3. 配置SchedulerFactory
4. 使用自定义的Scheduler
5. 查看数据库
6. 再次启动 Scheduler
一、Quartz概念
1. 基本介绍
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合,也可以单独使用。

Quartz是开源且具有丰富特性的“任务调度库”,能够集成于任何的Java应用,小到独立的应用,大至电子商业系统。Quartz能够创建亦简单亦复杂的调度,以执行上十、上百,甚至上万的任务。任务job被定义为标准的Java组件,能够执行任何你想要实现的功能。Quartz调度框架包含许多企业级的特性,如JTA事务、集群的支持。

简而言之,Quartz就是基于Java实现的任务调度框架,用于执行你想要执行的任何任务。

官方网址:http://www.quartz-scheduler.org/
官方文档:http://www.quartz-scheduler.org/documentation/
原码地址:https://github.com/quartz-scheduler/quartz

2. Quartz运行环境
Quartz可以运行嵌入在另一个独立式应用程序
Quartz可以在应用程序服务器(或Servlet容器)内被实例化,并且参与事务
Quartz可以作为一个独立的程序运行(其自己的Java虚拟机内),可以通过RMI使用
Quartz可以被实例化,作为独立的项目集群(负载平衡和故障转移功能),用于作业的执行
3. Quartz核心概念
任务 Job

Job 就是你想要实现的任务类,每一个 Job 必须实现 org.quartz.job 接口,且只需实现接口定义的 execute() 方法。

触发器 Trigger

Trigger 为你执行任务的触发器,比如你想每天定时3点发送一份统计邮件,Trigger 将会设置3点执行该任务。
Trigger 主要包含两种 SimplerTrigger 和 CronTrigger 两种。详见 7.9 与 7.10

调度器 Scheduler

Scheduler 为任务的调度器,它会将任务 Job 及触发器 Trigger 整合起来,负责基于 Trigger 设定的时间来执行 Job。

4. Quartz的体系结构


二、Quart的使用
1、引入Quartz的jar包
创建一个 springboot(版本:2.2.4.RELEASE) 应用,直接引入依赖即可

<dependencies>
    <!-- Quartz 核心包 -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
    </dependency>

    <!-- Quartz 工具包 -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz-jobs</artifactId>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
2、入门案例
(1)创建HelloJob任务类

// 定义任务类
public class HelloJob implements Job {

    @Override
    public void execute(JobExecutionContext arg0) throws JobExecutionException {
        // 输出当前时间
        ystem.out.println(new Date());
    }
}
1
2
3
4
5
6
7
8
9
(2)创建任务调度类HelloSchedulerDemo

public class HelloSchedulerDemo {

    public static void main(String[] args) throws Exception {
        // 1、调度器(Scheduler),从工厂中获取调度的实例(默认:实例化new StdSchedulerFactory();)
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2、任务实例(JobDetail)定义一个任务调度实例,将该实例与HelloJob绑定,任务类需要实现Job接口
        JobDetail jobDetail = JobBuilder.newJob() // 加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
                .withIdentity("job1", "group1") // 参数1:任务的名称(唯一实例);参数2:任务组的名称
                .build();

        // 3、触发器(Trigger)定义触发器,马上执行,然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 参数1:触发器的名称(唯一实例);参数2:触发器组的名称
                .startNow() // 马上启动触发器
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever()) // 每2秒重复执行一次
                .build();

        // 4、让调度器关联任务和触发器,保证按照触发器定义的调整执行任务
        scheduler.scheduleJob(jobDetail, trigger);

        // 5、启动
        scheduler.start();
        // 关闭
        //scheduler.shutdown();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
3、Job和JobDetail详解
Job:工作任务调度的接口,任务需要实现该接口。该接口中定义execute方法,类似JDK提供的TimeTask类的run方法。在里面编写任务执行的业务逻辑。
Job实例在Quartz中的生命周期:每次调度器执行Job时,它在调用execute方法前会创建一个新的 Job 实例,当调用完成后,关联的Job对象实例会被释放,释放的实例会被垃圾回收机制回收。
JobDetail:JobDetail为Job实例提供了许多设置属性,以及JobDataMap成员变量属性,它用来存储特定Job实例的状态信息,调度器需要借助JobDetail对象来添加Job实例。
JobDetail重要属性:name、group、jobClass、JobDataMap
JobDetail job = JobBuilder.newJob(HelloJob.class)
        .withIdentity("job1", "group1") // 定义该实例唯一标识,并指定一个组
        .build();

System.out.println("name:" +job.getKey().getName());
System.out.println("group:" +job.getKey().getGroup());
System.out.println("jobClass:" +job.getJobClass().getName());
1
2
3
4
5
6
7
4、JobExecutionContext
当 Scheduler 调用一个 Job,就会将 JobExecutionContext 传递给 Job 的 execute() 方法;
Job 能通过 JobExecutionContext 对象访问到 Quartz 运行时候的环境以及 Job 本身的明细数据。
public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {

        Trigger trigger = jobExecutionContext.getTrigger(); //获取Trigger
        JobDetail jobDetail = jobExecutionContext.getJobDetail(); //获取JobDetail
        Scheduler scheduler = jobExecutionContext.getScheduler(); //获取Scheduler

        trigger.getKey().getName(); //获取Trigger名字
        trigger.getKey().getGroup(); //获取Trigger组名(默认为 DEFAULT)

        jobExecutionContext.getScheduledFireTime(); //触发器触发的预定时间。
        jobExecutionContext.getFireTime(); //实际触发时间。例如,计划时间可能是 10:00:00,但如果调度程序太忙,实际触发时间可能是 10:00:03。
        jobExecutionContext.getPreviousFireTime(); //上次触发时间
        jobExecutionContext.getNextFireTime(); //下次触发时间

        System.out.println(new Date());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
5、JobDataMap介绍
(1)使用Map获取
在进行任务调度时,JobDataMap 存储在 JobExecutionContext 中,非常方便获取。
JobDataMap 可以用来装载任何可序列化的数据对象,当 Job 实例对象被执行时这些参数对象会传递给它。
JobDataMap 实现了 JDK 的 Map 接口,并且添加了非常方便的方法用来存取基本数据类型。
在定义 Trigger 或者 JobDetail 时,将 JobDataMap 传入,然后 Job 中便可以获取到 JobDataMap 中的参数

public class HelloScheduler {
    public static void main(String[] args) throws SchedulerException {
        //1. 调度器(Scheduler)
        Scheduler defaultScheduler = StdSchedulerFactory.getDefaultScheduler();
        
        JobDataMap jobDataMap2 = new JobDataMap();
        jobDataMap2.put("message", "JobDetailMessage");

        //2. 任务实例(JobDetail)
        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
                .withIdentity("job1", "jobGroup1")
                .usingJobData(jobDataMap2)
                .build();

        // 定义 JobDataMap
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("message", "TriggerMessage");

        //3. 触发器(Trigger)
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "triggerGroup1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever())
                .endAt(new Date(new Date().getTime() + 3000L))
                .usingJobData(jobDataMap) // 将 JobDataMap 放入 Trigger 中
                .build();

        defaultScheduler.scheduleJob(jobDetail, trigger);
        defaultScheduler.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
HelloJob.java

public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(jobExecutionContext.getTrigger().getJobDataMap().get("message")); //TriggerMessage
        System.out.println(jobExecutionContext.getJobDetail().getJobDataMap().get("message")); //JobDetailMessage

        System.out.println(jobExecutionContext.getMergedJobDataMap().get("message")); //TriggerMessage
        System.out.println(new Date());
    }
}
1
2
3
4
5
6
7
8
9
10
(2)使用 Setter 方法获取
Job实现类中添加setter方法对应JobDataMap的键值,Quartz框架默认的JobFactory实现类在初始化Job实例对象时会自动调用这些setter方法。

HelloScheduler 类和上面一样。

HelloJob.java:

@Data
public class HelloJob implements Job {

    private String message;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(message); //TriggerMessage
        System.out.println(new Date());
    }
}
1
2
3
4
5
6
7
8
9
10
11
注意:如果遇到同名的 key,Trigger 中 JobDataMap 的值会覆盖 JobDetail 中 JobDataMap 同名的 Key

6、有状态的Job和无状态的Job(@PersistJobDataAfterExecution)
有状态的 Job 可以理解为多次 Job 调用期间可以持有一些状态信息,这些状态信息存储在 JobDataMap 中,而默认的无状态 Job 每次调用时都会创建一个新的 JobDataMap。

(1)修改HelloSchedulerDemo.java。在 JobDetail 中添加 .usingJobData("count", 0),表示计数器。

JobDetail job = JobBuilder.newJob(HelloJob.class)
        .withIdentity("job1", "group1") // 定义该实例唯一标识,并指定一个组
        .usingJobData("message", "打印日志")
        .usingJobData("count", 0)
        .build();
1
2
3
4
5
(2)HelloJob.java

@Data
@PersistJobDataAfterExecution
public class HelloJob implements Job {

    private Integer count;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(++count);
        jobExecutionContext.getJobDetail().getJobDataMap().put("count", count);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
HelloJob类没有添加 @PersistJobDataAfterExecution 注解,每次调用时都会创建一个新的 JobDataMap。不会累加。

HelloJob类添加 @PersistJobDataAfterExecution 注解,多次调用期间可以持有一些状态信息,即可以实现 count 的累加。

7、Trigger


(1)SimpleTrigger触发器
SimpleTrigger 对于设置和使用是最为简单的一种QuartzTrigger。

它是为那种需要在特定的日期/时间启动,且以一个可能的间隔时间重复执行 n 次的 Job 所设计的。

案例一:表示在一个指定的时间段内,执行一次作业任务;

SimpleTrigger 常用方法:

方法    说明
startNow()    Scheduler 开始执行时,触发器也即执行
startAt(new Date())    在指定的时间开始执行
withIntervalInSeconds(2)    执行间隔,方法名中对应时间单位
repeatForever()    一直重复执行
withRepeatCount(3)    重复执行指定的次数
endAt(new Date())    结束时间
例子:

//立即开始执行,2秒执行一次,重复3次,3秒后结束执行(当重复次数或者结束时间有一个先达到时,就会停止执行)
Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "triggerGroup1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).withRepeatCount(3))
                .endAt(new Date(new Date().getTime() + 3000L))
                .build();
1
2
3
4
5
6
7
需要注意的点

SimpleTrigger的属性有:开始时间、结束时间、重复次数和重复的时间间隔。
重复次数属性的值可以为0、正整数、或常量 SimpleTrigger.REPEAT_INDEFINITELY。
重复的时间间隔属性值必须为大于0或者长整形的正整数,以毫秒作为时间单位,当重复的时间间隔为0时,意味着与Trigger同时触发执行。
如果有指定结束时间属性值,则结束时间属性优先于重复次数属性,这样的好处在于:当我们需要创建一个每间隔10秒触发一次直到指定的结束时间的Trigger,而无需去计算从开始到结束的所重复的次数,我们只需简单的指定结束时间和使用REPEAT_INDEFINITELY作为重复次数的属性值即可。
(2)CronTrigger触发器
如果你需要像日历那样按日程来触发任务,而不是像 SimpleTrigger 那样每隔特定的间隔时间触发,CronTrigger 通常比 SimpleTrigger 更有用,因为它是基于日历的作业调度器。

cron 表达式:CRON表达式
在线生成cron:https://www.pppet.net/,或者直接百度 在线cron 也行
案例:

Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("trigger1", "group1")
        .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?"))  // 日历
        .build();
1
2
3
4
8、 SchedulerFactory
Quartz以模块方式架构,因此,要使它运行,几个组件必须很好的咬合在一起。幸运的是,已经有了一些现存的助手可以完成这些工作。

所有的Scheduler实例由SchedulerFactory创建。

Quartz的三个核心概念:调度器、任务、触发器,三者之间的关系是:

大家都知道,一个作业,比较重要的三个要素就是Scheduler,JobDetail,Trigger;而Trigger对于Job而言就好比一个驱动器,没有触发器来定时驱动作业,作业就无法运行;对于Job而言,一个Job可以对应多个Trigger,但对于Trigger而言,一个Trigger只能对应一个Job,所以一个Trigger只能被指派给一个Job;如果你需要一个更负责的触发计划,你可以创建多个Trigger并指派它们给同一个Job。

(1)StdSchedulerFactory
Quartz 默认的 SchedulerFactory

使用一组参数(java.util.Properties)来创建和初始化Quartz调度器
配置参数一般存储在 quartz.properties 文件中
调用 getScheduler方法就能创建和初始化调度器对象
创建方法:

//静态方法
Scheduler defaultScheduler = StdSchedulerFactory.getDefaultScheduler();

//实例方法
StdSchedulerFactory stdSchedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = stdSchedulerFactory.getScheduler();
1
2
3
4
5
6
(2)DirectSchedulerFactory(了解)
DirectSchedulerFactory 是对 SchedulerFactory 的直接实现,通过它可以直接构建 Scheduler、ThreadPool 等

DirectSchedulerFactory directSchedulerFactory = DirectSchedulerFactory.getInstance();
Scheduler scheduler = directSchedulerFactory.getScheduler();
1
2
9、Scheduler 常用方法
scheduler.scheduleJob(jobDetail, trigger); //绑定jobDetail与trigger
scheduler.checkExists(JobKey.jobKey(name, group))    //检查JobDetail是否存在
scheduler.checkExists(TriggerKey.triggerKey(name, group))    //检查Trigger是否存在
scheduler.deleteJob(JobKey.jobKey(name, group))        //删除jobDetail

scheduler.triggerJob(JobKey.jobKey(name, group), dataMap)    //立即执行一次指定的任务

scheduler.start();        //启动任务调度
scheduler.pauseJob(jobKey);    //暂停指定的job
scheduler.standby();    //任务调度挂起,即暂停操作
scheduler.shutdown();    //关闭任务调度,同shutdown(false)
scheduler.shutdown(true);    //表示等待所有正在执行的Job执行完毕之后,再关闭Scheduler
scheduler.shutdown(false);    // 表示直接关闭Scheduler
1
2
3
4
5
6
7
8
9
10
11
12
13
10、 Quartz.properties
默认路径:quartz-2.3.2 中的 org.quartz.quartz.properties

我们也可以在项目的资源下添加 quartz.properties 文件,去覆盖底层的配置文件。

#===============================================================
#Configure Main Scheduler Properties 调度器属性
#===============================================================
#调度器的实例名
org.quartz.scheduler.instanceName = QuartzScheduler
#调度器的实例ID,大多数情况设置为AUTO即可
org.quartz.scheduler.instanceId = AUTO

#===============================================================
#Configure ThreadPool 线程池属性
#===============================================================
#处理Job的线程个数,至少为1,但最多的话最好不要超过100,在多数机器上设置该值超过100的话显得相当不实用了,特别是在你的Job执行时间较长的情况下
org.quartz.threadPool.threadCount =  5
#线程的优先级,优先级别搞的线程比优先级别低的线程优先得到执行。最小为1,最大为10,默认为5
org.quartz.threadPool.threadPriority = 5
#一个实现了org.quartz.spi.threadPool接口的类,Quartz自带的线程池实现类是org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool

#===============================================================
#Configure JobStore 作业存储设置
#===============================================================
#Job默认是存储在内存中的,即下面配置
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

#===============================================================
#Configure Plugins 插件配置
#===============================================================
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin

org.quartz.plugin.jobInitializer.overWriteExistingJobs = true
org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.jobInitializer.validating=false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
也可以编写程序代码操作quartz.properties文件的内容:

// 创建工厂实例
StdSchedulerFactory schedulerFactory = new StdSchedulerFactory();
// 创建配置工厂的属性的对象
Properties prop = new Properties();
prop.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "5");

try {
    // 加载上面定义的属性
    schedulerFactory.initialize(prop);

    Scheduler scheduler = schedulerFactory.getScheduler();

    scheduler.start();
} catch (SchedulerException e) {
    e.printStackTrace();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
三、Quartz监听器
1、概念
Quartz的监听器用于当任务调度中你所关注事件发生时,能够及时获取这一事件的通知。类似于任务执行过程中的邮件、短信类的提醒。Quartz监听器主要由JobListener、TriggerListener、SchedulerListener三种,顾名思义,分布表示任务、触发器、调度器对应的监听器。三者的使用方法类似,在开始介绍三种监听器之前,需要明确两个概念:全局监听器与非全局监听器,二者的区别在于:

全局监听器能够接收到所有的Job/Trigger的事件通知
而非全局监听器只能接收到在其上注册的Job或者Trigger的事件,不在其上注册的Job或Trigger则不会进行监听。
本课程关于全局与非全局的监听器的使用,将一一介绍。

2、JobListener
任务调度过程中,与任务Job相关的事件包括:Job开始要执行的提示;Job执行完成的提示等。

public interface JobListener {
    public String getName();
    public void jobToBeExecuted(JobExecutionContext context);
    public void jobExecutionVetoed(JobExecutionContext context);
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException);
}
1
2
3
4
5
6
其中:

. getName方法:用于获取该JobListener的名称。
. jobToBeExecuted方法:Scheduler在JobDetail将要被执行时调用这个方法。
. jobExecutionVetoed方法:Scheduler在JobDetail即将被执行,但又被TriggerListener否决时会调用该方法。
. jobWasExecuted方法:Scheduler在JobDetail被执行之后调用这个方法。

示例:

HelloJobListener.java

// 定义任务类
public class HelloJobListener implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 输出当前时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateString = dateFormat.format(date);
        // 工作内容
        System.out.println("正在进行数据库的备份工作,备份数据库的时间是:" +dateString);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
创建自定义的JobListener

MyJobListener.java

public class MyJobListener implements JobListener {

    @Override
    public String getName() {
        String name = this.getClass().getSimpleName();
        System.out.println("监听器的名称是:" +name);
        return name;
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        String name = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:" +name + "          Scheduler在JobDetail将要被执行时调用的方法");
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        String name = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:" +name + "          Scheduler在JobDetail即将被执行,但又被TriggerListener否决时会调用该方法");
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        String name = context.getJobDetail().getKey().getName();
        System.out.println("Job的名称是:" +name + "          Scheduler在JobDetail被执行之后调用这个方法");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
执行调度器

HelloSchedulerDemoJobListener.java

public class HelloSchedulerDemoJobListener {

    public static void main(String[] args) throws Exception {
        // 1、调度器(Scheduler),从工厂中获取调度的实例(默认:实例化new StdSchedulerFactory();)
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2、任务实例(JobDetail)定义一个任务调度实例,将该实例与HelloJobSimpleTrigger绑定,任务类需要实现Job接口
        JobDetail jobDetail = JobBuilder.newJob(HelloJobListener.class) // 加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
                .withIdentity("job1", "group1") // 参数1:任务的名称(唯一实例);参数2:任务组的名称
                .build();

        // 3、触发器(Trigger)定义触发器,马上执行,然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 参数1:触发器的名称(唯一实例);参数2:触发器组的名称
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5).withRepeatCount(2))  // 每5秒执行一次,连续执行3次后停止,默认是0
                .build();
        // 4、让调度器关联任务和触发器,保证按照触发器定义的调整执行任务
        scheduler.scheduleJob(jobDetail, trigger);
        
        // 创建并注册一个全局的Job Listener
        // scheduler.getListenerManager().addJobListener(new MyJobListener(), EverythingMatcher.allJobs());
        // 创建并注册一个局部的Job Listener,表示指定的任务Job
        scheduler.getListenerManager().addJobListener(new MyJobListener(), KeyMatcher.keyEquals(JobKey.jobKey("job1", "group1")));

        // 5、启动
        scheduler.start();
        // 关闭
        //scheduler.shutdown();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
3、TriggerListener
任务调度过程中,与触发器Trigger相关的事件包括:触发器触发、触发器未正确触发、触发器完成等。

public interface TriggerListener {
    public String getName();
    public void triggerFired(Trigger trigger, JobExecutionContext context);
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context);
    public void triggerMisfired(Trigger trigger);
    public void triggerComplete(Trigger trigger, JobExecutionContext context,            CompletedExecutionInstruction triggerInstructionCode)
}
1
2
3
4
5
6
7
其中:

. getName方法:用于获取触发器的名称。
. triggerFired方法:当与监听器关联的Trigger被触发,Job上的Execute()方法将被执行时,Scheduler就调用该方法。
. vetoJobExecution方法:在Trigger触发后,Job将要执行时由Scheduler调用这个方法。TriggerListener给了一个选择去否决Job的执行。假如这个方法返回true,这个Job将不会为此次Trigger触发而得到执行。
. triggerMisfired方法:Scheduler调用这个方法是在Trigger错过触发时。你应该关注此方法中持续时间长的逻辑:在出现许多错过触发的Trigger时,长逻辑会导致骨牌效应。你应当保持这个方法尽量的小。
. triggerComplete方法:Trigger被触发并且完成了Job的执行时,Scheduler调用这个方法。

示例:

下面的例子简单展示了TriggerListener的使用,其中创建并注册TriggerListener与JobListener几乎类似。

HelloJobListener.java

// 定义任务类
public class HelloJobListener implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 输出当前时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateString = dateFormat.format(date);
        // 工作内容
        System.out.println("正在进行数据库的备份工作,备份数据库的时间是:" +dateString);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
MyTriggerListener.java

public class MyTriggerListener implements TriggerListener {
    
    private String name;
    // 构造方法,自定义传递触发器的名称,默认是类的名称
    public MyTriggerListener(String name) {
        super();
        this.name = name;
    }
    @Override
    public String getName() {
        return this.name;  // 不返还会抛出一个名称为空的异常
    }

//    @Override
//    public String getName() {
//        String name = this.getClass().getSimpleName();
//        System.out.println("触发器的名称:" +name);
//        return name;  // 不返还会抛出一个名称为空的异常
//    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        String name = this.getClass().getSimpleName();
        System.out.println(name +"被触发");
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        String name = this.getClass().getSimpleName();
        // TriggerListener给了一个选择去否决Job的执行。假如这个方法返回true,这个Job将不会为此次Trigger触发而得到执行。
        System.out.println(name +" 没有被触发");
        return false;  // true:表示不会执行Job的方法
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        String name = this.getClass().getSimpleName();
        // Scheduler调用这个方法是在Trigger错过触发时
        System.out.println(name +" 错过触发");
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context,
            CompletedExecutionInstruction triggerInstructionCode) {
        String name = this.getClass().getSimpleName();
        // Trigger被触发并且完成了Job的执行时,Scheduler调用这个方法。
        System.out.println(name +" 完成之后触发");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
HelloSchedulerDemoTriggerListener.java

public class HelloSchedulerDemoTriggerListener {

    public static void main(String[] args) throws Exception {
        // 1、调度器(Scheduler),从工厂中获取调度的实例(默认:实例化new StdSchedulerFactory();)
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2、任务实例(JobDetail)定义一个任务调度实例,将该实例与HelloJobSimpleTrigger绑定,任务类需要实现Job接口
        JobDetail jobDetail = JobBuilder.newJob(HelloJobListener.class) // 加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
                .withIdentity("job1", "group1") // 参数1:任务的名称(唯一实例);参数2:任务组的名称
                .build();

        // 3、触发器(Trigger)定义触发器,马上执行,然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 参数1:触发器的名称(唯一实例);参数2:触发器组的名称
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5).withRepeatCount(2))  // 每5秒执行一次,连续执行3次后停止,默认是0
                .build();
        // 4、让调度器关联任务和触发器,保证按照触发器定义的调整执行任务
        scheduler.scheduleJob(jobDetail, trigger);

        // 创建并注册一个全局的Trigger Listener
        // scheduler.getListenerManager().addTriggerListener(new MyTriggerListener(), EverythingMatcher.allTriggers());
        // 创建并注册一个局部的Trigger Listener
        scheduler.getListenerManager().addTriggerListener(new MyTriggerListener(), KeyMatcher.keyEquals(TriggerKey.triggerKey("trigger1", "group1")));

        // 5、启动
        scheduler.start();
        // 关闭
        //scheduler.shutdown();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
4、SchedulerListener
SchedulerListener会在Scheduler的生命周期中关键事件发生时被调用。与Scheduler有关的事件包括:增加一个Job/Trigger,删除一个Job/Trigger,Scheduler发生严重错误,关闭Scheduler等。

public interface SchedulerListener {
    public void jobScheduled(Trigger trigger);
    public void jobUnscheduled(TriggerKey triggerKey);
    public void triggerFinalized(Trigger trigger);
    public void triggersPaused(String triggerGroup);
    public void triggersResumed(String triggerGroup);
    public void jobsPaused(String jobGroup);
    public void jobsResumed(String jobGroup);
    public void schedulerError(String msg, SchedulerException cause);
    public void schedulerStarted();
    public void schedulerInStandbyMode();
    public void schedulerShutdown();
    public void schedulingDataCleared()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
其中:

. jobScheduled方法:用于部署JobDetail时调用。
. jobUnscheduled方法:用于卸载JobDetail时调用。
. triggerFinalized方法:当一个Trigger来到了再也不会触发的状态时调用这个方法。除非这个Job已设置成了持久性,否则它就会从Scheduler中移除。
. triggersPaused方法:Scheduler调用这个方法是发生在一个Trigger或Trigger组被暂停时。假如是Trigger组的话,triggerName参数将为null。
. triggersResumed方法:Scheduler调用这个方法是发生在一个Trigger或Trigger组从暂停中恢复时。假如是Trigger组的话,triggerName参数将为null。
. jobsPaused方法:当一个或一组JobDetail暂停时调用这个方法。
. jobsResumed方法:当一个或一组Job从暂停上恢复时调用这个方法。假如是一个Job组,jobName将为null。
. schedulerError方法:在Scheduler的正常运行期间产生一个严重错误时调用这个方法。
. schedulerStarted方法:当Scheduler开启时,调用该方法。
. schedulerInStandbyMode方法:当Scheduler处于StandBy模式时,调用该方法。
. schedulerShutdown方法:当Scheduler停止时,调用该方法。
. schedulingDataCleared方法:当Scheduler中的数据被清除时,调用该方法。

示例:

下面的代码简单描述了如何使用SchedulerListener方法:

HelloJobListener.java

// 定义任务类
public class HelloJobListener implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        // 输出当前时间
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateString = dateFormat.format(date);
        // 工作内容
        System.out.println("正在进行数据库的备份工作,备份数据库的时间是:" +dateString);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
MySchedulerListener.java

public class MySchedulerListener implements SchedulerListener {

    @Override
    public void jobScheduled(Trigger trigger) {
        String name = trigger.getKey().getName();
        // 用于部署JobDetail时调用
        System.out.println(name +" 完成部署");
    }

    @Override
    public void jobUnscheduled(TriggerKey triggerKey) {
        String name = triggerKey.getName();
        // 用于卸载JobDetail时调用
        System.out.println(name +" 完成卸载");
    }

    @Override
    public void triggerFinalized(Trigger trigger) {
        String name = trigger.getKey().getName();
        // 当一个Trigger来到了再也不会触发的状态时调用这个方法。除非这个Job已设置成了持久性,否则它就会从Scheduler中移除。
        System.out.println(name +" 触发器被移除");
    }

    @Override
    public void triggerPaused(TriggerKey triggerKey) {
        String name = triggerKey.getName();
        // Scheduler调用这个方法是发生在一个Trigger或Trigger组被暂停时。假如是Trigger组的话,triggerName参数将为null。
        System.out.println(name +" 正在被暂停");
    }

    @Override
    public void triggersPaused(String triggerGroup) {
        // Scheduler调用这个方法是发生在一个Trigger或Trigger组被暂停时。假如是Trigger组的话,triggerName参数将为null。
        System.out.println("触发器组" +triggerGroup +" 正在被暂停");
    }

    @Override
    public void triggerResumed(TriggerKey triggerKey) {
        // Scheduler调用这个方法是发生在一个Trigger或Trigger组从暂停中恢复时。假如是Trigger组的话,triggerName参数将为null。参数将为null。
        String name = triggerKey.getName();
        System.out.println(name +" 正在从暂停中恢复");
    }

    @Override
    public void triggersResumed(String triggerGroup) {
        // Scheduler调用这个方法是发生在一个Trigger或Trigger组从暂停中恢复时。假如是Trigger组的话,triggerName参数将为null。参数将为null。
        System.out.println("触发器组" +triggerGroup +" 正在从暂停中恢复");
    }

    @Override
    public void jobAdded(JobDetail jobDetail) {
        // 
        System.out.println(jobDetail.getKey() +" 添加工作任务");
    }

    @Override
    public void jobDeleted(JobKey jobKey) {
        // 
        System.out.println(jobKey +" 删除工作任务");
    }

    @Override
    public void jobPaused(JobKey jobKey) {
        // 
        System.out.println(jobKey +" 工作任务正在被暂停");
    }

    @Override
    public void jobsPaused(String jobGroup) {
        // 
        System.out.println("工作组" +jobGroup +" 正在被暂停");
    }

    @Override
    public void jobResumed(JobKey jobKey) {
        // 
        System.out.println(jobKey +" 正在从暂停中恢复");
    }

    @Override
    public void jobsResumed(String jobGroup) {
        // 
        System.out.println("工作组" +jobGroup +" 正在从暂停中恢复");
    }

    @Override
    public void schedulerError(String msg, SchedulerException cause) {
        // 在Scheduler的正常运行期间产生一个严重错误时调用这个方法。
        System.out.println("产生严重错误的时候调用" +msg +"    " +cause.getUnderlyingException());
    }

    @Override
    public void schedulerInStandbyMode() {
        // 当Scheduler处于StandBy模式时,调用该方法。
        System.out.println("调度器被挂起模式的时候调用");
    }

    @Override
    public void schedulerStarted() {
        // 当Scheduler开启时,调用该方法
        System.out.println("调度器开启的时候调用");
    }

    @Override
    public void schedulerStarting() {
        // 
        System.out.println("调度器正在开启的时候调用");
    }

    @Override
    public void schedulerShutdown() {
        // 
        System.out.println("调度器关闭的时候调用");
    }

    @Override
    public void schedulerShuttingdown() {
        // 
        System.out.println("调度器正在关闭的时候调用");
    }

    @Override
    public void schedulingDataCleared() {
        // 当Scheduler中的数据被清除时,调用该方法
        System.out.println("调度器数据被清除的时候调用");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
HelloSchedulerDemoTriggerListener.java

public class HelloSchedulerDemoTriggerListener {

    public static void main(String[] args) throws Exception {
        // 1、调度器(Scheduler),从工厂中获取调度的实例(默认:实例化new StdSchedulerFactory();)
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

        // 2、任务实例(JobDetail)定义一个任务调度实例,将该实例与HelloJobSimpleTrigger绑定,任务类需要实现Job接口
        JobDetail jobDetail = JobBuilder.newJob(HelloJobListener.class) // 加载任务类,与HelloJob完成绑定,要求HelloJob实现Job接口
                .withIdentity("job1", "group1") // 参数1:任务的名称(唯一实例);参数2:任务组的名称
                .build();

        // 3、触发器(Trigger)定义触发器,马上执行,然后每5秒重复执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "group1") // 参数1:触发器的名称(唯一实例);参数2:触发器组的名称
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().repeatSecondlyForever(5).withRepeatCount(2))  // 每5秒执行一次,连续执行3次后停止,默认是0
                .build();
        // 4、让调度器关联任务和触发器,保证按照触发器定义的调整执行任务
        scheduler.scheduleJob(jobDetail, trigger);

        // 创建调度器的监听
        scheduler.getListenerManager().addSchedulerListener(new MySchedulerListener());
        // 移除对应的调度器的监听
        // scheduler.getListenerManager().removeSchedulerListener(new MySchedulerListener());

        // 5、启动
        scheduler.start();

        // 线程延迟7秒后关闭
        Thread.sleep(7000L);

        // 关闭
        scheduler.shutdown();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
四、持久化到Mysql中
1. 下载sql文件
Quartz 原码中有 sql 文件:

https://github.com/quartz-scheduler/quartz/blob/v2.3.2/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore/tables_mysql.sql
另一个地址:https://hub.fastgit.org/quartz-scheduler/quartz/tree/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore
下载然后导入到数据库中,我这里使用的是 mysql5.7

表名    描述
QRTZ_BLOB_TRIGGERS    作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候)
QRTZ_CALENDARS    以 Blob 类型存储 Quartz 的 Calendar 信息
QRTZ_CRON_TRIGGERS    存储 Cron Trigger,包括 Cron 表达式和时区信息
QRTZ_FIRED_TRIGGERS    存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息
QRTZ_JOB_DETAILS    存储每一个已配置的 Job 的详细信息
QRTZ_LOCKS    存储程序的非观锁的信息(假如使用了悲观锁)
QRTZ_PAUSED_TRIGGER_GRPS    存储已暂停的 Trigger 组的信息
QRTZ_SCHEDULER_STATE    存储少量的有关 Scheduler 的状态信息,和别的 Scheduler 实例(假如是用于一个集群中)
QRTZ_SIMPLE_TRIGGERS    存储简单的 Trigger,包括重复次数,间隔,以及已触的次数
QRTZ_SIMPROP_TRIGGERS    
QRTZ_TRIGGERS    存储已配置的 Trigger 的信息
2. 引入依赖
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.zyx</groupId>
<artifactId>quartz</artifactId>
<version>0.0.1-SNAPSHOT</version>

<properties>
    <java.version>1.8</java.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <exclusions>
            <exclusion>
                <!--排除自带的JDBC连接池-->
                <groupId>com.mchange</groupId>
                <artifactId>c3p0</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz-jobs</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!--定时任务需要依赖context模块-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
    </dependency>

    <!--连接数据库-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--下面的包可以替换为 mybatis 或者 mybatisPlus-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
3. 配置SchedulerFactory
配置数据源:

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://10.211.55.12:3306/test
    driver-class-name: com.mysql.cj.jdbc.Driver
1
2
3
4
5
6
配置 SchedulerFactory:

@Configuration
public class ScheduleConfig {

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource)
    {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setDataSource(dataSource);

        // quartz参数
        Properties prop = new Properties();
        prop.put("org.quartz.scheduler.instanceName", "ZyxScheduler");
        prop.put("org.quartz.scheduler.instanceId", "AUTO"); //如果使用集群,instanceId必须唯一,设置成AUTO
        // 线程池配置
        prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
        prop.put("org.quartz.threadPool.threadCount", "20"); //线程数
        prop.put("org.quartz.threadPool.threadPriority", "5"); //优先级
        // JobStore配置
        prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); //配置使用数据库
        // 集群配置
        prop.put("org.quartz.jobStore.isClustered", "true"); //是否是集群模式
        prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
        prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
        prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true");

        // sqlserver 启用
        // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");
        prop.put("org.quartz.jobStore.misfireThreshold", "12000");
        prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); //数据库表前缀
        factory.setQuartzProperties(prop);

        factory.setSchedulerName("ZyxScheduler");
        // 延时启动
        factory.setStartupDelay(1);
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        // 可选,QuartzScheduler
        // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了
        factory.setOverwriteExistingJobs(true);
        // 设置自动启动,默认为true
        factory.setAutoStartup(true);

        return factory;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
4. 使用自定义的Scheduler
定义一个简单的 Job:

@Data
public class HelloJob implements Job {
    
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(new Date());
    }
}
1
2
3
4
5
6
7
8
使用自定义的 Scheduler:

@SpringBootTest
class QuartzApplicationTests {

    //将上面配置好的 factoryBean 注入进来
    @Autowired
    private SchedulerFactoryBean factoryBean;

    @Test
    void contextLoads() throws SchedulerException, InterruptedException {
        Scheduler scheduler = factoryBean.getScheduler();
        scheduler.clear();

        //2. 任务实例(JobDetail)
        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class)
                .withIdentity("job1", "jobGroup1")
                .build();

        //3. 触发器(Trigger)
        Trigger trigger = TriggerBuilder.newTrigger()
                .startNow()
                .withIdentity("trigger1", "triggerGroup1")
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2).repeatForever())
                .build();

        scheduler.scheduleJob(jobDetail, trigger);
        scheduler.start();
        Thread.sleep(100000);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
5. 查看数据库
查看数据库,可以发现 Quartz 中相关的数据已经保存到数据库中了

6. 再次启动 Scheduler
直接让程序运行起来,不创建新的定时任务,会发现刚才保存到数据库中的定时任务会自动执行

@SpringBootTest
class QuartzApplicationTests {

    @Autowired
    private SchedulerFactoryBean factoryBean;

    @Test
    void contextLoads() throws SchedulerException, InterruptedException {
        Thread.sleep(100000);
    }

}
————————————————
版权声明:本文为CSDN博主「一名小码农」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zyx1260168395/article/details/118760511

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值