基於Spring-AOP的自定義分片工具

2022-11-22 12:01:50

作者:陳昌浩

1 背景

隨著資料量的增長,發現系統在與其他系統互動時,批次介面會出現超時現象,發現原批次介面在實現時,沒有做分片處理,當資料過大時或超過其他系統閾值時,就會出現錯誤。由於與其他系統互動比較多,一個一個介面做分片優化,改動量較大,所以考慮通過AOP解決此問題。

2 Spring-AOP

AOP (Aspect Orient Programming),直譯過來就是 面向切面程式設計。AOP 是一種程式設計思想,是物件導向程式設計(OOP)的一種補充。物件導向程式設計將程式抽象成各個層次的物件,而面向切面程式設計是將程式抽象成各個切面。

Spring 中的 AOP 是通過動態代理實現的。 Spring AOP 不能攔截對物件欄位的修改,也不支援構造器連線點,我們無法在 Bean 建立時應用通知。

3 功能實現

自定義分片處理分三個部分:自定義註解(MethodPartAndRetryer)、重試器(RetryUtil)、切面實現(RetryAspectAop)。

3.1 MethodPartAndRetryer

原始碼

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MethodPartAndRetryer {
    /**
     * 失敗重試次數 
     * @return
     */
    int times() default 3;
    /**
     * 失敗間隔執行時間 300毫秒
     * @return
     */
    long waitTime() default 300L;
    /**
     * 分片大小     
     * @return
     */
    int parts() default 200;
}

 

@interface說明這個類是個註解。
@Target是這個註解的作用域

public enum ElementType {
    /** 類、介面(包括註釋型別)或列舉宣告   */
    TYPE,
    /** 欄位宣告(包括列舉常數) */
    FIELD,
    /** 方法宣告 */
    METHOD,
    /** 正式的引數宣告 */
    PARAMETER,
    /** 建構函式宣告 */
    CONSTRUCTOR,
    /** 區域性變數宣告 */
    LOCAL_VARIABLE,
    /** 註釋型別宣告*/
    ANNOTATION_TYPE,
    /** 程式包宣告 */
    PACKAGE,
    /**型別引數宣告*/
    TYPE_PARAMETER,
    /**型別的使用*/
    TYPE_USE
}
@Retention註解的生命週期

public enum RetentionPolicy {
    /** 編譯器處理完後不儲存在class中*/
    SOURCE,
    /**註釋將被編譯器記錄在類檔案中,但不需要在執行時被VM保留。 這是預設值*/
    CLASS,
    /**編譯器儲存在class中,可以由虛擬機器器讀取*/
    RUNTIME
}

 

  • times():介面呼叫失敗時,重試的次數。
  • waitTime():介面呼叫失敗是,間隔多長時間再次呼叫。
  • int parts():進行分片時,每個分片的大小。

3.2 RetryUtil

原始碼

public class RetryUtil<V> {

    public Retryer<V> getDefaultRetryer(int times,long waitTime) {
        Retryer<V> retryer = RetryerBuilder.<V>newBuilder()
                .retryIfException()
                .retryIfRuntimeException()
                .retryIfExceptionOfType(Exception.class)
                .withWaitStrategy(WaitStrategies.fixedWait(waitTime, TimeUnit.MILLISECONDS))
                .withStopStrategy(StopStrategies.stopAfterAttempt(times))
                .build();
        return retryer;
    }
}

 

說明

  • RetryerBuilder:是用於設定和建立Retryer的構建器。
  • retryIfException:丟擲runtime異常、checked異常時都會重試,但是丟擲error不會重試。
  • retryIfRuntimeException:只會在拋runtime異常的時候才重試,checked異常和error都不重試。
  • retryIfExceptionOfType:允許我們只在發生特定異常的時候才重試。
  • withWaitStrategy:等待策略,每次請求間隔。
  • withStopStrategy:停止策略,重試多少次後停止。

3.3 RetryAspectAop

原始碼:

public class RetryAspectAop {
      public Object around(final ProceedingJoinPoint point) throws Throwable {
        Object result = null;
        final Object[] args = point.getArgs();
        boolean isHandler1 = isHandler(args);
        if (isHandler1) {
            String className = point.getSignature().getDeclaringTypeName();
            String methodName = point.getSignature().getName();
            Object firstArg = args[0];
            List<Object> paramList = (List<Object>) firstArg;
            //獲取方法資訊
            Method method = getCurrentMethod(point);
            //獲取註解資訊
            MethodPartAndRetryer retryable = AnnotationUtils.getAnnotation(method, MethodPartAndRetryer.class);
            //重試機制
            Retryer<Object> retryer = new RetryUtil<Object>().getDefaultRetryer(retryable.times(),retryable.waitTime());
            //分片
            List<List<Object>> requestList = Lists.partition(paramList, retryable.parts());
            for (List<Object> partList : requestList) {
                args[0] = partList;
                Object tempResult = retryer.call(new Callable<Object>() {
                    @Override
                    public Object call() throws Exception {
                        try {
                            return point.proceed(args);
                        } catch (Throwable throwable) {
                            log.error(String.format("分片重試報錯,類%s-方法%s",className,methodName),throwable);
                            throw new RuntimeException("分片重試出錯");
                        }
                    }
                });
                if (null != tempResult) {
                    if (tempResult instanceof Boolean) {
                        if (!((Boolean) tempResult)) {
                            log.error(String.format("分片執行報錯返回型別不能轉化bolean,類%s-方法%s",className,methodName));
                            throw new RuntimeException("分片執行報錯!");
                        }
                        result = tempResult;
                    } else if (tempResult instanceof List) {
                        if(result ==null){
                            result =Lists.newArrayList();
                        }
                        ((List) result).addAll((List) tempResult);
                    }else {
                        log.error(String.format("分片執行返回的型別不支援,類%s-方法%s",className,methodName));
                        throw new RuntimeException("不支援該返回型別");
                    }
                } else {
                    log.error(String.format("分片執行返回的結果為空,類%s-方法%s",className,methodName));
                    throw new RuntimeException("呼叫結果為空");
                }
            }
        } else {
            result = point.proceed(args);
        }
        return result;
    }
    private boolean isHandler(Object[] args) {
        boolean isHandler = false;
        if (null != args && args.length > 0) {
            Object firstArg = args[0];
            //如果第一個引數是list 並且數量大於1
            if (firstArg!=null&&firstArg instanceof List &&((List) firstArg).size()>1) {
                isHandler = true;
            }
        }
        return isHandler;
    }
    private Method getCurrentMethod(ProceedingJoinPoint point) {
        try {
            Signature sig = point.getSignature();
            MethodSignature msig = (MethodSignature) sig;
            Object target = point.getTarget();
            return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

 

說明:

getCurrentMethod:獲取方法資訊即要做分片的批次呼叫的介面。
isHandler1:判斷是否要做分片處理,只有第一引數是list並且list 的值大於1時才做分片處理。
around:具體分片邏輯。

  1. 獲取要分片方法的引數。
  2. 判斷是否要做分片處理。
  3. 獲取方法。
  4. 獲取重試次數、重試間隔時間和分片大小。
  5. 生成重試器。
  6. 根據設定的分片大小,做分片處理。
  7. 呼叫批次介面並處理結果。

4 功能使用

4.1 組態檔

 

4.2 程式碼範例

@MethodPartAndRetryer(parts=100)
public Boolean writeBackOfGoodsSN(List<SerialDTO> listSerial,ObCheckWorker workerData)

 

只要在需要做分片的批次介面方法上,加上MethodPartAndRetryer註解就可以,重試次數、重試間隔時間和分片大小可以在註解時設定,也可以使用預設值。

5 小結

通過自定義分片工具,可以快速地對老程式碼進行分片處理,而且增加了重試機制,提高了程式的可用性,提高了對老程式碼的重構效率。