У меня есть два метода.
Основной метод:
@PostMapping("/login")
public Mono<ResponseEntity<ApiResponseLogin>> loginUser(@RequestBody final LoginUser loginUser) {
return socialService.verifyAccount(loginUser)
.flatMap(socialAccountIsValid -> {
if (socialAccountIsValid) {
return this.userService.getUserByEmail(loginUser.getEmail())
.switchIfEmpty(insertUser(loginUser))
.flatMap(foundUser -> updateUser(loginUser, foundUser))
.map(savedUser -> {
String jwts = jwt.createJwts(savedUser.get_id(), savedUser.getFirstName(), "user");
return new ResponseEntity<>(HttpStatus.OK);
});
} else {
return Mono.just(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
}
});
}
И этот вызываемый метод (сервис вызывает внешний API):
public Mono<User> getUserByEmail(String email) {
UriComponentsBuilder builder = UriComponentsBuilder
.fromHttpUrl(USER_API_BASE_URI)
.queryParam("email", email);
return this.webClient.get()
.uri(builder.toUriString())
.exchange()
.flatMap(resp -> {
if (Integer.valueOf(404).equals(resp.statusCode().value())) {
return Mono.empty();
} else {
return resp.bodyToMono(User.class);
}
});
}
В приведенном выше примере switchIfEmpty() всегда вызывается из основного метода, даже если возвращается результат с Mono.empty().
Я не могу найти решение этой простой проблемы.
Также не работает следующее:
Mono.just(null)
Поскольку этот метод вызовет ошибку NullPointerException.
Что я также не могу использовать, так это метод flatMap для проверки того, что foundUser имеет значение null.
.
К сожалению, flatMap вообще не вызывается, если я возвращаю Mono.empty(), поэтому здесь я также не могу добавить условие.
@SimY4
@PostMapping("/login")
public Mono<ResponseEntity<ApiResponseLogin>> loginUser(@RequestBody final LoginUser loginUser) {
userExists = false;
return socialService.verifyAccount(loginUser)
.flatMap(socialAccountIsValid -> {
if (socialAccountIsValid) {
return this.userService.getUserByEmail(loginUser.getEmail())
.flatMap(foundUser -> {
return updateUser(loginUser, foundUser);
})
.switchIfEmpty(Mono.defer(() -> insertUser(loginUser)))
.map(savedUser -> {
String jwts = jwt.createJwts(savedUser.get_id(), savedUser.getFirstName(), "user");
return new ResponseEntity<>(HttpStatus.OK);
});
} else {
return Mono.just(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
}
});
}
Не могли бы вы немного подробнее рассказать о своей проблеме? ''switchIfEmpty() всегда вызывается из основного метода, даже если возвращается результат с Mono.empty()''. Это ожидаемое поведение.
@Barath Чего я хочу добиться, так это того, что если внешняя служба возвращает 404, я могу вернуть Mono со значением null из уровня службы, который может быть обработан основным методом. Думаю, я тоже мог бы выдать ошибку, но предпочитаю этого не делать. Ошибка 404 должна обрабатываться на сервисном уровне, и когда пользователь не найден, это логика приложения, которую, как мне кажется, следует обрабатывать с помощью if, а не путем обработки исключений. Я собираюсь просмотреть switfhIfEmpty в документации. Тем не менее, рабочее предложение?
@PrashantPandey Пожалуйста, смотрите комментарий выше.
@Trace, ваш код все еще работает, если 404, вы возвращаете Mono.empty() , который будет вызывать switchIfEmpty. В любом случае, если вы хотите обрабатывать ошибки, если это то, что вы ищете, вы можете использовать onErrorResume() и обрабатывать их соответствующим образом, или вы также можете использовать onErrorReturn(). руководство
@guide Проблема в том, что switchIfEmpty также вызывается, когда Mono не пуст, чего я не хочу. Условие будет -> вставить, если пусто, иначе обновить.
точно не может быть. Пожалуйста, включите журналы и поделитесь выводом журналов.
@Trace Вот как бы я справился с этим: если ответ содержит 404, сгенерируйте обернутое исключение. Затем в основном методе вернитесь к альтернативному Mono, используя onErrorResume(insertUser(loginUser)). Если вы не хотите генерировать исключение, ваша логика должна работать нормально: вернуть пустой наблюдаемый объект и обработать его с помощью switchIfEmpty.
@Trace Проблема в том, что switchIfEmpty также вызывается, когда Mono не пуст: вы уверены? Можете немного отладить, так как этого быть не должно.
@PrashantPandey Метод insertUser в switchIfEmpty вызывается до вызова метода flatMap, обрабатывающего 404. Не знаю, почему это так.
@Trace Наверное, глупая идея, но не могли бы вы добавить .publishOn(Schedulers.parallel()) после плоской карты в методе getUserByEmail? Спрашиваю об этом, так как я делаю что-то очень похожее прямо сейчас, и это работает для меня.
@Trace, не могли бы вы предоставить метод insertUser (loginUser)




Это потому, что switchIfEmpty принимает Mono «по значению». Это означает, что даже до того, как вы подпишетесь на свой моно, оценка этого альтернативного моно уже активирована.
Представьте себе такой метод:
Mono<String> asyncAlternative() {
return Mono.fromFuture(CompletableFuture.supplyAsync(() -> {
System.out.println("Hi there");
return "Alternative";
}));
}
Если вы определяете свой код следующим образом:
Mono<String> result = Mono.just("Some payload").switchIfEmpty(asyncAlternative());
Это всегда будет запускать альтернативу, независимо от того, что происходит во время построения потока. Чтобы решить эту проблему, вы можете отложить оценку второго моно с помощью Mono.defer.
Mono<String> result = Mono.just("Some payload")
.switchIfEmpty(Mono.defer(() -> asyncAlternative()));
Таким образом, он будет печатать «Привет» только тогда, когда запрашивается альтернатива.
УПД:
Немного уточняя мой ответ. Проблема, с которой вы столкнулись, связана не с Reactor, а с самим языком Java и с тем, как он разрешает параметры метода. Давайте рассмотрим код из первого предоставленного мной примера.
Mono<String> result = Mono.just("Some payload").switchIfEmpty(asyncAlternative());
Мы можем переписать это в:
Mono<String> firstMono = Mono.just("Some payload");
Mono<String> alternativeMono = asyncAlternative();
Mono<String> result = firstMono.switchIfEmpty(alternativeMono);
Эти два фрагмента кода семантически эквивалентны. Мы можем продолжить разворачивать их, чтобы увидеть, в чем проблема:
Mono<String> firstMono = Mono.just("Some payload");
CompletableFuture<String> alternativePromise = CompletableFuture.supplyAsync(() -> {
System.out.println("Hi there");
return "Alternative";
}); // future computation already tiggered
Mono<String> alternativeMono = Mono.fromFuture(alternativePromise);
Mono<String> result = firstMono.switchIfEmpty(alternativeMono);
Как видите, будущие вычисления уже были запущены в тот момент, когда мы начинаем составлять наши типы Mono. Чтобы предотвратить нежелательные вычисления, мы можем обернуть наше будущее в отложенную оценку:
Mono<String> result = Mono.just("Some payload")
.switchIfEmpty(Mono.defer(() -> asyncAlternative()));
Который развернется в
Mono<String> firstMono = Mono.just("Some payload");
Mono<String> alternativeMono = Mono.defer(() -> Mono.fromFuture(CompletableFuture.supplyAsync(() -> {
System.out.println("Hi there");
return "Alternative";
}))); // future computation defered
Mono<String> result = firstMono.switchIfEmpty(alternativeMono);
Во втором примере будущее застряло в ленивом поставщике и запланировано для выполнения только тогда, когда оно будет запрошено.
Я пробовал это, но метод, который вы определяете asyncAlternative(), всегда срабатывает, несмотря на Mono.defer().
@Trace, можешь показать мне, что ты пробовал? Потому что единственная цель отсрочки — не допустить, чтобы оценка произошла раньше времени.
Пожалуйста, смотрите обновленный пост. В режиме отладки я видел, что Mono.defer вызывается после, но это не отменяет того, что он всегда выполняется, даже когда this.userService.getUserByEmail(loginUser.getEmail()) не возвращает Mono.empty().
It'll always trigger alternative no matter what during stream construction. Тогда какой толк. Это метод с именем switchIfEmpty. Некоторые вещи действительно не имеют смысла.
Спасибо за объяснение. Но, как вы видите в моем примере кода, я использую ваш отложенный метод, но обратный вызов всегда выполняется, несмотря на использование defer. getUserByEmail возвращает Mono, как и методы insertUser и updateUser. Я действительно не понимаю, почему.
Ваш ответ был правильным. Я разобрался, оказалось, что причина срабатывания switchIfEmpty заключалась в том, что на самом деле updateUser вернул пустое тело с кодом состояния http 204! Я немного неохотно модифицировал апи, но теперь он работает корректно. Спасибо за это!
Нам действительно нужен Mono.defer(() -> Mono.fromFuture(CompletableFuture.supplyAsync(() -> { System.out.println("Hi there"); return "Alternative"; }))); целиком или достаточно только Mono.defer(() -> "Alternative");?
@Akshay Эмпирическое правило таково: если у альтернативной ветки есть побочные эффекты, отложите ее. Mono.defer(() -> «Альтернатива») не имеет побочных эффектов, поэтому в отсрочке нет необходимости.
Я понял отсрочку, мой вопрос нужен ли нам Mono.fromFutuer(CompletableFuter.supplyAsync(())... с отсрочкой? или достаточно Mono.defer()? (Предположим, что ветка имеет побочный эффект.)
Вам не нужно будущее, чтобы сделать реактивный код параллельным. На самом деле, я бы рекомендовал избегать этого из-за проблем, описанных выше. Но иногда это необходимость.
Для тех, кто, несмотря на хорошо проголосовавший ответ, до сих пор не понимает, почему такое поведение:
Источники Reactor (Mono.xxx и Flux.xxx):
Лениво оценено : содержимое источника оценивается/запускается только тогда, когда на него подписывается подписчик;
или жадно оценил: содержимое источника оценивается немедленно, даже до того, как подписчик подпишется.
Такие выражения, как Mono.just(xxx), Flux.just(xxx), Flux.fromIterable(x,y,z), нетерпеливы.
Используя defer(), вы заставляете исходный код лениво оцениваться. Вот почему принятый ответ работает.
Итак, делаем это:
someMethodReturningAMono()
.switchIfEmpty(buildError());
с buildError(), полагающимся на нетерпеливый источник для создания альтернативного Mono всегда будет, который будет оцениваться перед подпиской:
Mono<String> buildError(){
return Mono.just("An error occured!"); //<-- evaluated as soon as read
}
Чтобы этого не произошло, сделайте следующее:
someMethodReturningAMono()
.switchIfEmpty(Mono.defer(() -> buildError()));
Прочтите этот отвечать, чтобы узнать больше.
Я не уверен, правильно ли я понял это предложение.
switchIfEmpty() is always called from the main method, even when a result with Mono.empty() is returned.. Это должно называться, не так ли?