簡體   English   中英

在Springboot應用程序中使用monad捕獲異常

[英]Using monads in Springboot application to catch exceptions

因此,我們的項目后端是Java 8 Springboot應用程序,springboot允許您真正輕松地完成一些工作。 例如,要求驗證:

class ProjectRequestDto {
    @NotNull(message = "{NotNull.DotProjectRequest.id}")
    @NotEmpty(message = "{NotEmpty.DotProjectRequest.id}")
    private String id;
}

當不滿足此約束時,spring(springboot?)實際上會引發驗證異常,因此,我們在應用程序中的某個位置捕獲了該異常,並為我們的應用程序構造了404(錯誤請求)響應。

現在,鑒於這一事實,我們在整個應用程序中都遵循相同的理念,也就是說,在應用程序的更深層中,我們可能會遇到以下情況:

class ProjectService throws NotFoundException {
  DbProject getProject(String id) {
      DbProject p = ... // some hibernate code
      if(p == null) {
          Throw new NotFoundException();
      }

      return p;
  }
}

同樣,我們在更高級別上捕獲了此異常,並為客戶端構造了另一個404。

現在,這引起了一些問題:

  1. 最重要的一點:我們的錯誤跟蹤不再有用,當異常很重要時,我們無法(輕松地)進行區分,因為它們一直發生,因此,如果服務突然開始拋出錯誤,直到為時已晚,我們才會注意到。

  2. 大量無用的日志記錄,例如在登錄請求時,用戶可能輸入了錯誤的密碼,因此我們將其記錄下來,這是一個次要點:我們的分析無法幫助我們確定我們實際上在做錯什么,我們看到了很多4xx,但這是我們的期望。

  3. 異常的代價很高,收集堆棧跟蹤信息是一項資源密集型任務,這是目前的一個小問題,因為隨着服務規模的擴大,這將成為一個更大的問題。

我認為解決方案非常明確,我們需要進行架構更改,以不將異常作為正常數據流的一部分,但這是一個很大的更改,而且我們的時間很短,因此我們計划隨時間遷移,但是問題是短期內仍然存在。

現在,我的實際問題是:當我問一位建築師時,他建議使用monads(作為c的暫時解決方案),因此我們不修改我們的體系結構,而是處理最污染的端點(例如錯誤的登錄)。在短期內,但是我一直在為monad范式而苦苦掙扎,甚至在Java中也是如此,我真的不知道如何將其應用於我們的項目,您能幫我這個忙嗎? 一些代碼片段會非常好。

TL:DR:如果您使用的是一個普通的Spring Boot應用程序,該應用程序在其數據流中拋出錯誤,那么如何應用monad模式以避免登錄不必要的數據量,並將此Error臨時修復為數據流體系結構的一部分。

異常處理的標准monadic方法實質上是將結果包裝為成功結果或錯誤類型。 它類似於Optional類型,盡管在這里您有一個錯誤值而不是一個空值。

在Java中,最簡單的實現如下所示:

public interface Try<T> {

    <U> Try<U> flatMap(Function<T, Try<U>> f);

    class Success<T> implements Try<T> {
        public final T value;

        public Success(T value) {
            this.value = value;
        }

        @Override
        public <U> Try<U> flatMap(Function<T, Try<U>> f) {
            return f.apply(value);
        }
    }

    class Fail<T> implements Try<T> {
        // Alternatively use Exception or Throwable instead of String.
        public final String error;

        public Fail(String error) {
            this.error = error;
        }

        @Override
        public <U> Try<U> flatMap(Function<T, Try<U>> f) {
            return (Try<U>)this;
        }
    }
}

(對於equals,hashCode,toString具有明顯的實現)

先前您有可能返回T類型結果或引發異常的操作的地方,它們將返回Try<T> (可能是Success<T>Fail<T> )的結果,而不會拋出,例如:

class Test {
    public static void main(String[] args) {
        Try<String> r = ratio(2.0, 3.0).flatMap(Test::asString);
    }

    static Try<Double> ratio(double a, double b) {
        if (b == 0) {
            return new Try.Fail<Double>("Divide by zero");
        } else {
            return new Try.Success<Double>(a / b);
        }
    }

    static Try<String> asString(double d) {
        if (Double.isNaN(d)) {
            return new Try.Fail<String>("NaN");
        } else {
            return new Try.Success<String>(Double.toString(d));
        }
    }
}

即,您不會拋出異常,而是返回Fail<T>值來包裝錯誤。 然后,您可以使用flatMap方法編寫可能會失敗的操作。 應該清楚的是,一旦發生錯誤,它將短路任何后續操作-在上面的示例中,如果ratio返回Fail則不會調用asString ,並且錯誤會直接傳播到最終結果r

以您的示例為例,在這種方法下,它看起來像這樣:

class ProjectService throws NotFoundException {
  Try<DbProject> getProject(String id) {
      DbProject p = ... // some hibernate code
      if(p == null) {
          return new Try.Fail<DbProject>("Failed to create DbProject");
      }

      return new Try.Succeed<DbProject>(p);
  }
}

與原始異常相比,它的優點是更具可組合性,例如,允許您在值的集合上映射(例如Stream.map)可失敗的函數,並最終導致失敗和成功的集合。 如果使用異常,則第一個異常將使整個操作失敗,並且將丟失所有結果。

缺點之一是您必須在調用堆棧中一直使用Try返回類型(有點像檢查異常)。 另一個原因是,由於Java沒有內置的monad支持(例如la Haskell和Scala),因此flatMap的功能可能會有些冗長。 例如:

try {
    A a = f(x);
    B b = g(a);
    C c = h(b);
} catch (...

f,g,h可能拋出的位置變為:

Try<C> c = f(x).flatMap(a -> g(a))
               .flatMap(b -> h(b));

您可以通過使錯誤類型成為通用參數E (而不是String)來概括上述實現,因此它變為Try<T, E> 這是否有用取決於您的要求-我從不需要它。

在這里有一個更完整的版本,或者Javaslang和FunctionalJava庫提供了它們自己的變體。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM