0

I try to implement a webservice using webflux and an entry point aims to create a new User. For this project I try to implement Webflux with R2DBC (Postgresql).

I have 4 classes that are created to handle user creation:

  1. UserDetailsRequestModel used for sending user's information
  2. UserResponseModel used as returned object for user
  3. UserDto that communicates from controller to service
  4. UserEntity that is used for storing data on postgres

I have a Mapper class that uses MapStruct and a repository interface that uses ReactiveCrudRepository.

The user classes are rather simple:

Payload for user creation:

public class UserDetailsRequestModel {
    private String firstName;
    private String lastName;
    private String email;
    private String password;
}

Payload returned

public class UserResponseModel {
    private String userId;
    private String firstName;
    private String lastName;
    private String email;
}

UserDto

public class UserDto implements Serializable {
    @Serial
    private static final long serialVersionUID = -386521462517601642L;

    private Long id;
    private String userId;
    private String firstName;
    private String lastName;
    private String email;
    private String password;
    private String encryptedPassword;
    private String emailVerificationToken;
    private Boolean emailVerificationStatus = false;
}

UserEntity

I removed most of the annotation here...

public class UserEntity implements Serializable {
    @Serial
    private static final long serialVersionUID = -5590905788591386398L;

    @Id
    private Long id;

    @Column
    @NotNull
    @Size(max = 50)
    private String userId;
    private String firstName;
    private String lastName;
    private String email;
    private String encryptedPassword;
    private String emailVerificationToken;
    private Boolean emailVerificationStatus = false;
}

Now I have a Mapper interface that uses MapStruct:

@Mapper
public interface UserMapper {
    UserMapper USERMAPPER = Mappers.getMapper( UserMapper.class );
    UserDto toUserDto(UserDetailsRequestModel userDetails);
    UserResponseModel toUserResponse(UserDto userDto);
    UserEntity toUserEntity(UserDto userDto);
    UserDto entityToUserDto(UserEntity userEntity);
}

This interface aims to help me converting request to dto, dto to response, entity to dto and dto to entity...

My repository interface is basic:

@Repository
public interface UserRepository extends ReactiveCrudRepository<UserEntity, Long> {
    Mono<UserEntity> save(Mono<UserEntity> userEntity);
    Mono<UserEntity> findByEmail(Mono<String> email);
}

Now I have a controller and a service layer: I have a Mono<UserDetailsRequestModel> object as requestbody. I want to convert this object onto Mono<UserDto>, then call my service layer, convert this Mono<UserDto> onto Mono<UserEntity>, persist data, converting the Mono<UserEntity> onto Mono<UserDto> and finaly return a Mono<UserResponseModel> ...

@PostMapping(
        produces = MediaType.TEXT_EVENT_STREAM_VALUE
)
public Mono<UserResponseModel> createUser(@RequestBody Mono<UserDetailsRequestModel> userDetailsRequestModelMono) {
    return userDetailsRequestModelMono
            .map(userDetailsRequestModel -> UserMapper.USERMAPPER.toUserDto(userDetailsRequestModel))
            .map(userDto -> {
                    Mono<UserDto> userDtoMono = this.userService.createUser(Mono.just(userDto));
                    System.out.println("UserDto > " + userDto.toString());
                    return userDtoMono;
            })
            .flatMap(userDtoMono -> {
                Mono<UserResponseModel> userResponseModelMono = userDtoMono.map(userDtoResponse -> {
                    UserResponseModel userResponseModel = UserMapper.USERMAPPER.toUserResponse(userDtoResponse);
                    System.out.println("UserResponseModel > " + userResponseModel.toString());
                    return userResponseModel;
                });
                return userResponseModelMono;
            })
            .doOnError(err -> System.out.println("Error caught >> "  + err))
            .doFinally(System.out::println);
}

In my Service I have the following implementation:

@Override
public Mono<UserDto> createUser(Mono<UserDto> userDtoMono) {
    // System.out.println(userDtoMono.block().toString());
    return userDtoMono
            .map(userDto -> UserMapper.USERMAPPER.toUserEntity(userDto))
            .flatMap(userEntity -> {
                if (userRepository.findByEmail(Mono.just(userEntity.getEmail())) == null) {
                    // create user
                    userEntity.setUserId("azvxcvxcxcvcx");
                    userEntity.setVersion(1L);
                    userEntity.setEmailVerificationToken("emailVerifToken");
                    userEntity.setEmailVerificationStatus(Boolean.FALSE);
                    userEntity.setEncryptedPassword("encryptedPassword");
                    System.out.println("UserEntity > " + userEntity.toString());
                    return userRepository.save(Mono.just(userEntity));
                } else {
                    return null;
                }
            })
            .map(userEntity -> {
                UserDto userDto = UserMapper.USERMAPPER.entityToUserDto(userEntity);
                System.out.println(userDto);
                return userDto;
            });
}

I have 2 issues and questions:

  1. In my service layer, i would like to manage the case if user already exists and if so throw an exception (i'll try later to create an exception handler but at this stage that's not the point...)
  2. I have issue converting my objects apparently and I retrieve exception (null mono). Actually I don't get where is my error (i begin playing with webflux).

Here is my logs for the request sent:

UserDto > UserDto(id=null, userId=null, firstName=John, lastName=Wick, email=jw@mail.com, password=123, encryptedPassword=null, emailVerificationToken=null, emailVerificationStatus=null)
Error caught >> java.lang.NullPointerException: The mapper returned a null Mono
2023-03-20 21:51:55 [reactor-http-nio-3] DEBUG  r.n.http.server.HttpServerOperations - [e1be5f46-1, L:/[0:0:0:0:0:0:0:1]:8090 - R:/[0:0:0:0:0:0:0:1]:63068] Decreasing pending responses, now 0
2023-03-20 21:51:55 [reactor-http-nio-3] DEBUG  r.n.http.server.HttpServerOperations - [e1be5f46-1, L:/[0:0:0:0:0:0:0:1]:8090 - R:/[0:0:0:0:0:0:0:1]:63068] Last HTTP packet was sent, terminating the channel
2023-03-20 21:51:55 [reactor-http-nio-3] DEBUG  r.netty.channel.ChannelOperations - [e1be5f46-1, L:/[0:0:0:0:0:0:0:1]:8090 - R:/[0:0:0:0:0:0:0:1]:63068] [HttpServer] Channel inbound receiver cancelled (operation cancelled).
2023-03-20 21:51:55 [reactor-http-nio-3] DEBUG  r.n.http.server.HttpServerOperations - [e1be5f46-1, L:/[0:0:0:0:0:0:0:1]:8090 - R:/[0:0:0:0:0:0:0:1]:63068] Last HTTP response frame
onError
Lex Li
  • 60,503
  • 9
  • 116
  • 147
davidvera
  • 1,292
  • 2
  • 24
  • 55

1 Answers1

0

I have issue converting my objects apparently and I retrieve exception (null mono). Actually I don't get where is my error (i begin playing with webflux).

This comes from the line where null is returned instead of Mono.just in UserService.createUser.

In my service layer, i would like to manage the case if user already exists and if so throw an exception (i'll try later to create an exception handler but at this stage that's not the point...)

The easiest way to do this is to return with an exception wrapped in Mono.error and put ResponseStatus annotation on the exception. e.g.

Create a new Exception:

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class UserAlreadyRegisteredException extends RuntimeException {
   public UserAlreadyRegisteredException(String email) {
    super("user already exist with email: " + email);
  }
}

And use it in the following way:

...
else {
 return Mono.error(new UserAlreadyRegisteredException(userEntity.getEmail()));
}

see

other options for exception handling:

Other problem with your code

  1. As Mono<UserEntity> findByEmail(Mono<String> email) will return Mono.just and not null when it cannot find an email address (check the documentation of org.springframework.data.repository.reactive.ReactiveCrudRepository#existsById(ID)) your if statement will always returns false.

  2. Unfortunately the fix requires more than just to change userRepository.findByEmail(Mono.just(userEntity.getEmail())) == null to userRepository.findByEmail(Mono.just(userEntity.getEmail())) == Mono.just() as we have to check if it's empty or not.
    See How to check if Mono is empty? for different solutions.
    One that worked for me is to create Optional from Mono in first place:

         .flatMap(
         userEntity ->
             userRepository
                 .findByEmail(Mono.just(userEntity.getEmail()))
                 .map(Optional::of)
                 .defaultIfEmpty(Optional.empty())
                 .flatMap(
                     optionalUser -> {
                       if (optionalUser.isEmpty()) {
                         userEntity.setUserId("azvxcvxcxcvcx");
                         userEntity.setEmailVerificationToken("emailVerifToken");
                         userEntity.setEmailVerificationStatus(Boolean.FALSE);
                         userEntity.setEncryptedPassword("encryptedPassword");
                         System.out.println("UserEntity > " + userEntity.toString());
                         return userRepository.save(userEntity);
                       } else {
                           return Mono.error(new UserAlreadyRegisteredException( userEntity.getEmail()));
                       }
                     }))
    
Toni
  • 3,296
  • 2
  • 13
  • 34
balazs
  • 457
  • 2
  • 7