Spring Boot 異步線程


一般的後臺管理系統都有導出報表的功能,對於大數據量的報表導出,通常比較耗時,比如管理員點擊一個導出按鈕,往往要等待很長的時間直到報表成功導出纔可以進行下一步操作,顯然這種同步的方式已經滿足不了需求了。現在實際開發中常用的方式是採用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,感興趣的同學可以進行深入研究,他們的處理異步能力會更加強悍
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章