繁体   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