You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中Webflux+MapStruct实现用户创建的问题排查

Spring Webflux + R2DBC 用户创建接口:异常处理与空指针问题修复

场景说明

基于Spring Boot,使用Webflux结合R2DBC(PostgreSQL)实现用户创建接口,定义四类对象处理逻辑:

  • UserDetailsRequestModel:接收前端请求体
  • UserResponseModel:返回给前端的响应结果
  • UserDto:控制器与服务层之间的数据传输对象
  • UserEntity:数据库持久化实体

通过MapStruct实现对象转换,仓库基于ReactiveCrudRepository。

核心代码实现

用户创建请求体(UserDetailsRequestModel)

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

返回对象(UserResponseModel)

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)

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;
}

MapStruct转换接口(UserMapper)

@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);
}

仓库接口(UserRepository)

修正原仓库方法参数错误,Reactive查询需接收具体参数类型而非Mono

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

原控制器代码

@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);
}

原服务层实现

@Override
public Mono<UserDto> createUser(Mono<UserDto> userDtoMono) {
    return userDtoMono
            .map(userDto -> UserMapper.USERMAPPER.toUserEntity(userDto))
            .flatMap(userEntity -> {
                if (userRepository.findByEmail(Mono.just(userEntity.getEmail())) == null) {
                    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;
            });
}

遇到的问题

  1. 服务层无法正确处理用户已存在的场景,需实现异常抛出逻辑
  2. 运行时触发java.lang.NullPointerException: The mapper returned a null Mono异常

问题分析与解决方案

1. 空指针异常原因及修复

错误根源

  • 原仓库方法参数不符合Reactive规范,findByEmail应接收String而非Mono<String>
  • 服务层中if (userRepository.findByEmail(...) == null)判断错误:Mono永远不会为null,无查询结果时返回Mono.empty()
  • else分支返回null,导致flatMap接收null值触发空指针异常

修复后的服务层代码

// 自定义用户已存在异常
public class UserAlreadyExistsException extends RuntimeException {
    public UserAlreadyExistsException(String message) {
        super(message);
    }
}

// 服务层实现
@Override
public Mono<UserDto> createUser(UserDto userDto) {
    UserEntity userEntity = UserMapper.USERMAPPER.toUserEntity(userDto);
    
    return userRepository.findByEmail(userEntity.getEmail())
            // 查询到用户则抛出异常
            .flatMap(existingUser -> Mono.error(new UserAlreadyExistsException("邮箱" + userEntity.getEmail() + "已被注册")))
            // 未查询到则继续执行保存逻辑
            .then(Mono.just(userEntity))
            .map(entity -> {
                // 生成唯一userId,建议用UUID
                entity.setUserId(UUID.randomUUID().toString());
                entity.setEncryptedPassword("encryptedPassword"); // 实际项目需用加密算法处理密码
                entity.setEmailVerificationToken("emailVerifToken");
                entity.setEmailVerificationStatus(false);
                return entity;
            })
            // 保存实体
            .flatMap(userRepository::save)
            // 转换为Dto返回
            .map(UserMapper.USERMAPPER::entityToUserDto);
}

简化后的控制器代码

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

2. 用户已存在的异常处理

添加全局异常处理器,返回友好的HTTP响应:

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(UserAlreadyExistsException.class)
    public ResponseEntity<ErrorResponse> handleUserAlreadyExists(UserAlreadyExistsException ex) {
        ErrorResponse error = new ErrorResponse(
                HttpStatus.CONFLICT.value(),
                ex.getMessage(),
                LocalDateTime.now()
        );
        return new ResponseEntity<>(error, HttpStatus.CONFLICT);
    }
}

// 错误响应实体
public class ErrorResponse {
    private int statusCode;
    private String message;
    private LocalDateTime timestamp;

    public ErrorResponse(int statusCode, String message, LocalDateTime timestamp) {
        this.statusCode = statusCode;
        this.message = message;
        this.timestamp = timestamp;
    }

    // getter方法
    public int getStatusCode() { return statusCode; }
    public String getMessage() { return message; }
    public LocalDateTime getTimestamp() { return timestamp; }
}

内容的提问来源于stack exchange,提问作者davidvera

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 01:52:19