一般的後臺管理系統都有導出報表的功能,對於大數據量的報表導出,通常比較耗時,比如管理員點擊一個導出按鈕,往往要等待很長的時間直到報表成功導出纔可以進行下一步操作,顯然這種同步的方式已經滿足不了需求了。現在實際開發中常用的方式是採用JMS消息隊列方式,發送消息到其他的系統中進行導出,或者是在項目中開啓異步線程來完成耗時的導出工作。本文將結合報表導出的場景,來講解一些Spring Boot中如何開啓異步線程。
定義線程池和開啓異步可用
Spring中存在一個接口AsyncConfigurer接口,該接口就是用來配置異步線程池的接口,它有兩個方法,getAsyncExecutor和getAsyncUncaughtExceptionHandler,第一個方法是獲取一個線程池,第二個方法是用來處理異步線程中發生的異常。它的源碼如下所示:
package org.springframework.scheduling.annotation;
import java.util.concurrent.Executor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.lang.Nullable;
public interface AsyncConfigurer {
// 獲取線程池
@Nullable
default Executor getAsyncExecutor() {
return null;
}
// 異步異常處理器
@Nullable
default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
這裏的接口提供的都是空實現,所以想要開啓異步線程機制,那麼就需要我們手動實現這個接口,將實現該接口的類標註爲Spring的配置類,那麼就開啓了Spring的異步可用,那麼Spring就會通過getAsyncExecutor來獲取一個可用的線程來執行某項異步操作,當然,整個異步的開啓還需要結合兩個註解,一個是@EnableAsync,另外一個是@Async,第一個是標註在配置類中,用來告訴Spring異步可用,第二個註解通常標註在某個方法中,當調用這個方法的時候,就會從線程池中獲取新的線程來執行它。
現在我們來定義線程池並開啓異步可用,這裏寫一個配置類AsyncConfig來實現AsyncConfigurer,代碼如下所示:
package cn.itlemon.springboot.async.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* @author jiangpingping
* @date 2018/10/30 19:28
*/
@Configuration
@EnableAsync
@Slf4j
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
// 自定義線程池
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心線程數
taskExecutor.setCorePoolSize(10);
// 最大線程數
taskExecutor.setMaxPoolSize(30);
// 線程隊列最大線程數
taskExecutor.setQueueCapacity(2000);
// 初始化線程池
taskExecutor.initialize();
return taskExecutor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("Error Occurs in async method:{}", ex.getMessage());
};
}
}
第一個方法我們定義了一個線程池,並設置了一些基本參數,比如核心線程數、最大線程數、線程隊列最大線程數等,第二個方法是處理異步線程中發生的異常,它是一個異常處理器,返回AsyncUncaughtExceptionHandler接口的實現類對象,由於AsyncUncaughtExceptionHandler是一個函數式接口(只有一個抽象方法的接口,通常使用@FunctionalInterface註解標註的接口),所以這裏使用了Lambda表達式來簡寫它的實現類對象,這裏的異步異常處理就是記錄一下日誌,並沒有做其他的邏輯操作,如果對Lambda表達式不熟悉,也可以直接使用匿名內部類的方式來創建AsyncUncaughtExceptionHandler的實現類對象,如下所示:
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncUncaughtExceptionHandler() {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
log.error("Error Occurs in async method:{}", ex.getMessage());
}
};
}
需要注意的一點的是,我們在上面的配置類中加入了@EnableAsync註解,那麼在Spring註冊該配置類爲Spring Bean的時候,就會開啓異步可用機制。
測試異步可用機制
寫一個Service層接口,用來表明生成報表:
package cn.itlemon.springboot.async.service;
import java.util.concurrent.Future;
/**
* @author jiangpingping
* @date 2018/10/30 19:32
*/
public interface AsyncService {
/**
* 模擬生成報表的異步方法
*/
void generateReport();
}
它的實現類是:
package cn.itlemon.springboot.async.service.impl;
import cn.itlemon.springboot.async.service.AsyncService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.Future;
/**
* @author jiangpingping
* @date 2018/10/30 19:33
*/
@Service
public class AsyncServiceImpl implements AsyncService {
@Override
@Async
public void generateReport() {
// 模擬異步生成報表代碼,這裏設置爲打印
System.out.println("報表線程名稱:【" + Thread.currentThread().getName() + "】");
}
}
這裏假設進行了報表的導出工作,所以使用打印語句來進行簡單的模擬,並在方法中標註了@Async註解,那麼當調用該方法的時候,Spring會獲取一個新的線程來執行這個方法,所以這裏打印出執行當前方法的線程名稱。我們在寫一個控制器,代碼如下:
package cn.itlemon.springboot.async.controller;
import cn.itlemon.springboot.async.service.AsyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/**
* @author jiangpingping
* @date 2018/10/30 19:36
*/
@RestController
@RequestMapping("/async")
@Slf4j
public class AsyncController {
private final AsyncService asyncService;
@Autowired
public AsyncController(AsyncService asyncService) {
this.asyncService = asyncService;
}
@GetMapping("/page")
public String asyncPage() {
System.out.println("當前請求線程名稱爲:【" + Thread.currentThread().getName() + "】");
// 異步調用
asyncService.generateReport();
// 返回結果
return "async";
}
}
我們在當前Controller方法中也打印了當前的線程,運行項目,訪問指定的URL,就可以對比在調用generateReport方法的時候是否啓用了新的線程。我們啓動Spring Boot應用,在瀏覽器地址欄輸入:http://localhost:8080/async/page,在控制檯打印的結果是:
當前請求線程名稱爲:【http-nio-8080-exec-1】
報表線程名稱:【ThreadPoolTaskExecutor-1】
很明顯,這不是同一個線程,說明我們開啓異步線程成功。
處理異步線程中的異常
一般在Spring中處理異步線程異常分成兩類,一類是異步方法沒有返回值,另一類是異步方法有返回值。
第一類無返回值方法
對於第一類無返回值情況,我們已經在AsyncConfig配置類中進行了配置,即實現getAsyncUncaughtExceptionHandler方法,也就是當異步線程中的代碼發生了異常,就會調用這個方法來進行異常處理,爲了檢驗,我們在AsyncServiceImpl的方法generateReport中手動加一行代碼System.out.println(1 / 0);,從而導致其出除零異常,代碼如下所示:
@Override
@Async
public void generateReport() {
// 模擬異步生成報表代碼,這裏設置爲打印
System.out.println("報表線程名稱:【" + Thread.currentThread().getName() + "】");
System.out.println(1 / 0);
}
當再次啓動Spring Boot應用,在瀏覽器地址欄輸入:http://localhost:8080/async/page,那麼將在異步流程中發生異常,由於是在不同線程中發生的異常,所以它並不會影響主線程的執行,且發生異常後,由配置了getAsyncUncaughtExceptionHandler方法,那麼該異常將會被處理,處理的方式就是使用日誌進行了記錄:
2018-10-31 10:57:09.952 ERROR 2391 --- [lTaskExecutor-1] c.i.springboot.async.config.AsyncConfig : Error Occurs in async method:/ by zero
1
第二類有返回值方法
對於第二種情況,即異步方法會有返回值,那麼我們如何獲取到異步線程處理後的返回值呢,通常的方法是將異步方法的返回值使用接口Future、ListenableFuture或者類AsyncResult進行包裝,即將返回值作爲泛型傳入到上述接口或者類中。這裏我們來簡要分析一下它們的源碼中的常用方法。
Future接口:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
方法分析:
cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning爲true還是false,此方法肯定返回false,即如果取消已經完成的任務會返回false;如果任務正在執行,若mayInterruptIfRunning設置爲true,則返回true,若mayInterruptIfRunning設置爲false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning爲true還是false,肯定返回true。
isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回true。
isDone方法表示任務是否已經完成,若任務完成,則返回true;
get方法用來獲取執行結果,這個方法會產生阻塞,會一直等到任務執行完畢才返回;
get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。
ListenableFuture接口:
public interface ListenableFuture<T> extends Future<T> {
void addCallback(ListenableFutureCallback<? super T> callback);
void addCallback(SuccessCallback<? super T> successCallback, FailureCallback failureCallback);
default CompletableFuture<T> completable() {
CompletableFuture<T> completable = new DelegatingCompletableFuture<>(this);
addCallback(completable::complete, completable::completeExceptionally);
return completable;
}
}
ListenableFuture繼承了Future接口,它還額外添加了三個方法,主要用來添加異步現場的回調,可以用來處理異常和獲取異步方法的返回值的。AsyncResult類實現了ListenableFuture接口,也實現了它所有的方法。接下來,我們將分別介紹如何獲取異步處理後的返回值和異常處理。
使用Future接口
我們在AsyncService接口中添加一個方法:returnMessage(),並使用Future接口來進行包裝,代碼如下:
/**
* 異步回調消息方法
*
* @return 字符串
*/
Future<String> returnMessage();
實現類中的代碼如下:
@Override
@Async
public Future<String> returnMessage() {
System.out.println(Thread.currentThread().getName());
String message = "Async Method Result";
return new AsyncResult<>(message);
}
那麼在Controller層,就可以獲取到Future的實現類對象,代碼如下:
@GetMapping("/page1")
public String asyncPage1() {
try {
System.out.println(Thread.currentThread().getName());
Future<String> result = asyncService.returnMessage();
System.out.println(result.get());
} catch (ExecutionException | InterruptedException e) {
log.error("發生了異常:{}", e.getMessage());
}
return "async";
}
這裏對異步進行了try...catch異常處理,也使用了Future的get方法獲取了異步方法的返回值,但是這種獲取返回值的方式會阻塞當前線程,也就是說調用了get方法之後,會等待異步線程執行完畢後才進行下一行代碼的執行。
使用ListenableFuture接口
我們在AsyncService接口中添加一個方法:returnMsg(),並使用ListenableFuture接口來進行包裝,代碼如下:
/**
* 異步回調消息方法
*
* @return 字符串
*/
ListenableFuture<String> returnMsg();
實現類中的代碼如下:
@Override
@Async
public ListenableFuture<String> returnMsg() {
System.out.println(Thread.currentThread().getName());
String message = "Async Method Result";
return new AsyncResult<>(message);
}
那麼在Controller層,就可以獲取到ListenableFuture的實現類對象,代碼如下:
@GetMapping("/page2")
public String asyncPage2() {
System.out.println(Thread.currentThread().getName());
ListenableFuture<String> result = asyncService.returnMsg();
result.addCallback(new SuccessCallback<String>() {
@Override
public void onSuccess(String result) {
System.out.println("返回的結果是:" + result);
}
}, new FailureCallback() {
@Override
public void onFailure(Throwable ex) {
log.error("發生了異常:{}", ex.getMessage());
}
});
return "async";
}
從上面的代碼中可以看出,在返回的結果中添加了兩個回調,分別是異步處理成功的回調SuccessCallback接口的實現類對象和異步處理失敗發生異常的回調FailureCallback接口的實現類對象。ListenableFuture接口是對Future接口的擴展,支持回調,有效的避免了線程阻塞問題,也就是說,它會監聽Future接口的執行情況,一旦完成,就會調用onSuccess方法進行成功後的處理,一旦發生異常,就會調用onFailure方法進行異常處理。相比較而言,更加推薦使用ListenableFuture來進行有返回值的異步處理。對於Java1.8,其實更加推薦使用CompletableFuture或者guava的ListenableFuture,感興趣的同學可以進行深入研究,他們的處理異步能力會更加強悍