支付回撥異常如何捕獲? 借鑑Mybatis中的ErrorContext
前言
第三方支付接過不?支付回撥的程式碼寫過不?
1.接受支付平臺的回撥資訊,驗籤判斷是否是合法回撥
2.呼叫支付平臺查詢介面查詢訂單
3.獲取支付狀態,成功還是失敗
4.支付狀態為成功,處理業務
5.返回伺服器報文
哪些步驟可能會出錯?
第一步可能出錯,驗籤失敗
第二步可能查詢不到訂單,訂單是偽造的
第三步支付狀態為失敗
第四步業務重複處理,報異常
因為本公司的專案都是輸出在一個日誌檔案上,排查問題,就很不方便。
a業務的日誌1
b業務的日誌1
c業務的日誌1
a業務的日誌2
單個業務的日誌不在一起顯示
有一天,有個線上專案是一個商城類的,客戶充值到錢包裡面,支付了5筆,但實際只成功了3筆。一開始我以為是第三方支付的鍋,直接@那邊的技術人員,說你們是不是沒有回撥我們介面。他們檢視日誌說已經回調了,我看了下我這邊的日誌,確實是回調了。
那問題在哪呢?由於一開始我這邊只打印了回撥的資訊,並沒有針對每個步驟進行列印。改進之後
···程式碼
log.info(回撥資訊)
···程式碼
log.info(驗簽結果)
···程式碼
log.info(訂單查詢結果)
···程式碼
log.info(支付狀態)
···程式碼
log.info(處理業務的結果)
最後發現列印日誌,是東一塊,西一塊,很亂,排查問題還是很不方便,而且如果回撥都正常,還是會列印日誌,就有點浪費。
大家應該都用過Mybatis,有沒有覺得Mybatis報錯了,問題很快就可以排查到
resource:儲存異常存在於哪個資原始檔中。
如:### The error may exist in mapper/TagMapper.xml
activity:儲存異常是做什麼操作時發生的。
如:### The error occurred while setting parameters
object:儲存哪個物件操作時發生異常。
如:### The error may involve defaultParameterMap
message:儲存異常的概覽資訊。
如:### Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'name' at row 1
sql:儲存發生日常的 SQL 語句。
如:### SQL: insert into tag (`name`) values (?)
cause:儲存詳細的 Java 異常日誌。
如:### Cause: com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Data too long for column 'name' at row 1
at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:199)
at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:184)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:62)
at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:144)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:85)
at com.sun.proxy.$Proxy4.save(Unknown Source)
at com.test.error.ErrorContextTest.main(ErrorContextTest.java:31)
於是我照葫蘆畫瓢,自定義了一個 ErrorPayContext
/**
* 請求引數
*/
private String request;
/**
* 訂單號
*/
private String orderCode;
/**
* 驗簽結果
*/
private String check;
/**
* 訂單查詢返回結果
*/
private String queryOrderResult;
/**
* 支付狀態
*/
private String payStatus;
/**
* 業務處理返回結果
*/
private String businessResult;
/**
* message
*/
private String message;
/**
* 異常詳細資訊
*/
private Throwable cause;
單例模式
// 私有化構造
private ErrorPayContext() {}
// 公共的建立物件方法
public static ErrorPayContext instance() {
ErrorPayContext errorPayContext = LOCAL.get();
if (errorPayContext == null) {
errorPayContext = new ErrorPayContext();
LOCAL.set(errorPayContext);
}
return errorPayContext;
}
這裡用到了ThreadLocal
本地執行緒儲存,它的作用是為變數在每個執行緒中建立一個副本,每個執行緒內部都可以使用該副本,執行緒之間互不影響。
private static final ThreadLocal<ErrorPayContext> LOCAL = new ThreadLocal<>();
使用 ThreadLocal 來管理 ErrorContext:
保證了在多執行緒環境中,每個執行緒內部可以共用一份 ErrorContext,但多個執行緒持有的 ErrorContext 互不影響,保證了異常日誌的正確輸出。
set
方法
public ErrorPayContext request(String request) {
this.request = request;
return this;
}
public ErrorPayContext orderCode(String orderCode) {
this.orderCode = orderCode;
return this;
}
public ErrorPayContext check(String check) {
this.check = check;
return this;
}
public ErrorPayContext queryOrderResult(String queryOrderResult) {
this.queryOrderResult = queryOrderResult;
return this;
}
public ErrorPayContext payStatus(String payStatus) {
this.payStatus = payStatus;
return this;
}
public ErrorPayContext businessResult(String businessResult) {
this.payStatus = payStatus;
return this;
}
public ErrorPayContext message(String message) {
this.message = message;
return this;
}
public ErrorPayContext cause(Throwable cause) {
this.cause = cause;
return this;
}
釋放資源
public ErrorPayContext reset() {
request = null;
check = null;
orderCode = null;
queryOrderResult = null;
payStatus = null;
businessResult = null;
message = null;
cause = null;
LOCAL.remove();
return this;
}
toString() 方法
@Override
public String toString() {
StringBuilder description = new StringBuilder();
// message
if (this.message != null) {
description.append(LINE_SEPARATOR);
description.append("### ");
description.append(this.message);
}
// request
if (request != null) {
description.append(LINE_SEPARATOR);
description.append("### 回撥引數:");
description.append(this.request);
}
// orderCode
if (orderCode != null) {
description.append(LINE_SEPARATOR);
description.append("### 訂單號:");
description.append(this.orderCode);
}
// check
if(check != null ){
description.append(LINE_SEPARATOR);
description.append("### 驗簽結果:");
description.append(this.check);
}
// queryOrderResult
if(queryOrderResult != null ){
description.append(LINE_SEPARATOR);
description.append("### 訂單查詢結果:");
description.append(this.queryOrderResult);
}
// payStatus
if(payStatus != null ){
description.append(LINE_SEPARATOR);
description.append("### 訂單支付狀態:");
description.append(this.payStatus);
}
// businessResult
if(businessResult != null ){
description.append(LINE_SEPARATOR);
description.append("### 業務處理結果:");
description.append(this.businessResult);
}
// cause
if (cause != null) {
description.append(LINE_SEPARATOR);
description.append("### Cause:");
description.append(this.cause);
}
return description.toString();
}
如何使用
NotifyCallBackController.java
@RequestMapping(value = "/notifyCallBack/{type}")
public void notifyCallBack(@PathVariable int type, HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
handler(type,request,response);
} catch (Exception e) {
log.error(ErrorPayContext.instance().message(e.getMessage()).cause(e).toString());
e.printStackTrace();
} finally {
ErrorPayContext.instance().reset();
responseResult(response, "200");
}
}
XXXService.java
···程式碼
ErrorPayContext.instance().request(回撥資訊)
···程式碼
ErrorPayContext.instance().check(驗簽結果)
···程式碼
ErrorPayContext.instance().queryOrderResult(訂單查詢結果)
···程式碼
ErrorPayContext.instance().payStatus(支付狀態)
···程式碼
ErrorPayContext.instance().businessResult(處理業務的結果)
測試結果