问题
I am have two versions of a Webflux/Reactor handler class. This class mimics a user sign up use case - as far as the business logic is concerned.
The first version of the class relies upon a Mono<User>
whereas the second version uses a plain User
.
First version of the class: this is the version relying on a Mono<User>
argument down the chain. Notice the top level public method createUser
uses a userMono
.
@Component
@RequiredArgsConstructor
public class UserHandler {
private final @NonNull UserRepository userRepository;
private final @NonNull UserValidator userValidator;
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
Mono<User> userMono = serverRequest.bodyToMono(User.class).cache();
return validateUser(userMono)
.switchIfEmpty(validateEmailNotExists(userMono))
.switchIfEmpty(saveUser(userMono))
.single();
}
private Mono<ServerResponse> validateUser(Mono<User> userMono) {
return userMono
.map(this::computeErrors)
.filter(AbstractBindingResult::hasErrors)
.flatMap(err ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(err.getAllErrors()))
);
}
private AbstractBindingResult computeErrors(User user) {
AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
userValidator.validate(user, errors);
return errors;
}
private Mono<ServerResponse> validateEmailNotExists(Mono<User> userMono) {
return userMono
.flatMap(user -> userRepository.findByEmail(user.getEmail()))
.flatMap(existingUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
);
}
private Mono<ServerResponse> saveUser(Mono<User> userMono) {
return userMono
.flatMap(userRepository::save)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser))
);
}
}
Second version of the class: that is the version relying on a User
argument.
@Component
@RequiredArgsConstructor
public class UserHandler {
private final @NonNull UserRepository userRepository;
private final @NonNull UserValidator userValidator;
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
return serverRequest
.bodyToMono(User.class)
.flatMap(user ->
validateUser(user)
.switchIfEmpty(validateEmailNotExists(user))
.switchIfEmpty(saveUser(user))
.single()
);
}
private Mono<ServerResponse> validateUser(User user) {
return Mono.just(new BeanPropertyBindingResult(user, User.class.getName()))
.doOnNext(err -> userValidator.validate(user, err))
.filter(AbstractBindingResult::hasErrors)
.flatMap(err ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(err.getAllErrors()))
);
}
private Mono<ServerResponse> validateEmailNotExists(User user) {
return userRepository.findByEmail(user.getEmail())
.flatMap(existingUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
);
}
private Mono<ServerResponse> saveUser(User user) {
return userRepository.save(user)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser))
);
}
}
Now my questions:
What are the pros and cons of each of the two versions?
Which of the two would you recommend keeping?
Any feedback, advice and opinion welcome.
回答1:
you want feedback here is my feedback
your methods dont really make sense. If you look at the declarations of your methods without the containing code.
private Mono<ServerResponse> validateUser(User user)
this makes no sense, this method should validate a user but you return a ServerResponse? In my opinion a validation should first takes place, and return some sort of boolean, or a list of validation errors.
I do not recommend any of the solutions you have presented, you should instead looking into and start using Mono.error
instead of doing switchIfEmpty
You should seperate response building from validation logic.
What happens if validation rules change? or you want other responses based on what validation fails? right now they are together.
you can already see that you are returning the same bad request in two places but with different error messages. Duplications
This is my opinion and what i would do:
- receive a request
- map request to a user (bodyToMono)
- validate the user in a method that vill return a list containing number of errors
- check this list if user validation has failed, and if failed map the mono user to a mono error containing a illegalArgumentException with some sort of text of the error.
- map the exception in the mono error to a status code
- if validation passes save the user in a
Mono.doOnSuccess
block
this to me is much more clear and predictable with separations of return codes, and validation logic.
回答2:
Using Thomas Andolf's advice together with that of other users, I came up with the following implementation:
@Component
@RequiredArgsConstructor
public class UserHandler {
private final @NonNull UserRepository userRepository;
private final @NonNull UserValidator userValidator;
public Mono<ServerResponse> findUsers(ServerRequest serverRequest) {
return ok()
.contentType(APPLICATION_JSON)
.body(userRepository.findAll(), User.class);
}
public Mono<ServerResponse> createUser(ServerRequest serverRequest) {
return serverRequest.bodyToMono(User.class)
.flatMap(this::validate)
.flatMap(this::validateEmailNotExists)
.flatMap(this::saveUser)
.flatMap(newUser -> status(CREATED)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(newUser))
)
.onErrorResume(ValidationException.class, e -> status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(e.getErrors()))
)
.onErrorResume(DuplicateUserException.class, e -> status(CONFLICT)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromValue(e.getErrorMessage()))
);
}
private Mono<User> validateEmailNotExists(User user) {
return userRepository.findByEmail(user.getEmail())
.flatMap(userMono -> Mono.<User>error(new DuplicateUserException("User already exists")))
.switchIfEmpty(Mono.just(user));
}
private Mono<User> saveUser(User user) {
return userRepository.save(user);
}
private Mono<User> validate(User user) {
AbstractBindingResult errors = computeErrors(user);
return errors.hasErrors() ? Mono.error(new ValidationException(errors.getAllErrors())) : Mono.just(user);
}
private AbstractBindingResult computeErrors(User user) {
AbstractBindingResult errors = new BeanPropertyBindingResult(user, User.class.getName());
userValidator.validate(user, errors);
return errors;
}
}
It relies upon Mono.error
, custom exceptions, and the onErrorResume()
operator. It is equivalent to the two implementations in the question but somewhat leaner.
来源:https://stackoverflow.com/questions/57839652/should-i-rely-on-mono-of-item-or-plain-item-arguments-when-composing-reactiv