SpringBoot 建立定時任務、異步調用
- SpringBoot 使用定時任務@Scheduled-fixedRate方式
-
- 建立定時任務
- `@Scheduled`參數說明
- SpringBoot 使用定時任務@Scheduled-cron方式
-
- 修改 SchedulingTask(定時任務實作類)
- 參數說明
-
- cron 常用表達式例子
- SpringBoot 使用@Async實作異步調用
-
- 同步調用
- 異步調用
- Spring Boot 使用@Async 實作異步調用-異步回調結果
- Spring Boot 使用@Async 實作異步調用-自定義線程池
- 通過資料庫簡單的配置定時任務
SpringBoot 使用定時任務@Scheduled-fixedRate方式
在項目開發中,經常需要定時任務來幫助我們來做一些内容,比如定時發送短息/站内信、資料彙總統計、業務監控等。
建立定時任務
在
spring boot
中填寫定時任務是非常簡單的事,下面通過執行個體介紹如何在
spring boot
中建立定時任務
-
(隻需要引入pom 配置
jar包即可,spring-boot-starter
中已經内置了定時的方法)spring-boot-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
- 在
主類中加入spring boot
注解,啟用定時任務配置@EnableScheduling
package com.djy.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class SpringDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringDemoApplication.class, args);
}
}
- 建立
定時任務實作類
@Component
public class SchedulingTask {
private static final SimpleDateFormat f = new SimpleDateFormat("HH:mm:ss");
//五秒執行一次
@Scheduled(fixedRate = 5000)
public void processFixedRate() {
System.out.println("Scheduled-fixedRate 方式:開始定時任務,現在時間:"+f.format(new Date()));
}
}
運作程式,控制台中可以看到類似的輸出,定時任務開始正常運作了。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiYTMfhHLlN3XnxCM38FdsYkRGZkRG9lcvx2bjxSMx8VZ6l2cs0TPRVmdKJDWsJFWhlHZIFFbHFXNNtkW1okNhVTQClGVF5UMR9Fd4VGdsATNfd3bkFGazxycykFaKdkYzZUbapXNXlleSdVY2pESa9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLiZTYjFjNykTYlJjYhJzNilDM3QzNjljNiRGZlZWOkF2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
@Scheduled
參數說明
@Scheduled
在上面的例子中,使用了
@Scheduled(fixedRate = 5000)
注解來定義每5秒執行的任務,對于
@Scheduled
的使用可以總結如下幾種方式:
fixedRate 說明
-
上一次開始執行時間點之後5秒在執行@Scheduled(fixedRate = 5000)
-
上一次執行完畢時間點之後5秒執行@Scheduled(fixedDelay = 5000)
-
第一次延遲1秒後執行,之後按fixedRate的規則每5秒執行一次@Scheduled(initialDelay = 1000 ,fixedRate = 5000)
SpringBoot 使用定時任務@Scheduled-cron方式
修改 SchedulingTask(定時任務實作類)
@Component
public class SchedulingTask {
private static final SimpleDateFormat f = new SimpleDateFormat("HH:mm:ss");
// @Scheduled(fixedRate = 5000)
// public void processFixedRate() {
// System.out.println("Scheduled-fixedRate 方式:開始定時任務,現在時間:"+f.format(new Date()));
// }
@Scheduled(cron = "*/5 * * * * ?")
public void processCron() {
System.out.println("Scheduled-cron 方式:開始定時任務,現在時間:"+f.format(new Date()));
}
}
運作程式,控制台中可以看到類似的輸出,定時任務開始正常運作了。
參數說明
每一個域都使用數字,但還可以出現如下特殊字元,它們的含義是:
(1):表示比對該域的任意值。假如在Minutes域使用, 即表示每分鐘都會觸發事件。
(2)?:隻能用在DayofMonth和DayofWeek兩個域。它也比對域的任意值,但實際不會。因為DayofMonth和DayofWeek會互相影響。例如想在每月的20日觸發排程,不管20日到底是星期幾,則隻能使用如下寫法: 13 13 15 20 * ?, 其中最後一位隻能用?,而不能使用*,如果使用*表示不管星期幾都會觸發,實際上并不是這樣。
(3)-:表示範圍。例如在Minutes域使用5-20,表示從5分到20分鐘每分鐘觸發一次
(4)/:表示起始時間開始觸發,然後每隔固定時間觸發一次。例如在Minutes域使用5/20,則意味着5分鐘觸發一次,而25,45等分别觸發一次.
(5),:表示列出枚舉值。例如:在Minutes域使用5,20,則意味着在5和20分每分鐘觸發一次。
(6)L:表示最後,隻能出現在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最後的一個星期四觸發。
(7)W:表示有效工作日(周一到周五),隻能出現在DayofMonth域,系統将在離指定日期的最近的有效工作日觸發事件。例如:在 DayofMonth使用5W,如果5日是星期六,則将在最近的工作日:星期五,即4日觸發。如果5日是星期天,則在6日(周一)觸發;如果5日在星期一到星期五中的一天,則就在5日觸發。另外一點,W的最近尋找不會跨過月份 。
(8)LW:這兩個字元可以連用,表示在某個月最後一個工作日,即最後一個星期五。
(9)#:用于确定每個月第幾個星期幾,隻能出現在DayofMonth域。例如在4#2,表示某月的第二個星期三。
cron 常用表達式例子
(0)0/20 * * * * ? 表示每20秒 調整任務
(1)0 0 2 1 * ? 表示在每月的1日的淩晨2點調整任務
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15執行作業
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每個月的最後一個星期五上午10:15執行作
(4)0 0 10,14,16 * * ? 每天上午10點,下午2點,4點
(5)0 0/30 9-17 * * ? 朝九晚五工作時間内每半小時
(6)0 0 12 ? * WED 表示每個星期三中午12點
(7)0 0 12 * * ? 每天中午12點觸發
(8)0 15 10 ? * * 每天上午10:15觸發
(9)0 15 10 * * ? 每天上午10:15觸發
(10)0 15 10 * * ? * 每天上午10:15觸發
(11)0 15 10 * * ? 2005 2005年的每天上午10:15觸發
(12)0 * 14 * * ? 在每天下午2點到下午2:59期間的每1分鐘觸發
(13)0 0/5 14 * * ? 在每天下午2點到下午2:55期間的每5分鐘觸發
(14)0 0/5 14,18 * * ? 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
(15)0 0-5 14 * * ? 在每天下午2點到下午2:05期間的每1分鐘觸發
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44觸發
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15觸發
(18)0 15 10 15 * ? 每月15日上午10:15觸發
(19)0 15 10 L * ? 每月最後一日的上午10:15觸發
(20)0 15 10 ? * 6L 每月的最後一個星期五上午10:15觸發
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最後一個星期五上午10:15觸發
(22)0 15 10 ? * 6#3 每月的第三個星期五上午10:15觸發
SpringBoot 使用@Async實作異步調用
什麼是
異步調用
?
異步調用
的應得是
同步調用
,同步調用指程式按照定義順序依次執行,每一行程式都必須等待上一行下執行程式執行完成之後才執行,
異步調用
指程式在順序執行時,不等待
異步調用
的語句傳回結果裡就執行後。
同步調用
下面通過一個簡單的示例來直覺的了解什麼是同步調用:
- 定義Task類,建立三個處理函數分别模拟三個執行任務的操作,操作消耗時間随機取(10秒内)
@Component
public class MyTask {
public static Random random = new Random();
public void doTaskOne() throws Exception {
System.out.println("開始做任務一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務一,耗時:" + (end - start) + "毫秒");
}
public void doTaskTwo() throws Exception {
System.out.println("開始做任務二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務二,耗時:" + (end - start) + "毫秒");
}
public void doTaskThree() throws Exception {
System.out.println("開始做任務三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務三,耗時:" + (end - start) + "毫秒");
}
}
- 在單元測試用例中,注入Task對象,并在測試用例中執行
、doTaskOne
、doTaskTwo
三個函數。doTaskThree
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootAsyncApplicationTests {
@Test
public void contextLoads() {
}
@Autowired
private MyTask myTask;
@Test
public void testTask() throws Exception{
myTask.doTaskOne();
myTask.doTaskTwo();
myTask.doTaskThree();
}
}
- 執行單元測試,可以看到類似如下輸出:
開始做任務一
完成任務一,耗時:8653毫秒
開始做任務二
完成任務二,耗時:5215毫秒
開始做任務三
完成任務三,耗時:648毫秒
任務一、任務二、任務三順序的執行完了,換言之 doTaskOne 、 doTaskTwo 、 doTaskThree 三個函數順序的執行完成。
異步調用
上述的同步調用雖然順利的執行完了三個任務,但是可以看到執行時間比較長,若這三個任務本身之間不存在依賴關系,可以并發執行的話,同步調用在執行效率方面就比較差,可以考慮通過異步調用的方式來并發執行。
在Spring Boot中,我們隻需要通過使用
@Async
注解就能簡單的将原來的同步函數變為異步函數,Task類改在為如下模式:
@Component
public class MyTask {
public static Random random =new Random();
@Async
public void doTaskOne() throws Exception {
System.out.println("開始做任務一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務一,耗時:" + (end - start) + "毫秒");
}
@Async
public void doTaskTwo() throws Exception {
System.out.println("開始做任務二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務二,耗時:" + (end - start) + "毫秒");
}
@Async
public void doTaskThree() throws Exception {
System.out.println("開始做任務三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務三,耗時:" + (end - start) + "毫秒");
}
}
為了讓
@Async
注解能夠生效,還需要在Spring Boot的主程式中配置
@EnableAsync
,如下所示:
@SpringBootApplication
@EnableAsync
public class SpringbootAsyncApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootAsyncApplication.class, args);
}
}
此時可以反複執行單元測試,您可能會遇到各種不同的結果,比如:
- 沒有任何任務相關的輸出
- 有部分任務相關的輸出
- 亂序的任務相關的輸出
原因是目前 doTaskOne 、 doTaskTwo 、 doTaskThree 三個函數的時候已經是異步執行了。主程式在異步調用之後,主程式并不會理
會這三個函數是否執行完成了,由于沒有其他需要執行的内容,是以程式就自動結束了,導緻了不完整或是沒有輸出任務相關内容的
情況。
注: @Async所修飾的函數不要定義為static類型,這樣異步調用不會生效
Spring Boot 使用@Async 實作異步調用-異步回調結果
為了讓 doTaskOne 、 doTaskTwo 、 doTaskThree 能正常結束,假設我們需要統計一下三個任務并發執行共耗時多少,這就需要等到
上述三個函數都完成調動之後記錄時間,并計算結果。
那麼我們如何判斷上述三個異步調用是否已經執行完成呢?我們需要使用 Future 來傳回異步調用的結果,改造完成後如下:
@Component
public class MyTask {
public static Random random =new Random();
@Async
public Future<String> doTaskOne() throws Exception {
System.out.println("開始做任務一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務一,耗時:" + (end - start) + "毫秒");
return new AsyncResult<>("完成任務一");
}
@Async
public Future<String> doTaskTwo() throws Exception {
System.out.println("開始做任務二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務二,耗時:" + (end - start) + "毫秒");
return new AsyncResult<>("完成任務二");
}
@Async
public Future<String> doTaskThree() throws Exception {
System.out.println("開始做任務三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務三,耗時:" + (end - start) + "毫秒");
return new AsyncResult<>("完成任務三");
}
}
下面我們改造一下測試用例,讓測試在等待完成三個異步調用之後來做一些其他事情。
@Test
public void testTask() throws Exception{
// myTask.doTaskOne();
// myTask.doTaskTwo();
// myTask.doTaskThree();
long start = System.currentTimeMillis();
Future<String> task1 = myTask.doTaskOne();
Future<String> task2 = myTask.doTaskTwo();
Future<String> task3 = myTask.doTaskThree();
while(true) {
if(task1.isDone() && task2.isDone() && task3.isDone()) {
// 三個任務都調用完成,退出循環等待
break;
}
Thread.sleep(1000);
}
long end = System.currentTimeMillis();
System.out.println("任務全部完成,總耗時:" + (end - start) + "毫秒");
}
看看我們做了哪些改變:
- 在測試用例一開始記錄開始時間
- 在調用三個異步函數的時候,傳回 Future 類型的結果對象
-
在調用完三個異步函數之後,開啟一個循環,根據傳回的 Future 對象來判斷三個異步函數是否都結束了。若都結
束,就結束循環;若沒有都結束,就等1秒後再判斷。
- 跳出循環之後,根據結束時間 - 開始時間,計算出三個任務并發執行的總耗時。
執行一下上述的單元測試,可以看到如下結果
開始做任務二
開始做任務一
開始做任務三
完成任務二,耗時:1904毫秒
完成任務三,耗時:1914毫秒
完成任務一,耗時:4246毫秒
任務全部完成,總耗時:5008毫秒
Spring Boot 使用@Async 實作異步調用-自定義線程池
開啟異步注解
@EnableAsync
方法上加
@Async
預設實作
SimpleAsyncTaskExecutor
不是真的線程池,這個類不重用線程,每次調用
都會建立一個新的線程
- 配置線程池
@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;
}
- 改造MyTask
@Component
public class MyTask {
public static Random random =new Random();
@Async("myTaskExecutor")
public Future<String> doTaskOne() throws Exception {
System.out.println("開始做任務一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務一,耗時:" + (end - start) + "毫秒");
return new AsyncResult<>("完成任務一");
}
@Async("myTaskExecutor")
public Future<String> doTaskTwo() throws Exception {
System.out.println("開始做任務二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務二,耗時:" + (end - start) + "毫秒");
return new AsyncResult<>("完成任務二");
}
@Async("myTaskExecutor")
public Future<String> doTaskThree() throws Exception {
System.out.println("開始做任務三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務三,耗時:" + (end - start) + "毫秒");
return new AsyncResult<>("完成任務三");
}
}
執行一下上述的單元測試,可以看到如下結果:
開始做任務二
開始做任務三
開始做任務一
完成任務一,耗時:1090毫秒
完成任務三,耗時:4808毫秒
完成任務二,耗時:5942毫秒
任務全部完成,總耗時:6018毫秒
通過資料庫簡單的配置定時任務
package com.example.scheduledTask;
import com.example.mybatis.dao.SysTaskMapper;
import com.example.mybatis.model.SysTask;
import com.example.mybatis.model.SysTaskExample;
import com.example.util.SpringUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Lazy(value = false)
@Component
public class SysTaskConfig implements SchedulingConfigurer {
protected static Logger logger = LoggerFactory.getLogger(SysTaskConfig.class);
private SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Resource
private SysTaskMapper sysTaskMapper;
//從資料庫裡取得所有要執行的定時任務
private List<SysTask> getAllTasks() {
SysTaskExample example=new SysTaskExample();
example.createCriteria().andIsDeleteEqualTo((byte) 0);
return sysTaskMapper.selectByExample(example);
}
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
List<SysTask> tasks=getAllTasks();
logger.info("定時任務啟動,預計啟動任務數量="+tasks.size()+"; time="+sdf.format(new Date()));
//校驗資料(這個步驟主要是為了列印日志,可以省略)
checkDataList(tasks);
//通過校驗的資料執行定時任務
int count = 0;
if(tasks.size()>0) {
for (int i = 0; i < tasks.size(); i++) {
try {
taskRegistrar.addTriggerTask(getRunnable(tasks.get(i)), getTrigger(tasks.get(i)));
count++;
} catch (Exception e) {
logger.error("定時任務啟動錯誤:" + tasks.get(i).getClassName() + ";" + tasks.get(i).getMethodName() + ";" + e.getMessage());
}
}
}
logger.info("定時任務實際啟動數量="+count+"; time="+sdf.format(new Date()));
};
private Runnable getRunnable(SysTask task){
return new Runnable() {
@Override
public void run() {
try {
Object obj = SpringUtil.getBean(task.getClassName());
Method method = obj.getClass().getMethod(task.getMethodName(),null);
method.invoke(obj);
} catch (InvocationTargetException e) {
logger.error("定時任務啟動錯誤,反射異常:"+task.getClassName()+";"+task.getMethodName()+";"+ e.getMessage());
} catch (Exception e) {
logger.error(e.getMessage());
}
}
};
}
private Trigger getTrigger(SysTask task){
return new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
//将Cron 0/1 * * * * ? 輸入取得下一次執行的時間
CronTrigger trigger = new CronTrigger(task.getCron());
Date nextExec = trigger.nextExecutionTime(triggerContext);
return nextExec;
}
};
}
private List<SysTask> checkDataList(List<SysTask> list) {
String errMsg="";
for(int i=0;i<list.size();i++){
if(!checkOneData(list.get(i)).equalsIgnoreCase("success")){
errMsg+=list.get(i).getTaskName()+";";
list.remove(list.get(i));
i--;
};
}
if(!StringUtils.isBlank(errMsg)){
errMsg="未啟動的任務:"+errMsg;
logger.error(errMsg);
}
return list;
}
private String checkOneData(SysTask task){
String result="success";
Class cal= null;
try {
cal = Class.forName(task.getClassName());
Object obj =SpringUtil.getBean(cal);
Method method = obj.getClass().getMethod(task.getMethodName(),null);
String cron=task.getCron();
if(StringUtils.isBlank(cron)){
result="定時任務啟動錯誤,無cron:"+task.getTaskName();
logger.error(result);
}
} catch (ClassNotFoundException e) {
result="定時任務啟動錯誤,找不到類:"+task.getClassName()+ e.getMessage();
logger.error(result);
} catch (NoSuchMethodException e) {
result="定時任務啟動錯誤,找不到方法,方法必須是public:"+task.getClassName()+";"+task.getMethodName()+";"+ e.getMessage();
logger.error(result);
} catch (Exception e) {
logger.error(e.getMessage());
}
return result;
}
}
資料庫配置
執行的方法