如何避免验证DTO与Entity时Jakarta校验注解重复?
避免DTO与JPA实体校验规则重复的方案
在Spring项目中,DTO和JPA实体的校验注解重复会增加维护成本,以下几种方案可遵循DRY原则解决该问题:
方案一:自定义组合校验注解
将重复的校验规则封装为一个自定义注解,在实体和DTO中统一使用该注解,后续只需修改自定义注解的规则即可同步所有使用场景。
实现步骤:
- 定义包含组合校验规则的自定义注解:
import jakarta.validation.Constraint; import jakarta.validation.Payload; import jakarta.validation.constraints.NotEmpty; import java.lang.annotation.*; @Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = {}) // 无需自定义校验器,直接组合现有注解 @NotEmpty // 可添加多个校验注解,如@Size(min=2, max=50) public @interface ProductName { String message() default "商品名称不能为空"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
- 在实体和DTO中使用该注解:
@Entity @Table class Product { @ProductName private String name; // id、getter、setter等 }
class NewProductRequest { @ProductName public String name; }
方案二:自定义校验器复用实体校验逻辑
编写Spring MVC自定义校验器,将DTO字段映射到实体后复用实体的校验规则,无需在DTO上添加重复注解。
实现步骤:
- 实现自定义校验器:
import jakarta.validation.ConstraintViolation; import jakarta.validation.Validator; import org.springframework.validation.Errors; import org.springframework.validation.Validator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Set; @Component public class NewProductRequestValidator implements org.springframework.validation.Validator { private final jakarta.validation.Validator beanValidator; @Autowired public NewProductRequestValidator(Validator beanValidator) { this.beanValidator = beanValidator; } @Override public boolean supports(Class<?> clazz) { return NewProductRequest.class.isAssignableFrom(clazz); } @Override public void validate(Object target, Errors errors) { NewProductRequest request = (NewProductRequest) target; // 将DTO转换为实体对象 Product product = new Product(); product.setName(request.getName()); // 复用实体的校验规则 Set<ConstraintViolation<Product>> violations = beanValidator.validate(product); // 将校验结果转换为Spring Errors格式 for (ConstraintViolation<Product> violation : violations) { String field = violation.getPropertyPath().toString(); errors.rejectValue(field, null, violation.getMessage()); } } }
- 在控制器中绑定校验器:
import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; @RestController @RequestMapping("/products") public class ProductController { @Autowired private NewProductRequestValidator requestValidator; @InitBinder("newProductRequest") public void initBinder(WebDataBinder binder) { binder.addValidators(requestValidator); } @PostMapping public ResponseEntity<?> createProduct(@Validated @RequestBody NewProductRequest request) { // 业务逻辑处理 return ResponseEntity.ok().build(); } }
方案三:映射工具结合实体校验
借助MapStruct、ModelMapper等映射工具,将DTO转换为实体后直接校验实体,无需在DTO上添加校验注解。
示例(使用MapStruct):
- 定义映射接口:
import org.mapstruct.Mapper; import org.mapstruct.Mapping; @Mapper(componentModel = "spring") public interface ProductMapper { Product toEntity(NewProductRequest request); }
- 在控制器中转换并校验:
import jakarta.validation.Validator; import jakarta.validation.ConstraintViolation; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.beans.factory.annotation.Autowired; import java.util.Set; @RestController @RequestMapping("/products") public class ProductController { @Autowired private ProductMapper productMapper; @Autowired private Validator validator; @PostMapping public ResponseEntity<?> createProduct(@RequestBody NewProductRequest request) { Product product = productMapper.toEntity(request); // 校验实体 Set<ConstraintViolation<Product>> violations = validator.validate(product); if (!violations.isEmpty()) { // 处理校验错误,如抛出MethodArgumentNotValidException throw new IllegalArgumentException("参数校验失败"); } // 保存实体 productRepository.save(product); return ResponseEntity.ok().build(); } }
方案四:校验组区分场景(适用于部分规则复用)
通过定义校验组,让实体和DTO共用同一组规则,同时区分不同业务场景(如创建/更新)的校验要求。
实现步骤:
- 定义校验组接口:
public interface CreateGroup {} public interface UpdateGroup {}
- 在实体中指定字段的校验组:
@Entity @Table class Product { @Null(groups = CreateGroup.class) @NotNull(groups = UpdateGroup.class) private Long id; @NotEmpty(groups = {CreateGroup.class, UpdateGroup.class}) private String name; }
- 在DTO和控制器中使用校验组:
class NewProductRequest { @NotEmpty(groups = CreateGroup.class) public String name; }
@PostMapping public ResponseEntity<?> createProduct(@Validated(CreateGroup.class) @RequestBody NewProductRequest request) { // 业务逻辑处理 }
内容的提问来源于stack exchange,提问作者Ruslan K.
相关产品推荐
相关产品推荐

