在反应堆中流动多个物体的有效/正确方法是什么

Muh*_*yas 4 java reactive-programming project-reactor spring-webflux

我是反应式编程的新手,为了让我亲自动手,我正在尝试构建一个接近真实的示例。

当您看到 reactor 教程时,它们会向您展示非常简单的示例,例如。

return userRepository.findById(1);
Run Code Online (Sandbox Code Playgroud)

或诸如处理通量之类的东西,打破“棕色小狐狸”字符串并找到独特的字母等。但大多数这些教程都坚持单个对象,不幸的是我找不到任何指导线或教程来显示并排示例相同的代码首先是命令式,然后是反应式,这就是为什么我看到许多反应式编程的新手面临很多学习问题。

但我的观点是在现实生活中的应用程序中,我们处理多个对象,如下面我在 reactor 中编写的示例代码。为我仍在学习的错误代码道歉。

public Mono<ServerResponse> response(ServerRequest serverRequest) {

        return
                Mono.just(new UserRequest())
                        .map(userRequest -> {
                            Optional<String> name = serverRequest.queryParam("name");
                            if (name.isPresent() && !name.get().isEmpty()) {
                                userRequest.setName(name.get());
                                return userRequest;
                            }
                            throw new RuntimeException("Invalid name");
                        })
                        .map(userRequest -> {
                            Optional<String> email = serverRequest.queryParam("email");
                            if (email.isPresent() && !email.get().isEmpty()) {
                                userRequest.setEmail(email.get());
                                return userRequest;
                            }
                            throw new RuntimeException("Invalid email");
                        })
                        .map(userRequest -> {
                            userRequest.setUuid(UUID.randomUUID().toString());
                            return userRequest;
                        })
                        .flatMap(userRequest ->
                                userRepository
                                        .findByEmail(userRequest.getEmail())
                                        .switchIfEmpty(Mono.error(new RuntimeException("User not found")))
                                        .map(user -> Tuples.of(userRequest, user))
                        )
                        .map(tuple -> {
                            String cookiePrefix = tuple.getT2().getCode() + tuple.getT1().getUuid();
                            return Tuples.of(tuple.getT1(), tuple.getT2(), cookiePrefix);
                        })
                        //Some more chaining here.
                        .flatMap(tuple ->
                                ServerResponse
                                        .ok()
                                        .cookie(ResponseCookie.from(tuple.getT3(), tuple.getT2().getRating()).build())
                                        .bodyValue("Welcome")
                        );

    }
Run Code Online (Sandbox Code Playgroud)

首先考虑上面的代码,我从 UserRequest 对象开始映射这个对象中的查询字符串。然后我需要一些来自数据库的数据,等等反应链继续做更多的工作。现在考虑

  • 来自第一个链接方法的 UserRequest 对象和
  • 我从数据库中获取的用户文档然后我做了更多的操作,但在链接结束时我需要这两个对象来处理最终响应。实现我在谷歌上找到的唯一方法是元组。但是在那之后代码看起来更脏了,因为在每个下一个操作符中我都必须做
tuple.getT()
Run Code Online (Sandbox Code Playgroud)
tuple.getT2()
Run Code Online (Sandbox Code Playgroud)

所以最后我想问的是正确的方法还是我在这里遗漏了一些东西。因为我在反应式中学到了一件事,那就是数据在逻辑中间流动,就像命令式一样,我们得到了哦,我需要另一个变量/对象,所以我在顶部定义并使用它,但是当开发人员意识到时,在第 5 个或第 6 个运算符之后在反应式中哦,我在这里也需要那个对象,它是我在第二个操作符中创建的,然后我必须返回并在链接中传递它以进入我的第 5 个或第 6 个操作符,这是一个正确的方法。

Mic*_*rry 5

通常有两种策略可以用来避免“元组地狱”,有时是孤立的,有时是串联的:

  • 使用你自己的“自定义”元组类,它更能描述类型(我几乎总是在生产代码中推荐这个而不是使用内置Tuple类);
  • 连接您的一些map()/flatMap()调用,以便不需要声明元组。

此外,还有更多规则需要牢记,可以帮助这里的一般情况:

  • 除非你别无选择,否则永远不要改变反应链中的对象 - 使用具有@With模式的不可变对象;
  • 不要使用多个map()链接在一起的调用来返回相同的类型 - 最好在单个 map 调用中完成所有操作;
  • 将长反应链的可重用元素分出到单独的方法中,并使用map(),flatMap()或将它们嵌入到主反应链中transform()

如果我们将上面的例子付诸实践,我们可以将前三个 map 调用移植到一个“填充”用户对象的方法中,使用 @With 样式而不是 setter(尽管如果你真的需要,你可以在这里使用 setter) :

private UserRequest populateUser(UserRequest userRequest, ServerRequest serverRequest) {
    return userRequest
            .withName(serverRequest.queryParam("name")
                    .filter(s -> !s.isEmpty())
                    .orElseThrow(() -> new RuntimeException("Invalid name")))
            .withEmail(serverRequest.queryParam("email")
                    .filter(s -> !s.isEmpty())
                    .orElseThrow(() -> new RuntimeException("Invalid email")))
            .withUuid(UUID.randomUUID().toString());
}
Run Code Online (Sandbox Code Playgroud)

我们还可以将链中从数据库中查找用户的部分移出。这部分可能需要某种形式的新的类型的,但不是一个Tuple,创建一个单独的类-我们称之为VerifiedUser-这将采取userRequestuser对象。然后,此类型还可以负责生成响应 cookie 对象,并通过简单的 getter 提供它。(我会将编写VerifiedUser任务留给作者作为练习 - 这应该非常简单。)

然后我们会有一个这样的方法:

private Mono<VerifiedUser> lookupUser(UserRequest userRequest) {
    return userRepository
            .findByEmail(userRequest.getEmail())
            .map(user -> new VerifiedUser(userRequest, user)) //VerifiedUser can contain the logic to produce the ResponseCookie
            .switchIfEmpty(Mono.error(new RuntimeException("User not found")));
}
Run Code Online (Sandbox Code Playgroud)

所以现在我们有两个独立的小方法,每个方法都承担一个责任。我们还有另一种简单类型 ,VerifiedUser它是一个命名容器类型,比Tuple. 这种类型还为我们提供了一个 cookie 值。

这个过程意味着我们的主反应链现在可以变得非常简单:

return Mono.just(new UserRequest())
        .map(userRequest -> populateUser(userRequest, serverRequest))
        .flatMap(this::lookupUser)
        .flatMap(verifiedUser ->
                ServerResponse.ok()
                        .cookie(verifiedUser.getCookie())
                        .bodyValue("Welcome")
        );
Run Code Online (Sandbox Code Playgroud)

最终的结果是一个更安全的链(因为我们没有改变链中的值,一切都保持不变),更清晰易读,并且在我们需要时更容易扩展。如果我们需要更进一步,那么我们也可以 - 例如,如果这里创建的方法需要在其他地方使用,它们可以很容易地作为符合功能接口的 spring bean 进行移植,然后随意注入(并且很容易进行单元测试。 )

(顺便说一句,您当然是正确的,在撰写本文时,有很多琐碎的教程,但很少有“深入”或“现实世界”的材料。合理的新框架通常就是这种情况,但这确实使它们难以掌握,并导致大量无法维护的代码在野外!)

  • @MuhammadIlyas 它很安全,并且可以正常工作,但如果可能的话,我不喜欢用 `Mono.just()` 包装。单声道或通量意味着您正在处理反应性操作,而在这种情况下,您不是 - 这使得您的代码不太清晰恕我直言。通过将其保留为可选或流 API,您可以非常清楚地表明这是一个同步、非阻塞操作。 (2认同)