天天看點

聽說可以十分鐘掌握Spring Boot 內建定時任務、異步調用?

文章目錄

    • 1. 定時任務
      • 1.1 @Scheduled-fixedRate方式
        • 1.1.1 pom配置
        • 1.1.2 加入注解
        • 1.1.3 建立測試類
        • 1.1.4 參數說明
        • 1.1.5 運作測試
      • 1.2 @Scheduled-cron方式
        • 1.2.1 修改測試類
        • 1.2.2 測試
        • 1.2.3 參數說明
    • 2. 異步調用
      • 2.1 同步調用
        • 2.1.1 定義一個Task類
        • 2.1.2 建立測試類
        • 2.1.3 測試
      • 2.2 異步調用
        • 2.2.1 修改Task類
        • 2.2.2 修改SpringbootAsyncApplication
      • 2.3 異步調用結果傳回
        • 2.3.1 改造AsyncTask
        • 2.3.2 改造測試類
        • 2.3.3 測試
        • 2.3.4 總結
      • 2.4 異步調用自定義線程池
        • 2.4.1 自定義線程池
        • 2.4.2 改造AsyncTask

1. 定時任務

在項目開發中,經常需要定時任務來幫助我們來做一些内容,比如定時發送短信/站内資訊、資料彙總統計、業務監控等,是以就要用到我們的定時任務,在Spring Boot中編寫定時任務是非常簡單的事,下面通過執行個體介紹如何在Spring Boot中建立定時任務

1.1 @Scheduled-fixedRate方式

1.1.1 pom配置

隻需要引入 Spring Boot Starter jar包即可,Spring Boot Starter 包中已經内置了定時的方法

org.springframework.boot
    spring-boot-starter           

複制

1.1.2 加入注解

在Spring Boot的主類中加入**@EnableScheduling** 注解,啟用定時任務的配置

package com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class ScheduleTaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduleTaskApplication.class, args);
    }

}           

複制

1.1.3 建立測試類

package com.task;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

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

//定時任務
@Component
public class SchedulerTask {
    private static final SimpleDateFormat f=new SimpleDateFormat("HH:mm:ss");

    @Scheduled(fixedRate = 5000)//5秒執行一次
    public void processFixedRate(){
        System.out.println("processFixedRate方式開啟定時任務:現在的時間是"+f.format(new Date()));
    }
}           

複制

1.1.4 參數說明

在上面的入門例子中,使用了@Scheduled(fixedRate = 5000) 注解來定義每過5秒執行的任務,對于@Scheduled 的使用可以總結

如下幾種方式:

  1. @Scheduled(fixedRate = 5000) :上一次開始執行時間點之後5秒再執行
  2. @Scheduled(fixedDelay = 5000) :上一次執行完畢時間點之後5秒再執行
  3. @Scheduled(initialDelay=1000, fixedRate=5000) :第一次延遲1秒後執行,之後按fixedRate的規則每5秒執行一次

1.1.5 運作測試

聽說可以十分鐘掌握Spring Boot 內建定時任務、異步調用?

1.2 @Scheduled-cron方式

還可以用另一種方式實作定時任務,隻需修改測試類即可

1.2.1 修改測試類

package com.task;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

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

//定時任務
@Component
public class SchedulerTask {
    private static final SimpleDateFormat f=new SimpleDateFormat("HH:mm:ss");

    @Scheduled(cron = "*/5 * * * * *")
    public void processFixedRate(){
        System.out.println("processFixedRate方式開啟定時任務:現在的時間是"+f.format(new Date()));
    }
}           

複制

1.2.2 測試

聽說可以十分鐘掌握Spring Boot 內建定時任務、異步調用?

1.2.3 參數說明

cron 一共有七位,最後一位是年,Spring Boot 定時方案中隻需要設定六位即可

  1. 第一位,表示秒,取值 0 ~ 59;
  2. 第二位,表示分,取值 0 ~ 59;
  3. 第三位,表示小時,取值 0 ~ 23;
  4. 第四位,日期天/日,取值 1 ~31;
  5. 第五位,日期月份,取值 1~12;
  6. 第六位,星期,取值 1 ~ 7,星期一,星期二…,注,1 表示星期 天,2 表示星期一;
  7. 第七位,年份,可以留白,取值 1970 ~ 2099

cron 中,還有一些特殊的符号,含義如下:

  1. (*)星号,可以了解為每的意思,每秒、每分、每天、每月、每年…(?)問号,問号隻能出現在日期和星期這兩個位置,表示這個位置的值不确定(-)減号,表達一個範圍,如在小時字段中使用“10 ~ 12”,則表示從 10 到 12 點,即 10、11、12
  2. (,)逗号,表達一個清單值,如在星期字段中使用“1、2、4”,則表示星期一、星期二、星期四
  3. (/)斜杠,如 x/y,x 是開始值,y 是步⻓長,比如在第一位(秒),0/15 就是從 0 秒開始,每隔 15 秒執 行一次。

下面列舉幾個常用的例子: 0 0 1 * * ? :每天淩晨1 點執行; 0 5 1 * * ?:每天 淩晨1 點 5 分執行;

2. 異步調用

2.1 同步調用

同步調用指程式按照定義順序依次執行,每一行程式都必須等待上一行程式執行完成之後才能執行

2.1.1 定義一個Task類

建立三個處理函數分别模拟三個執行任務的操作,操作消耗時間随機取(10秒内)

package com.task;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.Random;

//同步調用
@Component
public class AsyncTask {

    public static Random random = new Random();

    
    public void testTask1() throws Exception{
        System.out.println("開啟任務一");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務一消耗的時間"+(endtime-starttime)+"毫秒");
    }
   
    public void testTask2() throws Exception{
        System.out.println("開啟任務二");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務二消耗的時間"+(endtime-starttime)+"毫秒");
    }
    
    public void testTask3() throws Exception{
        System.out.println("開啟任務三");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務三消耗的時間"+(endtime-starttime)+"毫秒");

    }
}           

複制

2.1.2 建立測試類

package com;

import com.task.AsyncTask;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class ScheduleTaskApplicationTests {

    @Test
    void contextLoads() {
    }

    @Autowired
    private AsyncTask asyncTask;

    @Test
    public void testTask() throws Exception{
        asyncTask.testTask1();
        asyncTask.testTask2();
        asyncTask.testTask3();
    }

}           

複制

2.1.3 測試

聽說可以十分鐘掌握Spring Boot 內建定時任務、異步調用?

任務一、任務二、任務三順序的執行完了,換言之testTask1、testTask2、testTask3三個函數順序的執行完成。

2.2 異步調用

上述的同步調用雖然順利的執行完了三個任務,但可以看到執行時間比較長,若這三個任務本身之間不存在依賴關系,可以并發執行的話,同步調用在執行效率方面就比較差,可以考慮通過異步調用的方式來并發執行異步調用指程式在順序執行時,不等待異步調用的語句傳回結果就執行後面的程式。

在Spring Boot中,我們隻需要通過使用@Async 注解就能簡單的将原來的同步函數變為異步函數

2.2.1 修改Task類

package com.task;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.Random;

//同步調用
@Component
public class AsyncTask {

    public static Random random = new Random();

    @Async
    public void testTask1() throws Exception{
        System.out.println("開啟任務一");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務一消耗的時間"+(endtime-starttime)+"毫秒");
    }
    @Async
    public void testTask2() throws Exception{
        System.out.println("開啟任務二");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務二消耗的時間"+(endtime-starttime)+"毫秒");
    }
    @Async
    public void testTask3() throws Exception{
        System.out.println("開啟任務三");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務三消耗的時間"+(endtime-starttime)+"毫秒");

    }
}           

複制

2.2.2 修改SpringbootAsyncApplication

為了讓@Async注解能夠生效,還需要在Spring Boot的主程式中配置@EnableAsync

package com;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableAsync
public class ScheduleTaskApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduleTaskApplication.class, args);
    }

}           

複制

此時可以反複執行單元測試,你可能會遇到各種不同的結果:

  • 沒有任何任務相關的輸出
  • 有部分任務相關的輸出
  • 亂序的任務相關的輸出

原因是目前testTask1、testTask2、testTask3三個函數的時候已經是異步執行了。主程式在異步調用之後,主程式并不會理

會這三個函數是否執行完成了,由于沒有其他需要執行的内容,是以程式就自動結束了,導緻了不完整或是沒有輸出任務相關内容的

情況

2.3 異步調用結果傳回

為了讓testTask1、testTask2、testTask3 能正常結束,假設我們需要統計一下三個任務并發執行共耗時多少,這就需要等到上述三個函數都完成調動之後記錄時間,并計算結果,我們如何判斷上述三個異步調用是否已經執行完成呢?我們需要使用Future 來傳回異步調用的結果

2.3.1 改造AsyncTask

package com.task;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;

import java.util.Random;
import java.util.concurrent.Future;

//同步調用
@Component
public class AsyncTask {

    public static Random random = new Random();

    @Async
    public Future<String> testTask1() throws Exception{
        System.out.println("開啟任務一");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務一消耗的時間"+(endtime-starttime)+"毫秒");
        return new AsyncResult<>("任務一完成");
    }
    @Async
    public Future<String> testTask2() throws Exception{
        System.out.println("開啟任務二");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務二消耗的時間"+(endtime-starttime)+"毫秒");
        return new AsyncResult<>("任務二完成");
    }
    @Async
    public Future<String> testTask3() throws Exception{
        System.out.println("開啟任務三");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務三消耗的時間"+(endtime-starttime)+"毫秒");
        return new AsyncResult<>("任務三完成");
    }
}           

複制

2.3.2 改造測試類

package com;

import com.task.AsyncTask;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.Future;

@SpringBootTest
class ScheduleTaskApplicationTests {

    @Test
    void contextLoads() {
    }

    @Autowired
    private AsyncTask asyncTask;

    @Test
    public void testTask() throws Exception{
//        asyncTask.testTask1();
//        asyncTask.testTask2();
//        asyncTask.testTask3();
        Future<String> taskOne = asyncTask.testTask1();
        Future<String> taskTwo = asyncTask.testTask2();
        Future<String> taskThree = asyncTask.testTask3();

        while (true){
            if (taskOne.isDone()&&taskTwo.isDone()&&taskThree.isDone()){
                break;
            }
            Thread.sleep(10000);
        }

    }

}           

複制

2.3.3 測試

聽說可以十分鐘掌握Spring Boot 內建定時任務、異步調用?

2.3.4 總結

  • 在測試用例一開始記錄開始時間
  • 在調用三個異步函數的時候,傳回Future 類型的結果對象
  • 在調用完三個異步函數之後,開啟一個循環,根據傳回的Future 對象來判斷三個異步函數是否都結束了。若都結束,就結束循環;若沒有都結束,就等1秒後再判斷。
  • 跳出循環之後,根據結束時間 - 開始時間,計算出三個任務并發執行的總耗時

2.4 異步調用自定義線程池

開啟異步注解 @EnableAsync 方法上加 @Async 預設實作 SimpleAsyncTaskExecutor 不是真的線程池,這個類不重用線程,每次調用

都會建立一個新的線程

2.4.1 自定義線程池

package com;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@SpringBootApplication
@EnableAsync
public class SpringbootAsyncApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootAsyncApplication.class, args);
    }

    @Bean("myTaskExecutor")
    public Executor myTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);//核心線程數量,線程池建立時候初始化的線程數
        executor.setMaxPoolSize(15);//最大線程數,隻有在緩沖隊列滿了之後才會申請超過核心線程數的線程
        executor.setQueueCapacity(200);//緩沖隊列,用來緩沖執行任務的隊列
        executor.setKeepAliveSeconds(60);//當超過了核心線程數之外的線程在空閑時間到達之後會被銷毀
        executor.setThreadNamePrefix("myTask-");//設定好了之後可以友善我們定位處理任務所在的線程池
        executor.setWaitForTasksToCompleteOnShutdown(true);//用來設定線程池關閉的時候等待所有任務都完成再
繼續銷毀其他的Bean
        executor.setAwaitTerminationSeconds(60);//該方法用來設定線程池中任務的等待時間,如果超過這個時候還沒
有銷毀就強制銷毀,以確定應用最後能夠被關閉,而不是阻塞住。
        //線程池對拒絕任務的處理政策:這裡采用了CallerRunsPolicy政策,當線程池沒有處理能力的時候,該政策會直接在 
execute 方法的調用線程中運作被拒絕的任務;如果執行程式已關閉,則會丢棄該任務
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}           

複制

2.4.2 改造AsyncTask

在 @Async後面加上自定義線程池名字即可

package com.task;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;

import java.util.Random;
import java.util.concurrent.Future;

//同步調用
@Component
public class AsyncTask {

    public static Random random = new Random();

    @Async("myTaskExecutor")
    public Future<String> testTask1() throws Exception{
        System.out.println("開啟任務一");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務一消耗的時間"+(endtime-starttime)+"毫秒");
        return new AsyncResult<>("任務一完成");
    }
    @Async("myTaskExecutor")
    public Future<String> testTask2() throws Exception{
        System.out.println("開啟任務二");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務二消耗的時間"+(endtime-starttime)+"毫秒");
        return new AsyncResult<>("任務二完成");
    }
    @Async("myTaskExecutor")
    public Future<String> testTask3() throws Exception{
        System.out.println("開啟任務三");
        long starttime = System.currentTimeMillis();
        Thread.sleep(random.nextInt(1000));
        long endtime = System.currentTimeMillis();
        System.out.println("完成任務三消耗的時間"+(endtime-starttime)+"毫秒");
        return new AsyncResult<>("任務三完成");
    }
}           

複制