1 Service 層驗證架構

Spring Validation Service層驗證_List

2 使用 @Validated 註解(方法級驗證)

// Service 接口
public interface OrderService {
    Order createOrder(OrderDTO orderDTO);
    Order updateOrder(Long id, OrderDTO orderDTO);
}

// Service 實現類
@Service
@Validated  // ⚠️ 類級別註解:開啓方法參數驗證
@Slf4j
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private ProductService productService;
    
    /**
     * 創建訂單 - 使用 @Valid 自動驗證
     */
    @Override
    public Order createOrder(@Valid OrderDTO orderDTO) {
        log.info("創建訂單: {}", orderDTO);
        
        // 驗證通過後執行業務邏輯
        
        // 1. 檢查商品庫存
        for (OrderItemDTO item : orderDTO.getItems()) {
            if (!productService.checkStock(item.getProductId(), item.getQuantity())) {
                throw new BusinessException("商品庫存不足");
            }
        }
        
        // 2. 計算總價
        BigDecimal totalAmount = calculateTotal(orderDTO);
        
        // 3. 創建訂單
        Order order = new Order();
        order.setCustomerId(orderDTO.getCustomerId());
        order.setTotalAmount(totalAmount);
        order.setStatus(OrderStatus.PENDING);
        
        // 4. 保存訂單
        return orderRepository.save(order);
    }
    
    /**
     * 更新訂單 - 使用分組驗證
     */
    @Override
    public Order updateOrder(@PathVariable Long id,
                            @Validated(ValidationGroups.Update.class) OrderDTO orderDTO) {
        Order order = orderRepository.findById(id)
            .orElseThrow(() -> new BusinessException("訂單不存在"));
        
        // 只允許更新特定狀態的訂單
        if (order.getStatus() != OrderStatus.PENDING) {
            throw new BusinessException("訂單狀態不允許修改");
        }
        
        // 更新訂單信息
        order.setRemark(orderDTO.getRemark());
        return orderRepository.save(order);
    }
    
    private BigDecimal calculateTotal(OrderDTO orderDTO) {
        return orderDTO.getItems().stream()
            .map(item -> {
                Product product = productService.getById(item.getProductId());
                return product.getPrice().multiply(new BigDecimal(item.getQuantity()));
            })
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

// DTO 定義
@Data
public class OrderDTO {
    
    @NotNull(groups = ValidationGroups.Update.class, message = "訂單ID不能為空")
    private Long id;
    
    @NotNull(message = "客户ID不能為空")
    private Long customerId;
    
    @Valid
    @NotEmpty(message = "訂單項不能為空")
    @Size(max = 100, message = "訂單項不能超過100個")
    private List<OrderItemDTO> items;
    
    @Size(max = 500, message = "備註長度不能超過500字符")
    private String remark;
    
    @NotNull(message = "收貨地址不能為空")
    @Valid
    private AddressDTO address;
}

@Data
public class OrderItemDTO {
    
    @NotNull(message = "商品ID不能為空")
    private Long productId;
    
    @NotNull(message = "數量不能為空")
    @Min(value = 1, message = "數量至少為1")
    @Max(value = 999, message = "數量不能超過999")
    private Integer quantity;
}

3 手動調用 Validator(編程式驗證)

// Service 實現類 - 手動驗證方式
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
    
    @Autowired
    private Validator validator;  // ⚠️ 注入 javax.validation.Validator
    
    @Autowired
    private ProductRepository productRepository;
    
    /**
     * 批量導入商品 - 逐個驗證
     */
    public BatchImportResult batchImport(List<ProductDTO> productDTOs) {
        List<Product> successProducts = new ArrayList<>();
        List<String> errors = new ArrayList<>();
        
        // 逐個驗證
        for (int i = 0; i < productDTOs.size(); i++) {
            ProductDTO dto = productDTOs.get(i);
            
            // 手動驗證
            Set<ConstraintViolation<ProductDTO>> violations = validator.validate(dto);
            
            if (!violations.isEmpty()) {
                // 收集錯誤信息
                String errorMsg = String.format("第%d行: %s", i + 1,
                    violations.stream()
                        .map(ConstraintViolation::getMessage)
                        .collect(Collectors.joining(", ")));
                errors.add(errorMsg);
                log.error("商品驗證失敗: {}", errorMsg);
            } else {
                // 驗證通過,轉換為實體
                Product product = convertToEntity(dto);
                successProducts.add(product);
            }
        }
        
        // 如果有錯誤,返回批量導入結果
        if (!errors.isEmpty()) {
            log.warn("批量導入完成,成功: {}, 失敗: {}", successProducts.size(), errors.size());
            return BatchImportResult.builder()
                .successCount(successProducts.size())
                .failCount(errors.size())
                .errors(errors)
                .build();
        }
        
        // 保存所有商品
        productRepository.saveAll(successProducts);
        return BatchImportResult.success(successProducts.size());
    }
    
    /**
     * 條件驗證 - 根據不同條件使用不同驗證組
     */
    public Product createOrUpdate(ProductDTO dto, boolean isUpdate) {
        Set<ConstraintViolation<ProductDTO>> violations;
        
        if (isUpdate) {
            // 更新時驗證(需要ID)
            violations = validator.validate(dto, ValidationGroups.Update.class);
        } else {
            // 創建時驗證(不需要ID)
            violations = validator.validate(dto, ValidationGroups.Create.class);
        }
        
        if (!violations.isEmpty()) {
            String errorMsg = violations.stream()
                .map(v -> v.getPropertyPath() + ": " + v.getMessage())
                .collect(Collectors.joining(", "));
            throw new ValidationException(errorMsg);
        }
        
        // 執行業務邏輯
        return isUpdate ? updateProduct(dto) : createProduct(dto);
    }
    
    /**
     * 驗證單個屬性
     */
    public void validateAndUpdatePrice(Long productId, BigDecimal newPrice) {
        ProductDTO dto = new ProductDTO();
        dto.setPrice(newPrice);
        
        // 只驗證 price 屬性
        Set<ConstraintViolation<ProductDTO>> violations = 
            validator.validateProperty(dto, "price");
        
        if (!violations.isEmpty()) {
            throw new ValidationException(
                violations.iterator().next().getMessage()
            );
        }
        
        // 更新價格
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new BusinessException("商品不存在"));
        product.setPrice(newPrice);
        productRepository.save(product);
    }
    
    /**
     * 驗證屬性值(無需創建對象實例)
     */
    public boolean isValidProductName(String name) {
        Set<ConstraintViolation<ProductDTO>> violations = 
            validator.validateValue(ProductDTO.class, "name", name);
        return violations.isEmpty();
    }
    
    private Product convertToEntity(ProductDTO dto) {
        Product product = new Product();
        product.setName(dto.getName());
        product.setPrice(dto.getPrice());
        product.setStock(dto.getStock());
        return product;
    }
    
    private Product createProduct(ProductDTO dto) {
        // 創建邏輯
        return null;
    }
    
    private Product updateProduct(ProductDTO dto) {
        // 更新邏輯
        return null;
    }
}

@Data
@Builder
class BatchImportResult {
    private int successCount;
    private int failCount;
    private List<String> errors;
    
    public static BatchImportResult success(int count) {
        return BatchImportResult.builder()
            .successCount(count)
            .failCount(0)
            .errors(Collections.emptyList())
            .build();
    }
}

4 封裝驗證工具類

/**
 * 驗證工具類 - 簡化驗證操作
 */
@Component
@Slf4j
public class ValidationHelper {
    
    @Autowired
    private Validator validator;
    
    /**
     * 驗證對象,失敗拋出異常
     */
    public <T> void validateOrThrow(T object, Class<?>... groups) {
        Set<ConstraintViolation<T>> violations = validator.validate(object, groups);
        if (!violations.isEmpty()) {
            String errorMsg = formatViolations(violations);
            log.error("驗證失敗: {}", errorMsg);
            throw new ValidationException(errorMsg);
        }
    }
    
    /**
     * 驗證對象,返回結果
     */
    public <T> ValidationResult validate(T object, Class<?>... groups) {
        Set<ConstraintViolation<T>> violations = validator.validate(object, groups);
        return new ValidationResult(violations);
    }
    
    /**
     * 驗證屬性,失敗拋出異常
     */
    public <T> void validatePropertyOrThrow(T object, String property, Class<?>... groups) {
        Set<ConstraintViolation<T>> violations =
            validator.validateProperty(object, property, groups);
        if (!violations.isEmpty()) {
            throw new ValidationException(
                property + ": " + violations.iterator().next().getMessage()
            );
        }
    }
    
    /**
     * 驗證值(無需對象實例)
     */
    public <T> boolean isValidValue(Class<T> beanType, String property, Object value, Class<?>... groups) {
        Set<ConstraintViolation<T>> violations =
            validator.validateValue(beanType, property, value, groups);
        return violations.isEmpty();
    }
    
    /**
     * 批量驗證集合
     */
    public <T> List<ValidationResult> validateList(List<T> list, Class<?>... groups) {
        return list.stream()
            .map(item -> validate(item, groups))
            .collect(Collectors.toList());
    }
    
    /**
     * 格式化驗證錯誤
     */
    private <T> String formatViolations(Set<ConstraintViolation<T>> violations) {
        return violations.stream()
            .map(v -> v.getPropertyPath() + ": " + v.getMessage())
            .collect(Collectors.joining("; "));
    }
    
    /**
     * 驗證結果封裝類
     */
    @Getter
    public static class ValidationResult {
        private final boolean valid;
        private final Map<String, String> errors;
        private final List<String> errorMessages;
        
        public ValidationResult(Set<? extends ConstraintViolation<?>> violations) {
            this.valid = violations.isEmpty();
            this.errors = violations.stream()
                .collect(Collectors.toMap(
                    v -> v.getPropertyPath().toString(),
                    ConstraintViolation::getMessage,
                    (v1, v2) -> v1 + "; " + v2,
                    LinkedHashMap::new
                ));
            this.errorMessages = new ArrayList<>(errors.values());
        }
        
        public void throwIfInvalid() {
            if (!valid) {
                throw new ValidationException(String.join("; ", errorMessages));
            }
        }
        
        public String getFirstError() {
            return errorMessages.isEmpty() ? null : errorMessages.get(0);
        }
    }
}

// 使用工具類
@Service
public class UserService {
    
    @Autowired
    private ValidationHelper validationHelper;
    
    public void createUser(UserDTO userDTO) {
        // 方式1:驗證失敗拋出異常
        validationHelper.validateOrThrow(userDTO);
        
        // 方式2:獲取驗證結果
        ValidationHelper.ValidationResult result = validationHelper.validate(userDTO);
        if (!result.isValid()) {
            log.error("用户數據驗證失敗: {}", result.getErrors());
            throw new ValidationException(result.getFirstError());
        }
        
        // 業務邏輯
    }
}

5 Service 層驗證注意事項

Spring Validation Service層驗證_#java_02

詳細説明

  1. 🔴** 關鍵配置**
// ✅ 正確:類級別添加 @Validated
@Service
@Validated
public class UserService {
    public void create(@Valid UserDTO dto) { }
}

// ❌ 錯誤:忘記類級別註解
@Service  // 缺少 @Validated
public class UserService {
    public void create(@Valid UserDTO dto) { }  // 不會觸發驗證
}
  1. 🟠** 驗證時機**
// ✅ 推薦:Controller 驗證格式,Service 驗證業務
@RestController
public class UserController {
    @PostMapping
    public ResponseEntity<?> create(@Valid @RequestBody UserDTO dto) {
        // 格式驗證已完成
        return ResponseEntity.ok(userService.create(dto));
    }
}

@Service
public class UserService {
    public User create(UserDTO dto) {
        // 只需驗證業務規則
        if (userRepository.existsByUsername(dto.getUsername())) {
            throw new BusinessException("用户名已存在");
        }
        return userRepository.save(convert(dto));
    }
}
  1. 🟡** 性能優化**
// ✅ 使用驗證組按需驗證
@Service
@Validated
public class ProductService {
    // 快速驗證:只驗證基本字段
    public void quickUpdate(@Validated(BasicValidation.class) ProductDTO dto) {
        productRepository.updatePrice(dto.getId(), dto.getPrice());
    }
    
    // 完整驗證:驗證所有字段
    public void fullUpdate(@Validated(FullValidation.class) ProductDTO dto) {
        // 完整更新邏輯
    }
}
  1. 🟢** 異常處理**
// ✅ Service 層統一處理驗證異常
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ConstraintViolationException.class)
    public ResponseEntity<?> handleConstraintViolation(ConstraintViolationException ex) {
        Map<String, String> errors = ex.getConstraintViolations().stream()
            .collect(Collectors.toMap(
                v -> v.getPropertyPath().toString(),
                ConstraintViolation::getMessage
            ));
        return ResponseEntity.badRequest().body(errors);
    }
}
  1. 🔵** 事務處理**
// ⚠️ 注意:驗證應在事務外進行
@Service
public class OrderService {
    
    @Autowired
    private Validator validator;
    
    @Transactional
    public Order createOrder(OrderDTO orderDTO) {
        // ❌ 錯誤:在事務內驗證(驗證失敗導致無用事務)
        validator.validate(orderDTO);  
        
        return orderRepository.save(convert(orderDTO));
    }
    
    // ✅ 正確:驗證在事務外
    public Order createOrderCorrect(OrderDTO orderDTO) {
        // 先驗證
        Set<ConstraintViolation<OrderDTO>> violations = validator.validate(orderDTO);
        if (!violations.isEmpty()) {
            throw new ValidationException("驗證失敗");
        }
        
        // 再開啓事務保存
        return saveOrder(orderDTO);
    }
    
    @Transactional
    private Order saveOrder(OrderDTO orderDTO) {
        return orderRepository.save(convert(orderDTO));
    }
}

6 Service 層驗證對比總結

特性

@Validated 註解

手動 Validator

推薦場景

使用方式

聲明式(註解)

編程式(代碼)

-

靈活性

⭐⭐

⭐⭐⭐⭐⭐

手動更靈活

代碼量


較多

簡單用註解

驗證時機

方法調用時

手動控制

複雜用手動

分組支持



都支持

條件驗證



手動

批量驗證



手動

適用場景

標準業務方法

批量、條件、複雜驗證

按需選擇


💡** 最佳實踐建議**:

  1. Controller 層:使用 @Valid 驗證請求參數格式
  2. Service 層:使用 @Validated 或手動 Validator 驗證業務規則
  3. 優先使用 @Validated:代碼簡潔,適合大多數場景
  4. 複雜場景用手動驗證:批量導入、條件驗證、部分屬性驗證
  5. 封裝工具類:簡化手動驗證操作,提高代碼複用性

7 @Validated/@Valid 不生效的常見情況

  1. 缺少 @Validated 註解
  • Service 層方法參數使用 @Valid/@Validated,但類上未加 @Validated,Spring 不會自動觸發方法參數校驗。
  1. 方法不是 public 或未被 Spring 管理
  • 只有 public 方法且被 Spring 容器管理的 Bean 才會觸發 AOP 校驗,private/protected 方法或 new 出來的對象不會生效。
  1. 參數未加 @Valid/@Validated
  • 方法參數未加 @Valid/@Validated 註解時,不會自動校驗。
  1. DTO 未加約束註解
  • DTO 字段未加 @NotNull、@Size 等約束註解,校驗不會生效。
  1. 嵌套對象未加 @Valid
  • DTO 中嵌套對象未加 @Valid 註解,嵌套校驗不會生效。
  1. 未配置異常處理器
  • 校驗異常未被捕獲,導致異常未能正確返回前端。
  1. 直接調用本類方法(this.xxx)
  • Spring 的 AOP 只對代理對象生效,直接調用本類方法不會觸發校驗。
  1. 未使用 SpringMVC 參數綁定
  • Controller 層未用 @RequestBody/@ModelAttribute 等參數綁定,@Valid 不會自動生效。

示例:本類方法調用不生效

@Service
@Validated
public class UserService {
    public void create(@Valid UserDTO dto) { ... }

    public void batchCreate(List<UserDTO> list) {
        // ❌ 直接調用本類方法,不會觸發參數校驗
        list.forEach(this::create);
    }
}

解決方法:通過代理調用

@Autowired
private UserService userServiceProxy;

public void batchCreate(List<UserDTO> list) {
    // ✅ 通過代理對象調用,觸發參數校驗
    list.forEach(userServiceProxy::create);
}