主頁 > 後端開發 > 一文快速入門任務調度框架-Quartz

一文快速入門任務調度框架-Quartz

2023-04-01 07:21:05 後端開發

前言

還不會 Quartz?如果你還沒有接觸過Quartz,那么你可能錯過了一個很棒的任務調度框架!Quartz 提供了一種靈活、可靠的方式來管理和執行定時任務,讓咱們的定時任務更加優雅,本篇文章將為你介紹 Quartz 框架的核心概念、API 和實戰技巧,讓你輕松上手,也不用擔心,作為過來人,我會把難懂的概念和術語解釋清楚,讓你看完本篇文章后,就知道該如何操作 Quartz,當然,本篇文章難免有不足之處,在此歡迎大家指出,那廢話少說,下面我們開始吧!

什么是 Quartz?

Quartz:https://github.com/quartz-scheduler/quartz

官網:http://www.quartz-scheduler.org/

Quartz 是一個功能豐富的開源任務調度框架(job scheduling library),從最小的獨立的 Java 應用程式到最大的電子商務系統,它幾乎都可以集成,Quartz 可用于創建簡單或復雜的調度,以執行數十、數百個甚至數萬個任務;這些任務被定義為標準 Java 組件,這些組件可以執行你想讓他做的任何事情,Quartz 調度程式包括許多企業級特性,例如支持 JTA 事務(Java Transaction API,簡寫 JTA)和集群,

注意:Job == 任務

JTA,即 Java Transaction API,JTA 允許應用程式執行分布式事務處理——在兩個或多個網路計算機資源上訪問并且更新資料,

為什么學習 Quartz?

定時任務直接用 Spring 提供的 @Schedule 不行嗎?為什么還要學習 Quartz?有什么好處?

是的,一開始我也是這么想的,但是某些場景,單靠 @Schedule 你就實作不了了,

比如我們需要對定時任務進行增刪改查,是吧,@Schedule 就實作不了,你不可能每次新增一個定時任務都去手動改代碼來添加吧,而 Quartz 就能夠實作對任務的增刪改查,當然,這只是 Quartz 的好處之一,

Quartz 的特性

運行時環境

  • Quartz 可以嵌入另一個獨立的應用程式中運行
  • Quartz 可以在應用程式服務器(比如 Tomcat)中實體化,并參與 XA 事務(XA 是一個分布式事務協議
  • Quartz 可以作為一個獨立程式運行(在其自己的Java虛擬機中),我們通過 RMI(Remote Method Invocation,遠程方法呼叫)使用它
  • Quartz 可以實體化為一個獨立程式集群(具有負載平衡和故障轉移功能),用于執行任務

任務的調度(Job Scheduling)

當一個觸發器(Trigger)觸發時,Job 就會被調度執行,觸發器就是用來定義何時觸發的(也可以說是一個執行計劃),可以有以下任意的組合:

  • 在一天中的某個時間(毫秒)
  • 在一周中的某些日子
  • 在一個月的某些日子
  • 在一年中的某些日子
  • 重復特定次數
  • 重復直到特定的時間/日期
  • 無限期重復
  • 以延遲間隔重復

Job 由我們自己去命名,也可以組織到命名組(named groups)中,Trigger 也可以被命名并分組,以便在調度器(Scheduler)中更容易地組織它們,

Job 只需在 Scheduler 中添加一次,就可以有多個 Trigger 進行注冊,

任務的執行(Job Execution)

  • 實作了 Job 介面的 Java 類就是 Job,習慣稱為任務類(Job class),
  • 當 Trigger 觸發時,Scheduler 就會通知 0 個或多個實作了 JobListener 和 TriggerListener 介面的 Java 物件,當然,這些 Java 物件在 Job 執行后也會被通知到,
  • 當 Job 執行完畢時,會回傳一個碼——JobCompletionCode,這個 JobCompletionCode 能夠表示 Job 執行成功還是失敗,我們就能通過這個 Code 來判斷后續該做什么操作,比如重新執行這個 Job,

任務的持久化(Job Persistence)

  • Quartz 的設計包括了一個 JobStore 介面,該介面可以為存盤 Job 提供各種機制,
  • 通過 JDBCJobStore,可以將 Job 和 Trigger 持久化到關系型資料庫中,
  • 通過 RAMJobStore,可以將 Job 和 Trigger 存盤到記憶體中(優點就是無須資料庫,缺點就是這不是持久化的),

事務

  • Quartz 可以通過使用 JobStoreCMT(JDBCJobStore的一個子類)參與 JTA 事務,
  • Quartz 可以圍繞任務的執行來管理 JTA 事務(開始并且提交它們),以便任務執行的作業自動發生在 JTA 事務中,

集群

  • 故障轉移
  • 負載均衡
  • Quartz 的內置集群功能依賴于 JDBCJobStore 實作的資料庫持久性,
  • Quartz 的 Terracotta 擴展提供了集群功能,而無需備份資料庫,

監聽器和插件

  • 應用程式可以通過實作一個或多個監聽器介面來捕獲調度事件以監聽或控制 Job / Trigger 的行為,
  • 插件機制,我們可向 Quartz 添加功能,例如保存 Job 執行的歷史記錄,或從檔案加載 Job 和 Trigger 的定義,
  • Quartz 提供了許多插件和監聽器,

初體驗

引入 Quartz 依賴項

創建一個 Spring Boot 專案,然后引入如下依賴,就可以體驗 Quartz 了,

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

示例

現在知道 Quartz 有這么幾個概念,分別是 JobTriggerScheduler,在它的設計實作上,分別是 Job 介面、JobDetail 介面、Trigger 介面、Scheduler 介面,除了 Job 介面的實作類需要我們自己去實作,剩下的都由 Quartz 實作了,

Scheduler

Quartz中的調度器(Scheduler)的主要作用就是調度 Job 和 Trigger 的執行,在Quartz中,Job代表需要執行的任務,Trigger代表觸發Job執行的條件和規則,調度器會根據Trigger的配置來確定Job的執行時機,

下面的代碼包含了一個 Scheduler 的實體物件,接著是呼叫 start 方法,最后呼叫 shutdown 方法,

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzTest {
    public static void main(String[] args) {
        try {
            // 從 Factory 中獲取 Scheduler 實體
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 開始并關閉
            scheduler.start();

            scheduler.shutdown();

        } catch (SchedulerException se) {
            se.printStackTrace();
        }
    }
}

一旦我們使用 StdSchedulerFactory.getDefaultScheduler() 獲取 Scheduler 物件后,那么程式就會一直運行下去,不會終止,直到我們呼叫了 scheduler.shutdown() 方法才會停止運行,這是因為獲取 Scheduler 物件后,就有許多執行緒在運行著,所以程式會一直運行下去,

與此同時,控制臺會輸出相應的日志:

10:14:02.442 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
10:14:02.445 [main] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: main
10:14:02.452 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
10:14:02.452 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
10:14:02.453 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
10:14:02.453 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

10:14:02.453 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
10:14:02.453 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2

從日志中也能看出 Quartz 的一些資訊,比如版本、使用的執行緒池、使用的任務存盤機制(這里默認是 RAMJobStore)等等資訊,

我們想要執行任務的話,就需要把任務的代碼放在 scheduler.start()scheduler.shutdown() 之間,

QuartzTest:

import cn.god23bin.demo.quartz.job.HelloJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

// 這里匯入了 static,下面才能直接 newJob, newTrigger
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;

public class QuartzTest {
    public static void main(String[] args) {
        try {
            // 從 Factory 中獲取 Scheduler 實體
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 開始并關閉
            scheduler.start();

            // 定義一個 Job(用JobDetail描述的Job),并將這個 Job 系結到我們寫的 HelloJob 這個任務類上
            JobDetail job = newJob(HelloJob.class)
                    .withIdentity("job1", "group1") // 名字為 job1,組為 group1
                    .build();

            // 現在觸發任務,讓任務執行,然后每5秒重復執行一次
            Trigger trigger = newTrigger()
                    .withIdentity("trigger1", "group1")
                    .startNow()
                    .withSchedule(simpleSchedule()
                            .withIntervalInSeconds(5)
                            .repeatForever())
                    .build();

            // 告知 Quartz 使用我們的 Trigger 去調度這個 Job
            scheduler.scheduleJob(job, trigger);

            // 為了在 shutdown 之前讓 Job 有足夠的時間被調度執行,所以這里當前執行緒睡眠30秒
            Thread.sleep(30000);

            scheduler.shutdown();

        } catch (SchedulerException | InterruptedException se) {
            se.printStackTrace();
        }
    }
}

HelloJob:實作 Job 介面,重寫 execute 方法,實作我們自己的任務邏輯,

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;

public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("Hello Job!!! 時間:" + sdf.format(jobExecutionContext.getFireTime()));
    }
}

運行程式,輸出如下資訊:

10:25:40.069 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.job1', class=cn.god23bin.demo.quartz.job.HelloJob
10:25:40.071 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:25:40.071 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.job1
Hello Job!!! 時間:2023-03-28 10:25:40
10:25:45.066 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.job1', class=cn.god23bin.demo.quartz.job.HelloJob
10:25:45.066 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:25:45.066 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.job1
Hello Job!!! 時間:2023-03-28 10:25:45
# 省略后面輸出的資訊,都是一樣的

API 有哪些?

Quartz API 的關鍵介面如下:

  • Scheduler :最主要的 API,可以使我們與調度器進行互動,簡單說就是讓調度器做事,
  • Job :一個 Job 組件,你自定義的一個要執行的任務類就可以實作這個介面,實作這個介面的類的物件就可以被調度器進行調度執行,
  • JobDetailJob 的詳情,或者說是定義了一個 Job,
  • JobBuilder : 用來構建 JobDetail 實體的,然后這些實體又定義了 Job 實體,
  • Trigger : 觸發器,定義 Job 的執行計劃的組件,
  • TriggerBuilder : 用來構建 Trigger 實體,

Quartz 涉及到的設計模式:

  • Factory Pattern:

    // 從 Factory 中獲取 Scheduler 實體
    Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
    
  • Builder Pattern:

    JobDetail job = newJob(HelloJob.class)
                        .withIdentity("job1", "group1") // 名字為 job1,組為 group1
                        .build();
    

    這里的 newJob 方法是 JobBuilder 類中的一個靜態方法,就是通過這個來構建 JobDetail 的,

    /**
     * Create a JobBuilder with which to define a <code>JobDetail</code>,
     * and set the class name of the <code>Job</code> to be executed.
     * 
     * @return a new JobBuilder
     */
    public static JobBuilder newJob(Class <? extends Job> jobClass) {
        JobBuilder b = new JobBuilder();
        b.ofType(jobClass);
        return b;
    }
    
    /**
     * Produce the <code>JobDetail</code> instance defined by this 
     * <code>JobBuilder</code>.
     * 
     * @return the defined JobDetail.
     */
    public JobDetail build() {
    
        JobDetailImpl job = new JobDetailImpl();
    
        job.setJobClass(jobClass);
        job.setDescription(description);
        if(key == null)
            key = new JobKey(Key.createUniqueName(null), null);
        job.setKey(key); 
        job.setDurability(durability);
        job.setRequestsRecovery(shouldRecover);
    
    
        if(!jobDataMap.isEmpty())
            job.setJobDataMap(jobDataMap);
    
        return job;
    }
    

    同樣,構建 Trigger 物件是使用 TriggerBuilder 類以及 SimpleScheduleBuilder 類構建的,Schedule 主要是一個時間安排表,就是定義何時執行任務的時間表,

  • 當然,除了上面說的兩種設計模式外,還有其他的設計模式,這里就不細說了,比如單例模式,觀察者模式,

簡單理解 Job、Trigger、Scheduler

每天中午12點唱、跳、Rap、籃球

  • Job:唱、跳、Rap、籃球
  • Trigger:每天中午12點為一個觸發點
  • Scheduler:自己,我自己調度 Trigger 和 Job,讓自己每天中午12點唱、跳、Rap、籃球

關于 Job

Job 介面原始碼:

package org.quartz;

public interface Job {
    
    void execute(JobExecutionContext context) throws JobExecutionException;
    
}

當該任務的 Trigger 觸發時,那么 Job 介面的 execute 方法就會被 Scheduler 的某一個作業執行緒呼叫,JobExecutionContext 物件就會作為引數傳入這個方法,該物件就提供 Job 實體的一些關于任務運行時的資訊,

我們知道,寫完一個 Job 類后,需要將定義一個 JobDetail 系結到我們的 Job 類:

// 定義一個 Job(用JobDetail描述的Job),并將這個 Job 系結到我們寫的 HelloJob 這個任務類上
JobDetail job = newJob(HelloJob.class)
    .withIdentity("job1", "group1") // 名字為 job1,組為 group1
    .build();

在這個程序中,有許多屬性是可以設定的,比如 JobDataMap,這個物件能夠存盤一些任務的狀態資訊資料,這個后面說,

Trigger 物件用于觸發任務的執行,當我們想要調度某個任務時,可以實體化 Trigger 并設定一些我們想要的屬性,Trigger 也可以有一個與之相關的 JobDataMap,這對于特定的觸發器觸發時,傳遞一些引數給任務是很有用,Quartz 有幾種不同的 Trigger 型別,但最常用的型別是 SimpleTriggerCronTrigger

關于 SimpleTrigger 和 CronTrigger

如果我們想要在某個時間點執行一次某個任務,或者想要在給定時間啟動一個任務,并讓它重復 N 次,執行之間的延遲為 T,那么就可以使用 SimpleTrigger,

如果我們想根據類似日歷的時間表來執行某個任務,例如每天晚上凌晨 4 點這種,那么就可以使用 CronTrigger,

為什么會設計出 Job 和 Trigger 這兩個概念?

在官網上是這樣說的:

Why Jobs AND Triggers? Many job schedulers do not have separate notions of jobs and triggers. Some define a ‘job’ as simply an execution time (or schedule) along with some small job identifier. Others are much like the union of Quartz’s job and trigger objects. While developing Quartz, we decided that it made sense to create a separation between the schedule and the work to be performed on that schedule. This has (in our opinion) many benefits.

For example, Jobs can be created and stored in the job scheduler independent of a trigger, and many triggers can be associated with the same job. Another benefit of this loose-coupling is the ability to configure jobs that remain in the scheduler after their associated triggers have expired, so that that it can be rescheduled later, without having to re-define it. It also allows you to modify or replace a trigger without having to re-define its associated job.

簡而言之,這有許多好處:

  1. 任務可以獨立于觸發器,它可以在調度器中創建和存盤,并且許多觸發器可以與同一個任務關聯,
  2. 這種松耦合能夠配置任務,在其關聯的觸發器已過期后仍然保留在調度器中,以便之后重新安排,而無需重新定義它,
  3. 這也允許我們修改或替換觸發器的時候無需重新定義其關聯的任務,

Job 和 Trigger 的身份標識(Identities)

在上面的代碼中我們也看到了,Job 和 Trigger 都有一個 withIdentity 方法,

JobBuilder 中的 withIdentity 方法:

private JobKey key;

public JobBuilder withIdentity(String name, String group) {
    key = new JobKey(name, group);
    return this;
}

TriggerBuilder 中的 withIdentity 方法:

private TriggerKey key;

public TriggerBuilder<T> withIdentity(String name, String group) {
    key = new TriggerKey(name, group);
    return this;
}

當 Job 和 Trigger 注冊到 Scheduler 中時,就會通過這個 key 來標識 Job 和 Trigger,

任務和觸發器的 key(JobKey 和 TriggerKey)允許將它們放入「組」中,這有助于將任務和觸發器進行分組,或者說分類,而且任務或觸發器的 key 的名稱在組中必須是唯一的,他們完整的 key(或識別符號)是名稱和組的組合,從上面的代碼中也可以看到,構造方法都是兩個引數的,第一個引數是 name,第二個引數是 group,構造出來的就是整個 key 了,

關于 JobDetail 和 JobDataMap

我們通過寫一個 Job 介面的實作類來撰寫我們等待執行的任務,而 Quartz 需要知道你將哪些屬性給了 Job,那 Quartz 是如何知道的呢?Quartz 就是通過 JobDetail 知道的,

注意,我們向 Scheduler 提供了一個 JobDetail 實體, Scheduler 就能知道要執行的是什么任務,只需在構建 JobDetail 時提供任務的類即可(即 newJob(HelloJob.class)),

每次調度程式執行任務時,在呼叫其 execute 方法之前,它都會創建該任務類的一個新實體,執行完成任務后,對任務類實體的參考將被丟棄,然后該實體將被垃圾回收,

那我們如何為作業實體提供屬性或者配置?我們如何在執行的程序中追蹤任務的狀態?這兩個問題的答案是一樣的:關鍵是 JobDataMap,它是 JobDetail 物件的一部分,

JobDataMap 可以用來保存任意數量的(可序列化的)資料物件,這些物件在任務實體執行時需要使用的,JobDataMap 是 Java 中 Map 介面的一個實作,具有一些用于存盤和檢索原始型別資料的方法,

示例:

PlayGameJob:

public class PlayGameJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobKey key = context.getJobDetail().getKey();
        // 獲取JobDataMap,該Map在創建JobDetail的時候設定的
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        String gameName = jobDataMap.getString("gameName");
        float gamePrice = jobDataMap.getFloat("gamePrice");
        System.out.println("我玩的" + gameName + "才花費了我" + gamePrice + "塊錢");
    }
}

接著使用 usingJobData 設定該任務需要的資料,最后調度該任務:

Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

JobDetail job = newJob(PlayGameJob.class)
    .withIdentity("myJob", "group1")
    .usingJobData("gameName", "GTA5")
    .usingJobData("gamePrice", 55.5f)
    .build();

Trigger trigger = newTrigger()
    .withIdentity("myJob", "group1")
    .build();

scheduler.scheduleJob(job, trigger);

Thread.sleep(10000);

scheduler.shutdown();

控制臺輸出:

14:18:43.295 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=cn.god23bin.demo.quartz.job.PlayGameJob
14:18:43.299 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers
14:18:43.300 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
我玩的GTA5才花費了我55.5塊錢

當然,也可以這樣寫:

JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("gameName", "GTA5");
jobDataMap.put("gamePrice", 55.5f);

JobDetail job = newJob(PlayGameJob.class)
    .withIdentity("myJob", "group1")
    .usingJobData(jobDataMap)
    .build();

之前還說過,Trigger 也是可以有 JobDataMap 的,當你有這種情況,就是在調度器中已經有一個 Job 了,但是想讓不同的 Trigger 去觸發執行這個 Job,每個不同的 Trigger 觸發時,你想要有不同的資料傳入這個 Job,那么就可以用到 Trigger 攜帶的 JobDataMap 了

噢對了!對于我們上面自己寫的 PlayGameJob ,還可以換一種寫法,不需要使用通過 context.getJobDetail().getJobDataMap() 獲取 JobDataMap 物件后再根據 key 獲取對應的資料,直接在這個任務類上寫上我們需要的屬性,提供 getter 和 setter 方法,這樣 Quartz 會幫我們把資料賦值到該物件的屬性上,

PlayGameJob:

// 使用Lombok的注解,幫我們生成 getter 和setter 方法以及無參的構造方法
@Data
@NoArgsConstructor
public class PlayGameJob implements Job {

    // Quartz 會把資料注入到任務類定義的屬性上,直接用就可以了
    private String gameName;

    private float gamePrice;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobKey key = context.getJobDetail().getKey();
        System.out.println("我玩的" + gameName + "才花費了我" + gamePrice + "塊錢");
    }
}

這樣的效果,就是減少了 execute 方法中的代碼量,

如何理解 Job 實體?

這個確實會有一些困惑,比如一開始說的 Job 介面,還有 JobDetail 介面,而且為什么會說成 JobDetail 物件是 Job 的實體?是吧,

想要理解,舉個例子:

現在我們寫了一個發送訊息的 Job 實作類——SendMessageJob,

接著我們創建了多個 JobDetail 物件,這些物件都有不同的定義,比如有叫做 SendMessageToLeBron 的 JobDetail、有 SendMessageToKobe 的 JobDetail,這兩個 JobDetail 都有它各自的 JobDataMap 傳遞給我們的 Job 實作類,

當 Trigger 觸發時,Scheduler 將加載與其關聯的 JobDetail(任務定義),并通過 Scheduler上配置的 JobFactory 實體化它所參考的任務類(SendMessageJob),默認的 JobFactory 只是在任務類上呼叫newInstance() ,然后嘗試在與 JobDataMap 中鍵的名稱匹配的類中的屬性名,進而呼叫 setter 方法將 JobDataMap 中的值賦值給對應的屬性,

在 Quartz 的術語中,我們將每個 JobDetail 物件稱為「Job 定義或者 JobDetail 實體」,將每個正在執行的任務稱為「Job 實體或 Job 定義的實體」,

一般情況下,如果我們只使用「任務」這個詞,我們指的是一個名義上的任務,簡而言之就是我們要做的事情,也可以指 JobDetail,當我們提到實作 Job 介面的類時,我們通常使用術語「任務類」,

兩個需要知道的注解

JobDataMap 可以說是任務狀態的資料,這里的資料和并發也有點關系,Quartz 提供了幾個注解,這幾個注解會影響到 Quartz 在這方面的動作,

@DisallowConcurrentExecution 注解是用在任務類上的,

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DisallowConcurrentExecution {

}

DisallowConcurrentExecution 這個注解的作用就是告知 Quartz 這個任務定義的實體(JobDetail 實體)不能并發執行,舉個例子,就上面的 SendMessageToLeBron 的 JobDetail 實體,是不能并發執行的,但它是可以與 SendMessageToKobe 的 JobDetail 的實體同時執行,需要注意的是它指的不是任務類的實體(Job 實體),

@PersistJobDataAfterExecution 注解也是用在任務類上的,

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PersistJobDataAfterExecution {

}

@PersistJobDataAfterExecution 這個注解的作用是告知 Quartz 在 execute 方法成功完成后更新 JobDetail 的 JobDataMap 的存盤副本(沒有引發例外),以便同一任務的下一次執行能接收更新后的值,而不是最初存盤的值,

@DisallowConcurrentExecution 注解一樣,這是適用于任務定義實體(JobDetail 實體),而不是任務類實體(Job 實體),

關于 Trigger

我們需要了解 Trigger 有哪些屬性可以去設定,從最開始的初體驗中,我們給 Trigger 設定了一個 TriggerKey 用來標識這個 Trigger 實體,實際上,它還有好幾個屬性給我們設定,

共用的屬性

上面也說過 Trigger 有不同的型別(比如 SimpleTrigger 和 CronTrigger),不過,即使是不同的型別,也有相同的屬性,

  • jobKey:作為 Trigger 觸發時應執行的任務的標識,
  • startTime:記錄下首次觸發的時間;對于某些觸發器,它是指定觸發器應該在何時觸發,
  • endTime:觸發器不再生效的時間
  • ...

還有更多,下面說一些重要的,

priority

優先級,這個屬性可以設定 Trigger 觸發的優先級,值越大則優先級越高,就優先被觸發執行任務,當然這個是在同一時間調度下才會有這個優先級比較的,如果你有一個 A 任務在 6 點觸發,有一個 B 任務在 7 點觸發,即使你的 B 任務的優先級比 A 任務的高,也沒用,6 點 的 A 任務總是會比 7點 的 B 任務先觸發,

misfireInstruction

misfire instruction,錯失觸發指令,也就是說當某些情況下,導致觸發器沒有觸發,那么就會執行這個指令,默認是一個「智能策略」的指令,它能夠根據不同的 Trigger 型別執行不同的行為,

當 Scheduler 啟動的時候,它就會先搜尋有沒有錯過觸發的 Trigger,有的話就會基于 Trigger 配置的錯失觸發指令來更新 Trigger 的資訊,

calendar

Quartz 中也有一個 Calendar 物件,和 Java 自帶的不是同一個,

在設定 Trigger 的時候,如果我們想排除某些日期時間,那么就可以使用這個 Calendar 物件,

SimpleTrigger

如果我們想在特定的時間點執行一次任務,或者在特定的時刻執行一次,接著定時執行,那么 SimpleTrigger 就能滿足我們的需求,

SimpleTrigger 包含了這么幾個屬性:

  • startTime:開始時間
  • endTime:結束時間
  • repeatCount:重復次數,可以是 0,正整數,或者是一個常量 SimpleTrigger.REPEAT_INDEFINITELY
  • repeatInterval:重復的時間間隔,必須是 0,或者是一個正的長整型的值(long 型別的值),表示毫秒,即多少毫秒后重復觸發

SimpleTrigger 的實體物件可以由 TriggerBuilder 和 SimpleScheduleBuilder 來創建,

示例

下面舉幾個例子:

  1. 構建一個給定時刻觸發任務的 Trigger,不會重復觸發:
// 今天22點30分0秒
Date startAt = DateBuilder.dateOf(22, 30, 0);

// 通過強轉構建一個 SimpleTrigger
SimpleTrigger trigger = (SimpleTrigger) newTrigger()
    .withIdentity("trigger1", "group1")
    .startAt(startAt) // 開始的日期時間
    .forJob("job1", "group1") // 通過 job 的 name 和 group 識別 job
    .build();
  1. 構建一個給定時刻觸發任務的 Trigger,每十秒重復觸發十次:
trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .startAt(startAt)  // 如果沒有給定開始時間,那么就默認現在開始觸發
    .withSchedule(SimpleScheduleBuilder.simpleSchedule() // 通過 simpleSchedule 方法構建 SimpleTrigger
              .withIntervalInSeconds(10) 
              .withRepeatCount(10)) // 每隔10秒重復觸發10次
    .forJob(job) // 通過 JobDetail 本身來識別 Job
    .build();
  1. 構建一個給定時刻觸發任務的 Trigger,在未來五分鐘內觸發一次:
Date futureDate = DateBuilder.futureDate(5, DateBuilder.IntervalUnit.MINUTE);
JobKey jobKey = job.getKey();

trigger = (SimpleTrigger) newTrigger()
    .withIdentity("trigger5", "group1")
    .startAt(futureDate) // 使用 DateBuilder 創建一個未來的時間
    .forJob(jobKey) // 通過 jobKey 識別 job
    .build();
  1. 構建一個給定時刻觸發任務的 Trigger,然后每五分鐘重復一次,直到晚上 22 點:
trigger = newTrigger()
    .withIdentity("trigger7", "group1")
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
              .withIntervalInMinutes(5)
              .repeatForever())
    .endAt(DateBuilder.dateOf(22, 0, 0))
    .build();
  1. 構建一個給定時刻觸發任務的 Trigger,然后每下一個小時整點觸發,然后每2小時重復一次,一直重復下去:
trigger = newTrigger()
    .withIdentity("trigger8") // 這里沒有指定 group 的話,那么 "trigger8" 就會在默認的 group 中
    .startAt(DateBuilder.evenHourDate(null)) // 下一個整點時刻 (分秒為零 ("00:00"))
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
              .withIntervalInHours(2)
              .repeatForever())
    .forJob("job1", "group1")
    .build();

錯失觸發指令

比如我在要觸發任務的時候,機器宕機了,當機器重新跑起來后怎么辦呢?

當 Trigger 錯失觸發時間去觸發任務時,那么 Quartz 就需要執行 Misfire Instruction,SimpleTrigger 有如下的以常量形式存在的 Misfire 指令:

  • MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
  • MISFIRE_INSTRUCTION_FIRE_NOW
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT

我們知道,所有的 Trigger,SimpleTrigger 也好,CronTrigger 也好,不管是什么型別,都有一個 Trigger.MISFIRE_INSTRUCTION_SMART_POLICY 可以使用,如果我們使用這個指令,那么 SimpleTrigger 就會動態地在上面 6 個指令中選擇,選擇的行為取決于我們對于 SimpleTrigger 的設定,

當我們在構建 Trigger 的時候,就可以給 Trigger 設定上 Misfire 指令:

trigger = newTrigger()
    .withIdentity("trigger7", "group1")
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
              .withIntervalInMinutes(5)
              .repeatForever()
              .withMisfireHandlingInstructionNextWithExistingCount())
    .build();

CronTrigger

使用 CronTrigger,我們可以指定觸發任務的時間安排(schedule),例如,每周五中午,或 每個作業日和上午9:30, 甚至 每周一,周三上午9:00到上午10:00之間每隔5分鐘 和 1月的星期五

CronTrigger 也有一個 startTime,用于指定計劃何時生效,以及一個(可選的)endTime,用于指定何時停止這個任務的執行,

cron 運算式

cron 運算式有 6 位,是必須的,從左到右分別表示:秒、分、時、日、月、周

當然也可以是 7 位,最后一位就是年(可選項):秒、分、時、日、月、周、年

取值說明:正常認識,秒分都是 0 - 59,則是 0 - 23,則是 1 - 31,在這邊則是 0-11,則是 1 - 7(這里的1指的是星期日),則只有 1970 - 2099

月份可以指定為0到11之間的值,或者使用字串 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV 和 DEC

星期幾可以使用字串 SUN,MON,TUE,WED,THU,FRI 和 SAT 來表示

詳細可參考這里:簡書-Cron運算式的詳細用法

Cron 生成工具:cron.qqe2.com/

示例

  1. 構建一個 Trigger,每天上午8點到下午5點之間每隔一分鐘觸發一次:
Trigger trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(cronSchedule("0 0/1 8-17 * * ?"))
    .forJob("myJob", "group1")
    .build();
  1. 構建一個 Trigger,每天上午10:42觸發:
JobKey myJobKey = job.getKey();

trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(10, 42))
    .forJob(myJobKey)
    .build();
  1. 構建一個觸發器,該觸發器將在星期三上午10點42分在TimeZone中觸發,而不是系統的默認值:
JobKey myJobKey = job.getKey();

trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(CronScheduleBuilder
            .weeklyOnDayAndHourAndMinute(DateBuilder.WEDNESDAY, 10, 42)
            .inTimeZone(TimeZone.getTimeZone("America/Los_Angeles")))
    .forJob(myJobKey)
    .build();

錯失觸發指令

對于 CronTrigger,它有 3 個 Misfire 指令

  • MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
  • MISFIRE_INSTRUCTION_DO_NOTHING
  • MISFIRE_INSTRUCTION_FIRE_NOW

我們在構建 Tirgger 的時候就可以給這個 Trigger 指定它的 Misfire 指令:

trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(cronSchedule("0 0/2 8-17 * * ?")
              .withMisfireHandlingInstructionFireAndProceed())
    .forJob("myJob", "group1")
    .build();

關于CRUD

存盤定時任務

存盤定時任務,方便后續使用,通過 Scheduler 的 addJob 方法

  • void addJob(JobDetail jobDetail, boolean replace) throws SchedulerException;

該方法會添加一個沒有與 Trigger 關聯的 Job 到 Scheduler 中,然后這個 Job 是處于休眠的狀態直到它被 Trigger 觸發進行執行,或者使用 Scheduler.triggerJob() 指定了這個 Job,這個 Job 才會被喚醒,

JobDetail job1 = newJob(MyJobClass.class)
    .withIdentity("job1", "group1")
    .storeDurably() // Job 必須被定義為 durable 的
    .build();

scheduler.addJob(job1, false);

更新已存盤的定時任務

addJob 方法的第二個引數-replace,就是用在這里,設定為 true,那么就是更新操作,

JobDetail job1 = newJob(MyJobClass.class)
    .withIdentity("job1", "group1")
    .build();

// store, and set overwrite flag to 'true'     
scheduler.addJob(job1, true);

更新觸發器

替換已存在的 Trigger:

// 定義一個新的 Trigger
Trigger trigger = newTrigger()
    .withIdentity("newTrigger", "group1")
    .startNow()
    .build();

// 讓 Scheduler 根據 Key 去移除舊的 Trigger, 然后將新的 Trigger 放上去
scheduler.rescheduleJob(new TriggerKey("oldTrigger", "group1"), trigger);

更新已存在的 Trigger:

// 根據 Key 檢索已存在的 Trigger
Trigger oldTrigger = scheduler.getTrigger(new TriggerKey("oldTrigger", "group1");

// 獲取 TriggerBuilder
TriggerBuilder tb = oldTrigger.getTriggerBuilder();

// 更新觸發動作,并構建新的 Trigger
// (other builder methods could be called, to change the trigger in any desired way)
Trigger newTrigger = tb.withSchedule(simpleSchedule()
    .withIntervalInSeconds(10)
    .withRepeatCount(10)
    .build();

// 重新用新的 Trigger 調度 Job
scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger);

取消定時任務

使用 Scheduler 的 deleteJob 方法,入參為一個 TriggerKey,即 Trigger 標識,這樣就能取消特定的 Trigger 去觸發對應的任務,因為一個 Job 可能有多個 Trigger,

scheduler.unscheduleJob(new TriggerKey("trigger1", "group1"));

使用 Scheduler 的 deleteJob 方法,入參為一個 JobKey,即 Job 標識,這樣就能洗掉這個 Job 并取消對應的 Trigger 進行觸發,

scheduler.deleteJob(new JobKey("job1", "group1"));

獲取調度器中的所有定時任務

思路:通過 scheduler 獲取任務組,然后遍歷任務組,進而遍歷組中的任務,

// 遍歷每一個任務組
for(String group: scheduler.getJobGroupNames()) {
    // 遍歷組中的每一個任務
    for(JobKey jobKey : scheduler.getJobKeys(GroupMatcher.groupEquals(group))) {
        System.out.println("通過標識找到了 Job,標識的 Key 為: " + jobKey);
    }
}

獲取調度器中的所有觸發器

思路:同上,

// 遍歷每一個觸發器組
for(String group: scheduler.getTriggerGroupNames()) {
    // 遍歷組中的每一個觸發器
    for(TriggerKey triggerKey : scheduler.getTriggerKeys(GroupMatcher.groupEquals(group))) {
        System.out.println("通過標識找到了 Trigger,標識的 Key 為: " + triggerKey);
    }
}

獲取某一個定時任務的觸發器串列

因為一個任務可以有多個觸發器,所以是獲取觸發器串列,

List<Trigger> jobTriggers = scheduler.getTriggersOfJob(new JobKey("jobName", "jobGroup"));

總結

想要使用 Quartz,那么就引入它的依賴,

從使用上來說:

  • 對于一個任務,我們可以寫一個任務類,即實作了 Job 介面的 Java 類,并重寫 execute 方法,接著需要一個 JobDetail 來描述這個 Job,或者說把這個 Job 系結到這個 JobDetail 上,然后我們就需要一個 Trigger,這個 Trigger 是用來表示何使觸發任務的,可以說是一個執行計劃,在何時如何觸發,Trigger 是有好幾種型別的,目前常用的就是 SimpleTrigger 和 CronTrigger,最后,在把 JobDetail 和 Trigger 扔給 Scheduler,讓它去組織調度;
  • 對于一個觸發器,它有對應的型別,以及對應的 Misfire 指令,一般在創建 Trigger 的時候,就指定上這些資訊;
  • 對于它們的 CRUD,都是使用調度器進行操作的,比如往調度器中添加任務,更新任務,

從 Quartz 的設計上來說,它有涉及到多種設計模式,包括 Builder 模式,Factory 模式等等,

以上,便是本篇文章的內容,我們下期再見!

參考:http://www.quartz-scheduler.org/

最后的最后

希望各位螢屏前的靚仔靚女們給個三連!你輕輕地點了個贊,那將在我的心里世界增添一顆明亮而耀眼的星!

咱們下期再見!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548767.html

標籤:Java

上一篇:基本概念

下一篇:java魔功心法-范型篇

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more