Sppring集成Quartz简单案例详解 包括(添加、停止、恢复、删除任务、获取下次执行时间等)

文章目录

一、Quartz简介

1.Java主流三大定时任务框架优缺点

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.什么是Quartz?

在这里插入图片描述

3.模型图

在这里插入图片描述

4.名词解释

名词举例

  • Job
  • JobDetail
  • JobExecutionContext
  • JobDataMap
  • Trigger、SimpleTrigger、CronTrigger
  • QuartzSchedulerThread
  • ThreadPool
  • QuartzSchedulerResources
  • SchedulerFactory
  • JobStore
  • QuartzScheduler
  • Scheduler

关系图-自己理解

在这里插入图片描述
一个job可以被多个Trigger 绑定,但是一个Trigger只能绑定一个job!

(重点掌握) job

Job是Quartz中的一个接口,接口下只有execute方法,在这个方法中编写业务逻辑。
Job有个子接口StatefulJob ,代表有状态任务。有状态任务不可并发,前次任务没有执行完,后面任务处于阻塞等到。

(重点掌握) JobDetail

JobDetail用来绑定Job,为Job实例提供许多属性:【name/group/jobClass/jobDataMap】
JobDetail绑定指定的Job,每次Scheduler调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。
JobDetails将使用JobBuilder创建/定义。

问题思考:为什么设计成JobDetail + Job,不直接使用Job?

答案: JobDetail定义的是任务数据,而真正的执行逻辑是在Job中。
这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。而JobDetail & Job 方式,Sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以规避并发访问的问题。

(重点掌握) JobExecutionContext

JobExecutionContext中包含了Quartz运行时的环境以及Job本身的详细数据信息。
当Schedule调度执行一个Job的时候,就会将JobExecutionContext传递给该Job的execute()中,Job就可以通过JobExecutionContext对象获取信息。
主要信息有:
在这里插入图片描述

(重点掌握) JobDataMap

JobDataMap实现了JDK的Map接口,可以以Key-Value的形式存储数据。
JobDetail、Trigger都可以使用JobDataMap来设置一些参数或信息,同名后者会覆盖前者值。
Job执行execute()方法的时候,JobExecutionContext可以获取到JobExecutionContext中的信息。
如:

JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class)
		.usingJobData("jobDetail1", "这个Job用来测试的")
        .withIdentity("job1", "group1").build();

 Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
      .usingJobData("trigger1", "这是jobDetail1的trigger")
      .startNow()//立即生效
      .withSchedule(SimpleScheduleBuilder.simpleSchedule()
      .withIntervalInSeconds(1)//每隔1s执行一次
      .repeatForever()).build();//一直执行

Job执行的时候,可以获取到这些参数信息:

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println(jobExecutionContext.getJobDetail().getJobDataMap().get("jobDetail1"));
        System.out.println(jobExecutionContext.getTrigger().getJobDataMap().get("trigger1"));
        String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date());
        System.out.println("PrintWordsJob start at:" + printTime + ", prints: Hello Job-" + new Random().nextInt(100));
    }

(重点掌握) Trigger、SimpleTrigger、CronTrigger

Quartz 中五种类型的 Trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,NthIncludedDayTrigger和Calendar 类( org.quartz.Calendar)。
最常用的:
SimpleTrigger:用来触发只需执行一次或者在给定时间触发并且重复N次且每次执行延迟一定时间的任务。
CronTrigger:按照日历触发,例如“每个周五”,每个月10日中午或者10:15分。
Trigger是Quartz的触发器,会去通知Scheduler何时去执行对应Job。
new Trigger().startAt():表示触发器首次被触发的时间;
new Trigger().endAt():表示触发器结束触发的时间;
使用TriggerBuilder实例化实际触发器。

SimpleTrigger可以实现在一个指定时间段内执行一次作业任务或一个时间段内多次执行作业任务。

CronTrigger功能非常强大,是基于日历的作业调度,而SimpleTrigger是精准指定间隔,所以相比SimpleTrigger,CroTrigger更加常用。
注意:
* cron表达式6位:秒分时日月周
* cron表达式7位:秒分时日月周年

(了解) QuartzSchedulerThread

负责执行向QuartzScheduler注册的触发Trigger的工作的线程。

(了解) ThreadPool

Scheduler使用一个线程池作为任务运行的基础设施,任务通过共享线程池中的线程提供运行效率。

(了解) QuartzSchedulerResources

包含创建QuartzScheduler实例所需的所有资源(JobStore,ThreadPool等)。

(重点掌握) SchedulerFactory

提供用于获取调度程序实例的客户端可用句柄的机制。

(了解) JobStore

通过类实现的接口,这些类要为org.quartz.core.QuartzScheduler的使用提供一个org.quartz.Job和org.quartz.Trigger存储机制。作业和触发器的存储应该以其名称和组的组合为唯一性。

(了解) QuartzScheduler

这是Quartz的核心,它是org.quartz.Scheduler接口的间接实现,包含调度org.quartz.Jobs,注册org.quartz.JobListener实例等的方法。

(重点掌握) Scheduler

这是Quartz Scheduler的主要接口,代表一个独立运行容器。调度程序维护JobDetails和触发器的注册表。 一旦注册,调度程序负责执行作业,当他们的相关联的触发器触发(当他们的预定时间到达时)。

5.Quartz定时任务管理(动态添加、停止、恢复任务及输出下次执行时间、删除定时任务)

停止:scheduler.pauseJob(jobKey);
恢复:scheduler.resumeJob(jobKey);
删除:scheduler.deleteJob(jobKey);
添加:scheduler.scheduleJob(jobDetail,cronTrigger)
输出下次执行时间(前提是只有执行了resumeJob()后才能获取下次执行时间,其他方法无效) scheduler.getTriggersOfJob(jobKey).get(0).getFireTimeAfter(new Date()))

二.案例举例详细说明

使用quartz须引入

<!--quartz调度器-->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>

使用阿里线程池须引入

<!--引入commons-lang3,为了创建阿里线程池-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

PrintWordsJob代码

import lombok.SneakyThrows;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.TriggerKey;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;

/**
 * 新建一个能够打印任意内容的Job
 * @Author 211145187
 * @Date 2022/4/6 17:06
 **/
public class PrintWordsJob implements Job {

    @SneakyThrows
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "triggerGroup1");
        //获取任务状态
        System.out.println("任务状态:" + jobExecutionContext.getScheduler().getTriggerState(triggerKey));

        //获取job标识
        System.out.println("getJobKey():" + jobExecutionContext.getTrigger().getJobKey());  //输出job1,group1
        //获取trigger标识
        System.out.println("getKey():" + jobExecutionContext.getTrigger().getKey());    //输出trigger1,triggerGroup1

        //get(key)和getString(key)等效
        System.out.println("get(\"name1\"):" + jobExecutionContext.getMergedJobDataMap().get("name1"));
        System.out.println("getString(\"name2\"):" + jobExecutionContext.getMergedJobDataMap().getString("name2"));
        System.out.println("get(\"name1\"):" + jobExecutionContext.getJobDetail().getJobDataMap().get("name1"));
        System.out.println("getString(\"name2\"):" + jobExecutionContext.getJobDetail().getJobDataMap().getString("name2"));

        //输出打印时间
        String printTime = new SimpleDateFormat("yy-MM-dd HH-mm-ss").format(new Date());
        System.out.println("PrintWordsJob start at:" + printTime + ", prints: Hello Job-" + new Random().nextInt(100));
    }

案例1:触发器类型为SimpleTrigger的简单任务实例,执行main方法,执行一次任务/每间隔1s,5s后结束执行

使用流程:创建schedulerFactory实例 -》 获取scheduler实例 -》 创建JobDetail -》 创建Trigger -》 封装进scheduler中 -》 scheduler.start()执行

public static void main(String[] args) throws SchedulerException, InterruptedException {
	//案例1:触发器类型为SimpleTrigger的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行
	SimpleTrigger1();
}
/**
     * 案例1:触发器类型为SimpleTrigger的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行
     * @Author 211145187
     * @Date 2022/4/6 17:23
     **/
    public static void SimpleTrigger1() throws SchedulerException, InterruptedException {
        // 1、创建调度器Scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容)
        JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class)
                .withIdentity("job1", "group1") //使用具有给定名称和组来标识 JobDetail的身份
                .build();
        // 3、构建Trigger实例,每隔1s执行一次
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1", "triggerGroup1")
                .startNow()//立即生效
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()    //设置调度器类型:构建SimpleTrigger简单触发器
                    .withIntervalInSeconds(1)//每隔1s执行一次
                    .repeatForever()
                ).build();//一直执行

        //4、执行
        scheduler.scheduleJob(jobDetail, trigger);
        System.out.println("--------SimpleTrigger1 scheduler start ! ------------");
        scheduler.start();

        //睡眠
        Thread.sleep(5000);
        scheduler.shutdown();
        System.out.println("--------SimpleTrigger1 scheduler shutdown ! ------------");
    }

案例2:触发器类型为SimpleTrigger的简单任务实例,指定开始结束时间,下面的程序就实现了程序运行5s后开始执行Job,执行Job 5s后结束执行

注意:请留意代码注释及方法注释中的文字说明
注意:参数设置有两种方式:
注意:封装TriggerKey和普通直接设置

public static void main(String[] args) throws SchedulerException, InterruptedException {
 	//案例2:触发器类型为SimpleTrigger的简单任务实例,下面的程序就实现了程序运行5s后开始执行Job,执行Job 5s后结束执行
	SimpleTrigger2();
}
/**
	* 案例2:触发器类型为SimpleTrigger的简单任务实例,可以实现在一个指定时间段内执行一次作业任务或一个时间段内多次执行作业任务
	 *      下面的程序就实现了程序运行5s后开始执行Job,执行Job 5s后结束执行
	 * 注意:
	 *      1.JobDetail和Trigger设置同名参数会被覆盖
	 *      2.JobDetail设置的参数使用jobExecutionContext.getJobDetail().getJobDataMap()和jobExecutionContext.getMergedJobDataMap()都可以获取到,而Trigger设置的参数只能使用jobExecutionContext.getMergedJobDataMap()才可以获取到
	 *      3.参数设置有两种方式:
	 *          设置参数方式1,直接使用usingJobData设置,用于执行execute()中获取
	 *          设置参数方式2,使用jobDetail.getJobDataMap().put(key, value)设置,用于执行execute()中获取
	 *      4.可以封装TriggerKey传入Trigger的withIdentity(形参中),也可以直接设置值  =》 既.withIdentity("trigger1", "triggerGroup1") 和.withIdentity(triggerKey)等效
	 * @Author 211145187
	 * @Date 2022/4/6 17:23
	 **/
	public static void SimpleTrigger2() throws SchedulerException, InterruptedException {
	    Date startDate = new Date();
	    startDate.setTime(startDate.getTime() + 5000);
	    Date endDate = new Date();
	    endDate.setTime(startDate.getTime() + 5000);
	
	    // 1、创建调度器Scheduler
	    SchedulerFactory schedulerFactory = new StdSchedulerFactory();
	    Scheduler scheduler = schedulerFactory.getScheduler();
	    // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容)
	    JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class)
	            .withIdentity("job1", "group1")     //使用具有给定名称和组来标识 JobDetail的身份
	            .usingJobData("name1", "孙悟空")   //设置参数方式1,直接使用usingJobData设置,用于执行execute()中获取
	            .build();
	    jobDetail.getJobDataMap().put("name3", "猪八戒");  //设置参数方式2,使用jobDetail.getJobDataMap().put(key, value)设置,用于执行execute()中获取
	    //注意:可以封装TriggerKey传入Trigger的withIdentity(形参中),也可以直接设置值  =》 既.withIdentity("trigger1", "triggerGroup1") 和.withIdentity(triggerKey)等效
	    TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "triggerGroup1");
	    // 3、构建Trigger实例,每隔1s执行一次
	    Trigger trigger = TriggerBuilder.newTrigger()
	            .withIdentity("trigger1", "triggerGroup1")   //使用具有给定名称和组来标识 Trigger的身份
	            .startNow()//立即生效
	            .usingJobData("name2", "这是jobDetail1的trigger")   //设置参数,用于执行execute()中获取
	            .startAt(startDate) //设置开始时间
	            .endAt(endDate) //设置结束时间
	            .withSchedule(SimpleScheduleBuilder.simpleSchedule()    //设置调度器类型:构建SimpleTrigger简单触发器
	                .withIntervalInSeconds(1)//每隔1s执行一次
	                .withRepeatCount(1)//定义重复执行次数
	                .repeatForever()
	            ).build();//一直执行
	
	    //4、执行
	    scheduler.scheduleJob(jobDetail, trigger);
	    System.out.println("--------SimpleTrigger2 scheduler start ! ------------");
	    scheduler.start();
	
	    //睡眠
	    Thread.sleep(10000);
	    scheduler.shutdown();
	    System.out.println("--------SimpleTrigger2 scheduler shutdown ! ------------");
	}

案例3:触发器类型为CronTrigger 的简单任务实例,执行main方法,执行一次任务/每间隔1s,5s后结束执行

注意:Trigger和CronTrigger对象实例,调用的方法存在细微差别
Trigger实例设置.withIdentity()、.startNow()、.usingJobData()、.startAt()、.endAt、.withSchedule()、.build()
CronTrigger实例设置.withIdentity()、.usingJobData()、.startNow()、.withSchedule()、.build()

public static void main(String[] args) throws SchedulerException, InterruptedException {
	//案例3:触发器类型为CronTrigger 的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行
    CronTrigger1();
}
/**
   * 案例3:触发器类型为CronTrigger 的简单任务实例,执行main方法,执行一次/每间隔1s,5s后结束执行
   * 注意:
   *      cron表达式6位:秒分时日月周
   *      cron表达式7位:秒分时日月周年
   * @Author 211145187
   * @Date 2022/4/6 17:23
   **/
  public static void CronTrigger1() throws SchedulerException, InterruptedException {
      // 1、创建调度器Scheduler
      SchedulerFactory schedulerFactory = new StdSchedulerFactory();
      Scheduler scheduler = schedulerFactory.getScheduler();
      // 2、创建JobDetail实例,并与PrintWordsJob类绑定(Job执行内容)
      JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class)
              .withIdentity("job1", "group1")
              .build();    //使用具有给定名称和组来标识 JobDetail的身份
      // 3、构建Trigger实例,每隔1s执行一次,执行10s后停止
      CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
              .usingJobData("trigger1", "这是jobDetail1的trigger")
              .startNow()//立即生效
              .withSchedule(CronScheduleBuilder.cronSchedule("0/1 * * * * ?"))     //设置调度器类型:构建CronTrigger简单触发器
              .build();

      //4、执行
      scheduler.scheduleJob(jobDetail, cronTrigger);
      System.out.println("--------CronTrigger1 scheduler start ! ------------");
      scheduler.start();

      //睡眠
      Thread.sleep(10000);
      scheduler.shutdown();
      System.out.println("--------CronTrigger1 scheduler shutdown ! ------------");
  }

案例4:使用阿里线程池,模拟定时执行任务途中停止任务,线程睡眠2秒后再激活任务,输出查看任务状态等信息

定时任务CronTrigger1一直执行, 然后新启动一个线程执行暂停方法 -》 休息2秒后 -》 再执行激活方法,查看job的execute()方法参数输出及任务改变状态打印等。
主要为了打印观察任务执行状态及改变。

import lombok.SneakyThrows;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.quartz.impl.StdSchedulerFactory;

import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
 * 创建Schedule,执行任务
 * @Author 211145187
 * @Date 2022/4/6 17:07
 **/
public class MyScheduler {
    //这里使用的是ThreadPoolExecutor的完整版构造函数
    private static final ThreadPoolExecutor singlePool = new ThreadPoolExecutor(10,10,100, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100),
            new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build()
            ,new ThreadPoolExecutor.CallerRunsPolicy());
            
public static void main(String[] args) throws SchedulerException, InterruptedException {
CronTrigger1();
        //案例4:使用阿里线程池,模拟定时执行任务途中停止任务,休息几秒后再激活任务,输出查看任务状态等信息
        singlePool.execute(new Runnable() {
            @SneakyThrows
            @Override
            public void run() {
                //执行暂停方法
                pauseJob();
                //睡眠2秒,让暂停方法和激活方法中间有点时间间隔
                Thread.sleep(2000);
                //激活任务
                resumeJob();
            }
        });
}
/**
 * 暂停任务
  * 注意:
  *      1.这里传的是job的name和group名字,千万不要写成trigger的名字和组名
  *      2.name和group都要跟任务job的一致,负责执行无效
  **/
 public static void pauseJob() throws SchedulerException {
     SchedulerFactory schedulerFactory = new StdSchedulerFactory();
//        JobKey jobKey = new JobKey("job1", Scheduler.DEFAULT_GROUP);   //无效。组名不一致
     JobKey jobKey = new JobKey("job1", "group1");
     Scheduler scheduler = schedulerFactory.getScheduler();
     scheduler.pauseJob(jobKey);
     System.out.println("--------CronTrigger1 pauseJob ! ------------");
 }
 //激活任务
 public static void resumeJob() throws SchedulerException {
     SchedulerFactory schedulerFactory = new StdSchedulerFactory();
     JobKey jobKey = new JobKey("job1", "group1");
     Scheduler scheduler = schedulerFactory.getScheduler();
     scheduler.resumeJob(jobKey);
     System.out.println("--------CronTrigger1 resumeJob ! ------------");
     System.out.println("输出下次执行之间:" +scheduler.getTriggersOfJob(jobKey).get(0).getFireTimeAfter(new Date()));    //输出下次执行时间
 }

三、容易出错点整理

1.创建JobDetail实例和Trigger实例,必须指定属性name和group值,当然也可以使用Scheduler.DEFAULT_GROUP提供的默认值等。

2.创建JobDetail实例和Trigger实例,都会配置属于自己的方法,比如

Trigger实例方法

方法名注释
.withIdentity()使用具有给定名称和组来标识 JobDetail的身份
.startNow()立即生效
.withSchedule()设置调度器类型
.build()构建
.usingJobData()设置参数
.withIntervalInSeconds()执行间隔
.withRepeatCount()重复次数
.startAt()设置开始时间
.endAt()设置结束时间
.repeatForever()一直执行

JobDetail实例方法

方法名注释
.withIdentity()使用具有给定名称和组来标识 JobDetail的身份
.build()构建
.usingJobData()设置参数

3.使用JobDetail和Trigger设置参数

1.JobDetail和Trigger都可以设置参数,且设置同名参数会被覆盖。
2.具体job的execute()方法获取形参方式有差异,比如JobDetail实例设置的参数,使用jobExecutionContext.getJobDetail().getJobDataMap()和jobExecutionContext.getMergedJobDataMap()都可以获取到。
而Trigger实例设置的参数只能使用jobExecutionContext.getMergedJobDataMap()才可以获取到
既 :个人建议使用getMergedJobDataMap()这个方法获取参数,目前都能获取到

 jobExecutionContext.getMergedJobDataMap().get("name1")
 jobExecutionContext.getMergedJobDataMap().getString("name2")
 jobExecutionContext.getJobDetail().getJobDataMap().get("name1")
 jobExecutionContext.getJobDetail().getJobDataMap().getString("name2")

4.JobDetail设置参数两种方式

第一种方式设置参数

JobDetail jobDetail = JobBuilder.newJob(PrintWordsJob.class)
                .withIdentity("job1", "group1")     //使用具有给定名称和组来标识 JobDetail的身份
                .usingJobData("name1", "孙悟空")   //设置参数方式1,直接使用usingJobData设置,用于执行execute()中获取
                .build();

第二种方式设置参数

jobDetail.getJobDataMap().put("name3", "猪八戒"); 

5.构建Trigger实例2种方式

该部分指构建Trigger实例时候使用

第一种实例化方式

Trigger trigger = TriggerBuilder.newTrigger()
         .withIdentity("trigger1", "triggerGroup1")
         .build();

第二种实例化方式,封装TriggerKey

TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", "triggerGroup1");
Trigger trigger = TriggerBuilder.newTrigger()
         .withIdentity(triggerKey)
         .build();

6.针对暂停、激活、删除任务、获取下次执行时间说明

暂停 pauseJob(jobKey)
激活 resumeJob(jobKey)
删除 deleteJob(jobKey)
输出下次执行时间 scheduler.getTriggersOfJob(jobKey).get(0).getFireTimeAfter(new Date()))

其中的jobKey为添加任务时候使用的jobKey,必须一致,不然无法找到,就会导致暂停、激活、删除操作失效。
注意:激活任务不会立即执行任务,而只是让任务状态处于“已就绪”状态,随时等待启动。
注意:构建JobKey传入的是job的name和group名字,千万不要写成trigger的名字和组名。

7.案例4使用阿里线程池中暂停方法和激活方法之间必须设置休眠时间,不然执行太快,看不出来打印任务状态改变啥的

8.具体实现job接口的实现类的execute()的形参jobExecutionContext输出展示

获取任务状态
TriggerKey triggerKey = TriggerKey.triggerKey(“trigger1”, “triggerGroup1”);
jobExecutionContext.getScheduler().getTriggerState(triggerKey)

获取job标识
jobExecutionContext.getTrigger().getJobKey() //输出job1,group1

获取trigger标识
jobExecutionContext.getTrigger().getKey() //输出trigger1,triggerGroup1

get(key)和getString(key)等效
jobExecutionContext.getMergedJobDataMap().get(“name1”)
jobExecutionContext.getMergedJobDataMap().getString(“name2”)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘大猫.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值