1. 程式人生 > 實用技巧 >支付回撥異常如何捕獲? 借鑑Mybatis中的ErrorContext

支付回撥異常如何捕獲? 借鑑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(處理業務的結果)

測試結果

參考

https://www.jianshu.com/p/901e37d05853