Spring Boot異步請求處理框架
1、前言
? 在Spring Boot專案中,經常會遇到處理時間過長,導致出現HTTP請求超時問題,狀態碼:502,
? 例如一個檔案匯入介面需要匯入一個Excel檔案的學員記錄,原來是針對一個班的學員,最多500條記錄,1分鐘的HTTP超時時長之內基本可以回應,現在將很多班級的學員混在一起,放在一個Excel檔案中(這樣可以提高操作人員的作業效率),比如5萬條學員記錄,于是就出現HTTP請求超時問題,
? ?解決方案有:1)Ajax異步請求方式;2)WebSocket方式;3)異步請求處理方式:請求+輪詢,
? 方案1,需要調整HTTP超時設定,Spring Boot開啟異步處理(使用@EnableAsync和@Async),這種方式,問題是超時時長要設定多大,沒有底,
? 方案2,需要前端支持Web2.0,對瀏覽器有所限制,代碼也變得復雜,
? 方案3,使用異步請求處理,所謂異步請求處理,就是將請求異步化,前端發起請求后,后端很快就回應,回傳一個任務ID,前端再用這個任務ID去輪詢,獲取處理行程和結果資訊,需要兩個介面:任務請求介面和任務資訊輪詢介面,
? 顯然,對于長時間的業務處理,通過輪詢,獲取處理行程資訊,可以獲得較好的用戶體驗,正如大檔案下載,用戶可以了解下載的進度一樣,業務處理同樣可以通過輸出處理日志資訊和進度,使得長時間業務處理程序可視化,而不至于讓用戶長時間面對一個在空轉滑鼠符號,
? 本文針對方案3,提出一種通用的處理框架,使用這個通用的異步請求處理框架,可以適應各種不同的需要長時間異步處理的業務需求,
2、異步請求處理框架描述
? 本異步請求處理框架,主要包括任務資訊、任務執行類(Runnable)、任務管理器,
2.1、任務資訊物件類TaskInfo
? 任務資訊物件,用于存盤任務資訊,其生命周期為:創建任務==>加入任務佇列==>加入執行緒池作業執行緒佇列==>任務執行==>任務執行完成==>任務物件快取超期銷毀,
? 1)任務識別資訊:
? 1.1)任務ID:任務ID用于識別任務資訊,一個任務ID對應一個任務,是全域唯一的,不區分任務型別,
? 1.2)任務名稱:即任務型別的名稱,對應于業務處理型別名稱,如查詢商品單價、查詢商品庫存等,這樣可方便可視化識別任務,一個任務名稱可以有多個任務實體,
? 1.3)會話ID(sessionId):用于身份識別,這樣只有請求者才能根據回傳的任務ID來查詢任務資訊,其它用戶無權訪問,想象一下同步請求,誰發起請求,回應給誰,
? 2)任務呼叫資訊:使得任務管理器可以使用反射方法,呼叫任務處理方法,
? 2.1)任務處理物件:這是業務處理物件,為Object型別,一般為Service實作類物件,
? 2.2)任務處理方法:這是一個Method物件型別,為異步處理的業務處理方法物件,這個方法必須是public的方法,
? 2.3)任務方法引數:這是一個Map<String,Object>型別字典物件,可適應任意引數結構,
? 3)任務處理程序和結果相關資訊:可以提供任務處理程序和結果可視化的資訊,
? 3.1)任務狀態:表示任務目前的處理狀態,0-未處理,1-處理中,2-處理結束,
? 3.2)處理日志:這是一個List<String>型別的字串串列,用于存放處理日志,處理日志格式化:"time level taskId taskName --- logInfo",便于前端展示,
? 3.3)處理進度百分比:這是double型別資料,0.0-100.0,業務單元可視需要使用,
? 3.4)處理結果:這是一個Object型別物件,真實資料型別由業務單元約定,在未處理結束前,該值為null,處理結束后,如有回傳值,此時賦值,
? 3.5)回傳碼:業務處理,可能遇到例外,如需設定回傳碼,此處賦值,
? 3.6)回傳訊息:與回傳碼相聯系的提示資訊,
? 3.7)開始處理時間戳:在任務啟動(開始執行時)設定,用于計算業務處理的耗時時長,
? 4)任務快取到期時間:任務處理完成后,任務資訊會快取一段時間(如60秒),等待前端獲取,超期后,任務物件被銷毀,意味著再也無法獲取任務資訊了,后端系統不可能累積存放超期的任務資訊,否則可能導致OOM(Out Of Memory)例外,
2.2、任務執行類TaskRunnable
? 任務執行類,實作Runnable介面,是為執行緒池的作業執行緒提供處理方法,
? 任務執行類,使用任務資訊物件作為引數,并呼叫任務資訊的任務呼叫資訊,使用反射方法,來執行任務處理,
2.3、任務管理器類TaskManService
? 任務管理器,全域物件,使用@Service注解,加入Spring容器,這樣,任何需要異步處理的業務都可以訪問任務管理器,
? 任務管理器,包含下列屬性:
? 1)任務佇列:LinkedBlockingQueue<TaskInfo>型別,考慮到OOM問題,容量使用有限值,如1萬,即最大快取1萬個任務,相當于二級快取,
? 2)任務資訊字典:Map<Integer,TaskInfo>型別,key為taskId,目的是為了方便根據taskId快速查詢任務資訊,
? 3)執行緒池:ThreadPoolExecutor型別,作業執行緒佇列長度為執行緒池的最大執行緒數,相當于一級快取,可以設定核心執行緒數,最大執行緒數,作業執行緒佇列長度等引數,如設定核心執行緒數為5,最大執行緒數為100,作業執行緒佇列長度為100,執行緒工廠ThreadFactory使用Executors.defaultThreadFactory(),
? 4)任務ID計數器:AtomicInteger型別,用于分配唯一的任務ID,
? 5)監視執行緒:用于任務調度,以及檢查快取到期時間超期的已結束任務資訊,
? 6)監視執行緒的執行類物件:Runnable物件,提供監視執行緒的執行方法,
? 7)上次檢查時間戳:用于檢查快取到期時間,每秒1次檢查,
? 任務管理器,包含下列介面方法:
? 1)添加任務:addTask,獲取sessionId,檢查任務處理物件、方法及引數是否為null,然后分配任務ID,創建任務物件,加入任務佇列,如果引數為void,也需要構造一個空的Map<String,Object>字典物件,如果任務佇列未滿,就將任務加入任務佇列中,并回傳包含任務ID的字典,否則拋出“任務佇列已滿”的例外資訊,
? 2)獲取任務資訊:getTaskInfo,引數為request和任務ID,如果sessionId與請求時相同,且任務物件能在任務資訊字典中找到,就回傳任務資訊物件,否則拋出相關例外,
? 任務管理器的核心方法:
? 1)初始化:使用@PostConstruct注解,啟動監視執行緒,并預啟動執行緒池的一個核心執行緒,
? 2)監視執行緒的執行類run方法:實作每秒一次的超期已處理結束的任務資訊的檢查,以及任務調度,任務調度方法:
? 2.1)如果任務佇列非空,且執行緒池未滿,則取出一個任務資訊物件,并創建一個任務執行類物件,加入到執行緒池的作業執行緒佇列(execute方法加入),
? 2.2)如果任務佇列非空,且執行緒池已滿,則等待100毫秒,
? 2.3)如果任務佇列為空,則等待100毫秒,
3、異步請求處理框架代碼
3.1、任務資訊物件類TaskInfo
? 任務資訊物件類TaskInfo,代碼如下:
package com.abc.example.asyncproc;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* @className : TaskInfo
* @description : 任務資訊
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/17 1.0.0 sheng.zheng 初版
*
*/
@Data
public class TaskInfo {
// ////////////////////////////////////////////////
// 任務識別資訊
// 任務ID
private Integer taskId = 0;
// sessionId,用于識別請求者
private String sessionId = "";
// 任務名稱,即業務處理的名稱,如查詢商品最低價,匯入學員名冊
private String taskName = "";
// ////////////////////////////////////////////////
// 任務執行相關的
// 請求引數,使用字典進行封裝,以便適應任意資料結構
private Map<String, Object> params;
// 處理物件,一般是service物件
private Object procObject;
// 處理方法
private Method method;
// ////////////////////////////////////////////////
// 任務處理產生的資料,中間資料,結果
// 處理狀態,0-未處理,1-處理中,2-處理結束
private int procStatus = 0;
// 處理結果,資料型別由業務單元約定
private Object result;
// 處理日志,包括中間結果,格式化顯示:Time level taskId taskName logInfo
private List<String> logList = new ArrayList<String>();
// 處理進度百分比
private double progress = 0;
// 到期時間,UTC,任務完成后才設定,超時后銷毀
private long expiredTime = 0;
// 回傳碼,保留,0表示操作成功
private int resultCode = 0;
// 回應訊息,保留
private String message = "";
// 開始處理時間,便于統計任務處理時長
private long startTime = 0;
// ////////////////////////////////////////////////
// 日志相關的方法
private DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
// 添加處理日志
public void addLogInfo(String level,String logInfo) {
// 格式化顯示:Time level taskId taskName logInfo
LocalDateTime current = LocalDateTime.now();
String strCurrent = current.format(df);
String log = String.format("%s %s %d %s --- %s",
strCurrent,level,taskId,taskName,logInfo);
logList.add(log);
}
// ////////////////////////////////////////////////
// 不同狀態的引數設定介面
// 設定任務初始化,未開始
public void init(Integer taskId,String taskName,String sessionId,
Object procObject,Method method,Map<String, Object> params) {
this.procStatus = 0;
this.taskId = taskId;
this.taskName = taskName;
this.sessionId = sessionId;
this.procObject = procObject;
this.method = method;
this.params = params;
}
// 啟動任務
public void start() {
this.procStatus = 1;
addLogInfo(TaskConstants.LEVEL_INFO,"開始處理任務...");
// 記錄任務開始處理的時間
startTime = System.currentTimeMillis();
}
// 結束任務
public void finish(Object result) {
this.result = result;
this.procStatus = 2;
// 設定結果快取的到期時間
long current = System.currentTimeMillis();
this.expiredTime = current + TaskConstants.PROC_EXPIRE_TIME;
long duration = 0;
double second = 0.0;
duration = current - startTime;
second = duration / 1000.0;
addLogInfo(TaskConstants.LEVEL_INFO,"任務處理結束,耗時(s):"+second);
}
// 處理例外
public void error(int resultCode,String message) {
this.resultCode = resultCode;
this.message = message;
this.procStatus = 2;
}
}
? 說明:任務資訊物件類TaskInfo提供了幾個常用的處理方法,如addLogInfo、init、start、finish、error,便于簡化屬性值設定,
3.2、任務執行類TaskRunnable
? 任務執行類TaskRunnable,代碼如下:
package com.abc.example.asyncproc;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import com.abc.example.common.utils.LogUtil;
import com.abc.example.exception.BaseException;
import com.abc.example.exception.ExceptionCodes;
/**
* @className : TaskRunnable
* @description : 可被執行緒執行的任務執行類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/17 1.0.0 sheng.zheng 初版
*
*/
public class TaskRunnable implements Runnable {
// 任務資訊
private TaskInfo taskInfo;
public TaskRunnable(TaskInfo taskInfo) {
this.taskInfo = taskInfo;
}
// 獲取任務ID
public Integer getTaskId() {
if (taskInfo != null) {
return taskInfo.getTaskId();
}
return 0;
}
@Override
public void run() {
Object procObject = taskInfo.getProcObject();
Method method = taskInfo.getMethod();
try {
// 使用反射方法,呼叫方法來處理任務
method.invoke(procObject, taskInfo);
}catch(BaseException e) {
// 優先處理業務處理例外
taskInfo.error(e.getCode(),e.getMessage());
LogUtil.error(e);
}catch(InvocationTargetException e) {
taskInfo.error(ExceptionCodes.ERROR.getCode(),e.getMessage());
LogUtil.error(e);
}catch(IllegalAccessException e) {
taskInfo.error(ExceptionCodes.ERROR.getCode(),e.getMessage());
LogUtil.error(e);
}catch(IllegalArgumentException e) {
taskInfo.error(ExceptionCodes.ERROR.getCode(),e.getMessage());
LogUtil.error(e);
}catch(Exception e) {
// 最后處理未知例外
taskInfo.error(ExceptionCodes.ERROR.getCode(),e.getMessage());
LogUtil.error(e);
}
}
}
3.3、任務常量類TaskConstants
? 任務常量類TaskConstants,提供異步請求處理框架模塊的相關常量設定,代碼如下:
package com.abc.example.asyncproc;
/**
* @className : TaskConstants
* @description : 任務處理相關常量
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/18 1.0.0 sheng.zheng 初版
*
*/
public class TaskConstants {
// 任務快取過期時間,單元毫秒,即任務處理完成后,設定此時長,超期銷毀
public static final int PROC_EXPIRE_TIME = 60000;
// 執行緒池核心執行緒數
public static final int CORE_POOL_SIZE = 5;
// 執行緒池最大執行緒數
public static final int MAX_POOL_SIZE = 100;
// 執行緒池KeepAlive引數,單位秒
public static final long KEEP_ALIVE_SECONDS = 10;
// 任務佇列最大數目
public static final int MAX_TASK_NUMS = 10000;
// 日志資訊告警等級
public static final String LEVEL_INFO = "INFO";
public static final String LEVEL_ERROR = "ERROR";
}
3.4、任務管理器類TaskManService
? 任務管理器類TaskManService,代碼如下:
package com.abc.example.asyncproc;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Service;
import com.abc.example.common.utils.LogUtil;
import com.abc.example.exception.BaseException;
import com.abc.example.exception.ExceptionCodes;
/**
* @className : TaskManService
* @description : 任務管理器
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/18 1.0.0 sheng.zheng 初版
*
*/
@Service
public class TaskManService {
// 任務佇列,考慮OOM(Out Of Memory)問題,限定任務佇列長度,相當于二級快取
private BlockingQueue<TaskInfo> taskQueue =
new LinkedBlockingQueue<TaskInfo>(TaskConstants.MAX_TASK_NUMS);
// 任務資訊字典,key為taskId,目的是為了方便根據taskId查詢任務資訊
private Map<Integer,TaskInfo> taskMap = new HashMap<Integer,TaskInfo>();
// 執行緒池,作業執行緒佇列長度為執行緒池的最大執行緒數,相當于一級快取
private ThreadPoolExecutor executor = new ThreadPoolExecutor(
TaskConstants.CORE_POOL_SIZE,
TaskConstants.MAX_POOL_SIZE,
TaskConstants.KEEP_ALIVE_SECONDS,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(TaskConstants.MAX_POOL_SIZE),
Executors.defaultThreadFactory());
// 任務ID計數器,累加
private AtomicInteger taskIdCounter = new AtomicInteger();
// 用于快取上次檢查時間
private long lastTime = 0;
// 監視執行緒,用于任務調度,以及檢查已結束任務的快取到期時間
private Thread monitor;
@PostConstruct
public void init(){
// 啟動執行緒實體
monitor = new Thread(checkRunnable);
monitor.start();
// 啟動一個核心執行緒
executor.prestartCoreThread();
}
// 檢查已結束任務的快取到期時間,超期的銷毀
private Runnable checkRunnable = new Runnable() {
@Override
public void run() {
while (true) {
long current = System.currentTimeMillis();
if(current - lastTime >= 1000) {
// 離上次檢查時間超過1秒
checkAndremove();
// 更新lastTime
lastTime = current;
}
synchronized(this) {
try {
// 檢查任務佇列
if(taskQueue.isEmpty()) {
// 如果任務佇列為空,則等待100ms
Thread.sleep(100);
}else {
// 如果任務佇列不為空
// 檢查執行緒池佇列
if (executor.getQueue().size() < TaskConstants.MAX_POOL_SIZE) {
// 如果執行緒池佇列未滿
// 從任務佇列中獲取一個任務
TaskInfo taskInfo = taskQueue.take();
// 創建Runnable物件
TaskRunnable tr = new TaskRunnable(taskInfo);
// 呼叫執行緒池執行任務
executor.execute(tr);
}else {
// 如果執行緒池佇列已滿,則等待100ms
Thread.sleep(100);
}
}
}catch (InterruptedException e) {
LogUtil.error(e);
}
}
}
}
};
/**
*
* @methodName : checkAndremove
* @description : 檢查并移除過期物件
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/15 1.0.0 sheng.zheng 初版
*
*/
private void checkAndremove() {
synchronized(taskMap) {
if (taskMap.size() == 0) {
// 如果無物件
return;
}
long current = System.currentTimeMillis();
Iterator<Map.Entry<Integer,TaskInfo>> iter = taskMap.entrySet().iterator();
while(iter.hasNext()) {
Map.Entry<Integer,TaskInfo> entry = iter.next();
TaskInfo taskInfo = entry.getValue();
long expiredTime = taskInfo.getExpiredTime();
if ((expiredTime != 0) && ((current - expiredTime) > TaskConstants.PROC_EXPIRE_TIME)) {
// 如果過期,移除
iter.remove();
}
}
}
}
/**
*
* @methodName : addTask
* @description : 添加任務
* @param request : request物件
* @param taskName : 任務名稱
* @param procObject : 處理物件
* @param method : 處理方法
* @param params : 方法引數,透明傳遞到處理方法中
* @return : 處理ID,唯一標識該請求的處理
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/19 1.0.0 sheng.zheng 初版
*
*/
public Integer addTask(HttpServletRequest request,
String taskName,Object procObject,Method method,
Map<String, Object> params) {
// 獲取sessionId
String sessionId = null;
if (request.getSession() != null) {
sessionId = request.getSession().getId();
}else {
// 無效的session
throw new BaseException(ExceptionCodes.SESSION_IS_NULL);
}
// 空指標保護
if (procObject == null) {
throw new BaseException(ExceptionCodes.ARGUMENTS_ERROR,"procObject物件為null");
}
if (method == null) {
throw new BaseException(ExceptionCodes.ARGUMENTS_ERROR,"method物件為null");
}
if (params == null) {
throw new BaseException(ExceptionCodes.ARGUMENTS_ERROR,"params物件為null");
}
// 獲取可用的任務ID
Integer taskId = taskIdCounter.incrementAndGet();
// 生成任務處理資訊物件
TaskInfo item = new TaskInfo();
// 初始化任務資訊
item.init(taskId,taskName,sessionId,procObject,method,params);
// 加入處理佇列
try {
synchronized(taskQueue) {
taskQueue.add(item);
}
}catch(IllegalStateException e) {
// 佇列已滿
throw new BaseException(ExceptionCodes.ADD_OBJECT_FAILED,"任務佇列已滿");
}
// 加入字典
synchronized(taskMap) {
taskMap.put(taskId, item);
}
return taskId;
}
/**
*
* @methodName : getTaskInfo
* @description : 獲取任務資訊
* @param request : request物件
* @param taskId : 任務ID
* @return : TaskInfo物件
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/19 1.0.0 sheng.zheng 初版
*
*/
public TaskInfo getTaskInfo(HttpServletRequest request,Integer taskId) {
TaskInfo item = null;
synchronized(taskMap) {
if (taskMap.containsKey(taskId)) {
item = taskMap.get(taskId);
String sessionId = request.getSession().getId();
if (!sessionId.equals(item.getSessionId())) {
throw new BaseException(ExceptionCodes.TASKID_NOT_RIGHTS);
}
}else {
throw new BaseException(ExceptionCodes.TASKID_NOT_EXIST);
}
}
return item;
}
}
3.5、例外處理類BaseException
? 例外處理類BaseException,代碼如下:
package com.abc.example.exception;
import lombok.Data;
/**
* @className : BaseException
* @description : 例外資訊基類
* @summary : 可以處理系統例外和自定義例外
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
@Data
public class BaseException extends RuntimeException{
private static final long serialVersionUID = 4359709211352401087L;
// 例外碼
private int code ;
// 例外資訊ID
private String messageId;
// 例外資訊
private String message;
// =============== 以下為各種建構式,多載 ===================================
public BaseException(String message) {
this.message = message;
}
public BaseException(String message, Throwable e) {
this.message = message;
}
public BaseException(int code, String message) {
this.message = message;
this.code = code;
}
public BaseException(ExceptionCodes e) {
this.code = e.getCode();
this.messageId = e.getMessageId();
this.message = e.getMessage();
}
public BaseException(ExceptionCodes e,String message) {
this.code = e.getCode();
this.messageId = e.getMessageId();
this.message = e.getMessage() + ":" + message;
}
public BaseException(int code, String message, Throwable e) {
this.message = message;
this.code = code;
}
}
3.6、例外資訊列舉類ExceptionCodes
? 例外資訊列舉類ExceptionCodes,代碼如下:
package com.abc.example.exception;
/**
* @className : ExceptionCodes
* @description : 例外資訊列舉類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
public enum ExceptionCodes {
// 0-99,reserved for common exception
SUCCESS(0, "message.SUCCESS", "操作成功"),
FAILED(1, "message.FAILED", "操作失敗"),
ERROR(99, "message.ERROR", "操作例外"),
ARGUMENTS_ERROR(2, "message.ARGUMENTS_ERROR","引數錯誤"),
TASKID_NOT_EXIST(16, "message.TASKID_NOT_EXIST","任務ID不存在,可能已過期銷毀"),
TASKID_NOT_RIGHTS(17, "message.TASKID_NOT_RIGHTS","無權訪問此任務ID"),
SESSION_IS_NULL(18, "message.SESSION_IS_NULL","session為空,請重新登錄"),
ARGUMENTS_IS_EMPTY(22, "message.ARGUMENTS_IS_EMPTY","引數值不能為空"),
ADD_OBJECT_FAILED(30, "message.ADD_OBJECT_FAILED", "新增物件失敗"),
; // 定義結束
// 回傳碼
private int code;
public int getCode() {
return this.code;
}
// 回傳訊息ID
private String messageId;
public String getMessageId() {
return this.messageId;
}
// 回傳訊息
private String message;
public String getMessage() {
return this.message;
}
ExceptionCodes(int code, String messageId, String message) {
this.code = code;
this.messageId = messageId;
this.message = message;
}
}
3.7、通用例外處理類UniveralExceptionHandler
? 通用例外處理類UniveralExceptionHandler,這是一個例外資訊捕獲的攔截器,代碼如下:
package com.abc.example.exception;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @className : UniveralExceptionHandler
* @description : 通用例外處理類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
@ControllerAdvice
public class UniveralExceptionHandler {
Logger logger = LoggerFactory.getLogger(getClass());
/**
*
* @methodName : handleException
* @description : 攔截非業務例外
* @param e : Exception型別的例外
* @return : JSON格式的例外資訊
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public Map<String,Object> handleException(Exception e) {
//將例外資訊寫入日志
logger.error(e.getMessage(), e);
//輸出通用錯誤代碼和資訊
Map<String,Object> map = new HashMap<>();
map.put("code", ExceptionCodes.ERROR.getCode());
map.put("message", ExceptionCodes.ERROR.getMessage());
return map;
}
/**
*
* @methodName : handleBaseException
* @description : 攔截業務例外
* @param e : BaseException型別的例外
* @return : JSON格式的例外資訊
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
@ResponseBody
@ExceptionHandler(BaseException.class)
public Map<String,Object> handleBaseException(BaseException e) {
//將例外資訊寫入日志
logger.error("業務例外:code:{},messageId:{},message:{}", e.getCode(), e.getMessageId(), e.getMessage());
//輸出錯誤代碼和資訊
Map<String,Object> map = new HashMap<>();
map.put("code", e.getCode());
map.put("message" ,e.getMessage());
return map;
}
}
3.8、日志工具類LogUtil
? 日志工具類LogUtil,相關方法代碼如下:
package com.abc.example.common.utils;
import lombok.extern.slf4j.Slf4j;
/**
* @className : LogUtil
* @description : 日志工具類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
@Slf4j
public class LogUtil {
/**
*
* @methodName : error
* @description : 輸出例外資訊
* @param e : Exception物件
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
public static void error(Exception e) {
e.printStackTrace();
String ex = getString(e);
log.error(ex);
}
/**
*
* @methodName : getString
* @description : 獲取Exception的getStackTrace資訊
* @param ex : Exception物件
* @return : 錯誤呼叫堆疊資訊
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
public static String getString(Exception ex) {
StringBuilder stack = new StringBuilder();
StackTraceElement[] sts = ex.getStackTrace();
for (StackTraceElement st : sts) {
stack.append(st.toString()).append("\r\n");
}
return stack.toString();
}
}
4、異步請求處理測驗例子
? 下面使用一個測驗例子,來說明如何使用此框架,
4.1、異步任務的業務處理類
? 假設有一個測驗任務服務類TestTaskService,簡單起見,不用介面類了,直接就是可實體化的類,這個類有一個需要異步處理的方法,方法名為testTask,
? testTask方法只接受TaskInfo型別的引數,但實際引數params為Map字典(相當于JSON物件),包含repeat和delay,這兩個引數是testTask方法所需要的,處理結果result此處為字串型別,這個型別在實際處理時可以是任意型別,只需要與前端有約定即可,
? 為方便控制器呼叫,TestTaskService提供兩個介面方法:addAsyncTask和getTaskInfo,
? addAsyncTask方法,有2個引數,request和請求引數params,請求引數params是控制器@RequestBody的請求引數,或者重新封裝的適應testTask處理的引數,對于業務處理類TestTaskService來說,testTask方法需要什么形式和型別的引數,屬于內部約定,只要兩者匹配即可,本例子比較簡單,直接透傳HTTP請求引數,作為任務處理的方法引數,addAsyncTask方法,執行輸入引數校驗(不要等執行任務時,再去校驗引數),然后呼叫任務管理器addTask方法,加入一個任務,并獲取任務ID,回傳前端,
? getTaskInfo方法,有2個引數,request和請求引數params,請求引數params包含任務ID引數,呼叫任務管理器的getTaskInfo方法,獲取TaskInfo物件,然后屏蔽一些不需要展示的資訊,回傳前端,getTaskInfo方法用于前端輪詢,查詢任務執行程序和結果,
? 測驗任務服務類TestTaskService,代碼如下:
package com.abc.example.asyncproc;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.abc.example.common.utils.LogUtil;
import com.abc.example.common.utils.Utility;
import com.abc.example.exception.BaseException;
import com.abc.example.exception.ExceptionCodes;
/**
* @className : TestTaskService
* @description : 測驗任務服務類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/19 1.0.0 sheng.zheng 初版
*
*/
@Service
public class TestTaskService {
// 任務管理器
@Autowired
private TaskManService taskManService;
/**
*
* @methodName : addAsyncTask
* @description : 新增一個異步任務
* @summary : 新增測驗任務型別的異步任務,
* 如果處理佇列未滿,可立即獲取任務ID:
* 根據此任務ID,可以通過呼叫getTaskInfo,獲取任務的處理進度資訊;
* 如果任務處理完畢,任務資訊快取60秒,過期后無法再獲取;
* 如果處理佇列已滿,回傳任務佇列已滿的失敗提示,
* @param request : request物件
* @param params : 請求引數,形式如下:
* {
* "repeat" : 10, // 重復次數,默認為10,可選
* "delay" : 1000, // 延時毫秒數,默認為1000,可選
* }
* @return : JSON物件,形式如下:
* {
* "taskId" : 1, // 任務ID
* }
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/19 1.0.0 sheng.zheng 初版
*
*/
public Map<String, Object> addAsyncTask(HttpServletRequest request,
Map<String, Object> params){
// 引數校驗
Integer repeat = (Integer)params.get("repeat");
if (repeat == null) {
repeat = 10;
}
Integer delay = (Integer)params.get("delay");
if (delay == null) {
delay = 1000;
}
if (repeat <= 0) {
// 引數錯誤
throw new BaseException(ExceptionCodes.ARGUMENTS_ERROR,"repeat");
}
if (delay <= 10) {
// 引數錯誤
throw new BaseException(ExceptionCodes.ARGUMENTS_ERROR,"delay");
}
// 任務名稱
String taskName = "測驗任務";
// 任務處理物件
Object procObject = this;
// 任務執行方法
Method method = Utility.getMethodByName(this,"testTask");
// 呼叫任務管理器,添加任務
Integer taskId = taskManService.addTask(request, taskName, procObject, method, params);
// 回傳值處理
Map<String, Object> map = new HashMap<String, Object>();
map.put("taskId", taskId);
return map;
}
/**
*
* @methodName : getTaskInfo
* @description : 根據任務ID,獲取任務資訊
* @summary : 如果任務ID對應的任務,屬于當前用戶,則可以有權獲取資訊,否則拒絕,
* 如果任務狀態為未處理或處理中,可以獲取任務資訊,
* 如果任務狀態為處理結束,且在快取到期時間之前,也可以獲取任務資訊,否則,無法獲取任務資訊,
* @param request : request物件
* @param params : 請求引數,形式如下:
* {
* "taskId" : 1, // 任務ID,必選
* }
* @return : JSON物件,形式如下:
* {
* "taskId" : 1, // 任務ID
* "procStatus": 1, // 處理狀態,0-未處理,1-處理中,2-處理結束
* "progress" : 0.0, // 處理進度百分比
* "logList" : [], // 處理日志,字串串列,格式化顯示:Time level taskId taskName logInfo
* "result" : "", // 處理結果,字串型別
* "resultCode": 0, // 回傳碼,0表示操作成功
* "message" : "", // 回應訊息
* }
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/19 1.0.0 sheng.zheng 初版
*
*/
public Map<String, Object> getTaskInfo(HttpServletRequest request,
Map<String, Object> params){
// 從請求引數中獲取taskId
Integer taskId = (Integer)params.get("taskId");
if(taskId == null) {
throw new BaseException(ExceptionCodes.ARGUMENTS_IS_EMPTY,"taskId");
}
// 呼叫任務管理器的方法,獲取任務資訊物件
TaskInfo taskInfo = taskManService.getTaskInfo(request, taskId);
// 回傳值處理
// 從任務資訊物件,篩選一些屬性回傳
Map<String, Object> map = new HashMap<String, Object>();
// 任務ID
map.put("taskId", taskId);
// 任務狀態
map.put("procStatus", taskInfo.getProcStatus());
// 處理結果,如任務未結束,則為null
map.put("result", taskInfo.getResult());
// 處理日志
map.put("logList", taskInfo.getLogList());
// 處理進度
map.put("progress", taskInfo.getProgress());
// 可能的回傳碼和訊息
map.put("resultCode", taskInfo.getResultCode());
map.put("message", taskInfo.getMessage());
return map;
}
/**
*
* @methodName : testTask
* @description : 測驗任務,異步執行
* @param taskInfo : 任務資訊
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/19 1.0.0 sheng.zheng 初版
*
*/
public void testTask(TaskInfo taskInfo) {
// 開始處理任務
taskInfo.start();
// 獲取引數
Map<String, Object> params = (Map<String, Object>)taskInfo.getParams();
Integer repeat = (Integer)params.get("repeat");
if (repeat == null) {
repeat = 10;
}
Integer delay = (Integer)params.get("delay");
if (delay == null) {
delay = 1000;
}
String result = "";
// 重復n次
for(int i = 0; i < repeat; i++) {
taskInfo.addLogInfo(TaskConstants.LEVEL_INFO, "處理步驟" + (i+1));
// 顯示處理進度
taskInfo.setProgress((i+1)*1.0/repeat*100);
// 延遲delay毫秒
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
LogUtil.error(e);
}
}
result = "OK";
// 處理完畢
taskInfo.finish(result);
}
}
4.2、相關工具類
? 4.1中涉及到工具類Utility的getMethodByName方法,代碼如下:
package com.abc.example.common.utils;
import java.lang.reflect.Method;
/**
* @className : Utility
* @description : 工具類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
public class Utility {
/**
*
* @methodName : getMethodByName
* @description : 根據方法名稱獲取方法物件
* @param object : 方法所在的類物件
* @param methodName : 方法名
* @return : Method型別物件
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
public static Method getMethodByName(Object object,String methodName) {
Class<?> class1 = object.getClass();
Method retItem = null;
Method[] methods = class1.getMethods();
for (int i = 0; i < methods.length; i++) {
Method item = methods[i];
if (item.getName().equals(methodName)) {
retItem = item;
break;
}
}
return retItem;
}
}
4.3、異步測驗任務控制器類AsycTestController
? 異步測驗任務控制器類AsycTestController,提供HTTP訪問介面,代碼如下:
package com.abc.example.controller;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.abc.example.asyncproc.TestTaskService;
import com.abc.example.vo.common.BaseResponse;
/**
* @className : AsycTestController
* @description : 異步任務測驗控制器類
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2022/08/19 1.0.0 sheng.zheng 初版
*
*/
@RequestMapping("/asycTest")
@RestController
public class AsycTestController extends BaseController{
@Autowired
private TestTaskService tts;
@RequestMapping("/addTask")
public BaseResponse<Map<String,Object>> addTask(HttpServletRequest request,
@RequestBody Map<String, Object> params) {
Map<String,Object> map = tts.addAsyncTask(request,params);
return successResponse(map);
}
@RequestMapping("/getTaskInfo")
public BaseResponse<Map<String,Object>> getTaskInfo(HttpServletRequest request,
@RequestBody Map<String, Object> params) {
Map<String,Object> map = tts.getTaskInfo(request, params);
return successResponse(map);
}
}
4.4、基本回應訊息體物件類BaseResponse
? 基本回應訊息體物件類BaseResponse,提供標準形式的HTTP回應格式,代碼如下:
package com.abc.example.vo.common;
import lombok.Data;
/**
* @className : BaseResponse
* @description : 基本回應訊息體物件
* @summary :
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
@Data
public class BaseResponse<T> {
// 回應碼
private int code;
// 回應訊息
private String message;
// 回應物體資訊
private T data;
// 簡單起見,屏蔽下列資訊
// 分頁資訊
// private Page page;
// 附加通知資訊
// private Additional additional;
}
4.5、控制器基類BaseController
? 控制器基類BaseController,支持事務處理,并提供操作成功的方法,代碼如下:
package com.abc.example.controller;
import java.util.List;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import com.abc.example.exception.ExceptionCodes;
import com.abc.example.vo.common.BaseResponse;
import com.abc.example.vo.common.Page;
import com.github.pagehelper.PageInfo;
/**
* @className : BaseController
* @description : 控制器基類
* @summary : 支持事務處理,并提供操作成功的方法
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
@RestController
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public class BaseController {
/**
*
* @methodName : successResponse
* @description : 操作成功,回傳資訊不含資料體
* @param <T> : 模板型別
* @return : 操作成功的回傳碼和訊息,不含資料體
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
protected <T> BaseResponse<T> successResponse() {
BaseResponse<T> response = new BaseResponse<T>();
response.setCode(ExceptionCodes.SUCCESS.getCode());
response.setMessage(ExceptionCodes.SUCCESS.getMessage());
return response;
}
/**
*
* @methodName : successResponse
* @description : 操作成功,回傳資訊含資料體
* @param <T> : 模板型別
* @param data : 模板型別的資料
* @return : 操作成功的回傳碼和訊息,并包含資料體
* @history :
* ------------------------------------------------------------------------------
* date version modifier remarks
* ------------------------------------------------------------------------------
* 2021/01/01 1.0.0 sheng.zheng 初版
*
*/
protected <T> BaseResponse<T> successResponse(T data) {
BaseResponse<T> response = new BaseResponse<T>();
response.setCode(ExceptionCodes.SUCCESS.getCode());
response.setMessage(ExceptionCodes.SUCCESS.getMessage());
response.setData(data);
return response;
}
}
5、測驗
? 使用postman來測驗,
5.1、添加測驗任務
url:/asycTest/addTask
method: POST
request body:
{
"repeat" : 10, // 步驟數目
"delay" : 2000 // 等待時長,毫秒
}
response:
{
"code": 0,
"message": "操作成功",
"data": {
"taskId": 1
}
}
5.2、獲取任務資訊,任務處理程序中
url:/asycTest/getTaskInfo
method: POST
request body:
{
"taskId": 1
}
response:
{
"code": 0,
"message": "操作成功",
"data": {
"result": null,
"procStatus": 1,
"logList": [
"2022-08-19 16:26:29.919 INFO 1 測驗任務 --- 開始處理任務...",
"2022-08-19 16:26:29.921 INFO 1 測驗任務 --- 處理步驟1",
"2022-08-19 16:26:31.923 INFO 1 測驗任務 --- 處理步驟2",
"2022-08-19 16:26:33.928 INFO 1 測驗任務 --- 處理步驟3",
"2022-08-19 16:26:35.933 INFO 1 測驗任務 --- 處理步驟4",
"2022-08-19 16:26:37.937 INFO 1 測驗任務 --- 處理步驟5",
"2022-08-19 16:26:39.942 INFO 1 測驗任務 --- 處理步驟6",
"2022-08-19 16:26:41.946 INFO 1 測驗任務 --- 處理步驟7",
"2022-08-19 16:26:43.951 INFO 1 測驗任務 --- 處理步驟8",
],
"resultCode": 0,
"progress": 80.0,
"message": "",
"taskId": 1
}
}
5.3、獲取任務資訊,處理結束
url:/asycTest/getTaskInfo
method: POST
request body:
{
"taskId": 1
}
response:
{
"code": 0,
"message": "操作成功",
"data": {
"result": "OK",
"procStatus": 2,
"logList": [
"2022-08-19 16:26:29.919 INFO 1 測驗任務 --- 開始處理任務...",
"2022-08-19 16:26:29.921 INFO 1 測驗任務 --- 處理步驟1",
"2022-08-19 16:26:31.923 INFO 1 測驗任務 --- 處理步驟2",
"2022-08-19 16:26:33.928 INFO 1 測驗任務 --- 處理步驟3",
"2022-08-19 16:26:35.933 INFO 1 測驗任務 --- 處理步驟4",
"2022-08-19 16:26:37.937 INFO 1 測驗任務 --- 處理步驟5",
"2022-08-19 16:26:39.942 INFO 1 測驗任務 --- 處理步驟6",
"2022-08-19 16:26:41.946 INFO 1 測驗任務 --- 處理步驟7",
"2022-08-19 16:26:43.951 INFO 1 測驗任務 --- 處理步驟8",
"2022-08-19 16:26:45.956 INFO 1 測驗任務 --- 處理步驟9",
"2022-08-19 16:26:47.960 INFO 1 測驗任務 --- 處理步驟10",
"2022-08-19 16:26:49.965 INFO 1 測驗任務 --- 任務處理結束,耗時(s):20.044"
],
"resultCode": 0,
"progress": 100.0,
"message": "",
"taskId": 1
}
}
5.4、獲取任務資訊,處理結束后任務資訊快取時間過期
url:/asycTest/getTaskInfo
method: POST
request body:
{
"taskId": 1
}
response:
{
"code": 16,
"message": "任務ID不存在,可能已過期銷毀"
}
6、異步處理的注意事項
? 撰寫異步處理方法(如測驗例子中的testTask方法)時,有幾點注意事項:
? 1)request引數失效,如果要傳遞request引數,將之封裝到params引數中,會有很多問題,如Session為null等,可以參考此文:https://www.shuzhiduo.com/A/gVdnaPp85W/,
? 2)事務處理:執行緒方法的@Transactional會失效,如需要使用事務處理,可參考此文:https://blog.csdn.net/u013844437/article/details/112983780,
作者:阿拉伯1999
出處:http://www.cnblogs.com/alabo1999/
本文著作權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段宣告,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利.
養成良好習慣,好文章隨手頂一下,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/502369.html
標籤:Java
上一篇:java基礎
