Spring Boot自定义请求类注入Repository做请求校验失败问题
问题描述
尝试在UserCreateRequest类中校验email和name字段是否已存在于数据库,但通过@Autowired注入的userRepository始终为null。希望在请求类中完成该数据库校验逻辑,而非在控制器中处理。
相关代码
UserCreateRequest.java
@Getter @Setter @Component public class UserCreateRequest { private UserRepository userRepository; public UserCreateRequest() { } @Autowired public UserCreateRequest(UserRepository userRepository) { this.userRepository = userRepository; } @NotBlank(message = "Name is mandatory") @Size(min = 2, max = 20, message = "Name should be between 2 and 20 characters") private String name; @NotBlank(message = "Email is mandatory") @Email(message = "Email should be valid") private String email; @NotNull(message = "Date of birth is mandatory") @DateFormatRule(message = "Date of birth should be in yyyy-MM-dd format") private String dob; public User validated() { if (userRepository.findUserByEmail(email).isPresent()) { throw new RuntimeException("Email already exists"); } if (userRepository.findUserByName(name).isPresent()) { throw new RuntimeException("Name already exists"); } User user = new User(); user.setName(name); user.setEmail(email); user.setDob(LocalDate.parse(dob)); return user; } }
UserController.java
@PostMapping public ResponseEntity<User> addUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { User validatedData = userCreateRequest.validated(); User createdUser = this.userService.addUser(validatedData); return ResponseEntity.ok(createdUser); }
报错信息
(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:1623)\n", "message": "Cannot invoke \"com.test.restapi.repository.UserRepository.findUserByEmail(String)\" because \"this.userRepository\" is null", "path": "/api/students"
原因分析
UserCreateRequest是通过@RequestBody接收的,Spring会使用Jackson将请求体JSON反序列化为该类的实例,这个实例不是Spring容器管理的Bean——即使你加了@Component注解,也不会从容器中获取实例,而是由Jackson直接创建。因此@Autowired的注入逻辑不会生效,userRepository自然为null。
解决方案
推荐使用自定义Spring验证注解来实现数据库唯一性校验,这符合Spring的验证规范,且能保持请求类的职责单一。
步骤1:创建自定义唯一性校验注解
1.1 邮箱唯一性注解@UniqueEmail
@Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = UniqueEmailValidator.class) public @interface UniqueEmail { String message() default "Email already exists"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
1.2 用户名唯一性注解@UniqueName
@Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = UniqueNameValidator.class) public @interface UniqueName { String message() default "Name already exists"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
步骤2:实现注解对应的验证器
2.1 邮箱验证器UniqueEmailValidator
@Component public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> { private final UserRepository userRepository; // 构造方法注入Spring容器管理的UserRepository public UniqueEmailValidator(UserRepository userRepository) { this.userRepository = userRepository; } @Override public boolean isValid(String email, ConstraintValidatorContext context) { // 空值由@NotBlank注解处理,此处无需重复校验 if (email == null) { return true; } return !userRepository.findUserByEmail(email).isPresent(); } }
2.2 用户名验证器UniqueNameValidator
@Component public class UniqueNameValidator implements ConstraintValidator<UniqueName, String> { private final UserRepository userRepository; public UniqueNameValidator(UserRepository userRepository) { this.userRepository = userRepository; } @Override public boolean isValid(String name, ConstraintValidatorContext context) { if (name == null) { return true; } return !userRepository.findUserByName(name).isPresent(); } }
步骤3:修改UserCreateRequest
移除@Component注解和userRepository相关代码,在对应字段上添加自定义注解:
@Getter @Setter public class UserCreateRequest { @NotBlank(message = "Name is mandatory") @Size(min = 2, max = 20, message = "Name should be between 2 and 20 characters") @UniqueName // 添加用户名唯一性校验 private String name; @NotBlank(message = "Email is mandatory") @Email(message = "Email should be valid") @UniqueEmail // 添加邮箱唯一性校验 private String email; @NotNull(message = "Date of birth is mandatory") @DateFormatRule(message = "Date of birth should be in yyyy-MM-dd format") private String dob; // 仅保留对象转换逻辑,移除校验逻辑 public User toUser() { User user = new User(); user.setName(name); user.setEmail(email); user.setDob(LocalDate.parse(dob)); return user; } }
步骤4:修改控制器
此时@Valid会自动触发所有校验(包括自定义的唯一性校验),无需手动调用validated()方法:
@PostMapping public ResponseEntity<User> addUser(@Valid @RequestBody UserCreateRequest userCreateRequest) { User user = userCreateRequest.toUser(); User createdUser = this.userService.addUser(user); return ResponseEntity.ok(createdUser); }
其他可选方案(不推荐)
- 将校验逻辑移至Service层:在
userService.addUser()方法中添加数据库唯一性检查,这是分层架构的常规做法,但不符合你“在请求类中校验”的需求。 - 使用
@Configurable:结合AspectJ让Jackson创建的实例也能被Spring注入,但需要额外配置AspectJ,复杂度高,不适合常规项目。
内容的提问来源于stack exchange,提问作者b. tsutskiridze
相关产品推荐
相关产品推荐

