基于Spring实现DDD时的字段校验位置及错误返回方案咨询
DDD架构下的校验分层实践与错误返回方案
一、校验逻辑的分层原则
你提到的两种校验放置方案并不冲突,符合DDD规范的分层校验规则如下:
- 适配层(DTO层)做前置契约校验:非业务强相关的格式校验、基础规则校验(比如字段非空、数字范围、日期格式等)可以放在DTO层,通过
@Valid注解触发,这层校验属于接入层的合法性过滤,作用是快速拦截非法请求,避免无效请求打入核心领域层,完全符合DDD的设计规范。 - 领域层做核心业务规则校验:和业务强绑定的规则(比如下单数量不能超过用户限购额度、商品必须处于可售状态等)必须放在领域层,要么在领域实体的构造/工厂方法中直接校验抛出异常,要么在领域服务中统一校验,保证不管请求来自REST接口、MQ消费还是内部服务调用,都必须经过核心业务规则校验,避免规则漏判。
你举例的「订单数量必须大于0」属于基础业务规则,可以两层都加校验:适配层做快速拦截返回,领域层做兜底校验,保证规则的全局一致性。
二、现有实现的优化点
你当前的实现逻辑是通顺的,但存在两个可优化的问题:
- 每次校验都调用
Validation.buildDefaultValidatorFactory()是很重的操作,Spring已经自动配置了单例的Validator实例,直接注入即可,不需要重复创建。 - 把校验结果作为返回值交给控制器判断,会产生大量重复的样板代码,更推荐用「自定义异常+全局异常处理器」的方式实现,简化上层代码。
三、统一校验错误返回的最佳实践
步骤1:自定义领域校验异常
public class DomainValidationException extends RuntimeException { private final Set<ConstraintViolation<?>> violations; public DomainValidationException(Set<ConstraintViolation<?>> violations) { super("领域模型校验不通过"); this.violations = violations; } public Set<ConstraintViolation<?>> getViolations() { return violations; } }
步骤2:改造领域服务代码
@Service public class OrderServiceImpl implements OrderService { // 直接注入Spring自动配置的Validator实例,无需自行创建 private final Validator validator; private final OrderRepository orderRepository; // 构造函数注入 public OrderServiceImpl(Validator validator, OrderRepository orderRepository) { this.validator = validator; this.orderRepository = orderRepository; } @Override public void save(Order order) { Set<ConstraintViolation<Order>> violations = validator.validate(order); if (!violations.isEmpty()) { throw new DomainValidationException(Collections.unmodifiableSet(violations)); } orderRepository.save(order); } }
步骤3:简化控制器代码
@RequestMapping( method = RequestMethod.POST, path = "/orders", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Void> addOrder(@RequestBody @Valid OrderDTO orderDTO) { Order order = ... // 转换为领域对象 orderService.save(order); return ResponseEntity.ok().build(); }
步骤4:新增全局异常处理器,统一返回JSON格式错误
@RestControllerAdvice public class GlobalValidationExceptionHandler { // 处理DTO层@Valid触发的校验异常 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleDtoValidation(MethodArgumentNotValidException e) { Map<String, String> errors = new HashMap<>(); e.getBindingResult().getFieldErrors().forEach(error -> errors.put(error.getField(), error.getDefaultMessage()) ); return ResponseEntity.badRequest().body(errors); } // 处理领域层抛出的校验异常 @ExceptionHandler(DomainValidationException.class) public ResponseEntity<Map<String, String>> handleDomainValidation(DomainValidationException e) { Map<String, String> errors = new HashMap<>(); e.getViolations().forEach(violation -> errors.put(violation.getPropertyPath().toString(), violation.getMessage()) ); return ResponseEntity.badRequest().body(errors); } }
改造后校验错误会自动返回JSON格式的响应,示例如下:
{ "quantity": "必须大于0" }
内容的提问来源于stack exchange,提问作者Samson
相关产品推荐
相关产品推荐

