1 Service 層驗證架構
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 層驗證注意事項
詳細説明:
- 🔴** 關鍵配置**
// ✅ 正確:類級別添加 @Validated
@Service
@Validated
public class UserService {
public void create(@Valid UserDTO dto) { }
}
// ❌ 錯誤:忘記類級別註解
@Service // 缺少 @Validated
public class UserService {
public void create(@Valid UserDTO dto) { } // 不會觸發驗證
}
- 🟠** 驗證時機**
// ✅ 推薦: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));
}
}
- 🟡** 性能優化**
// ✅ 使用驗證組按需驗證
@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) {
// 完整更新邏輯
}
}
- 🟢** 異常處理**
// ✅ 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);
}
}
- 🔵** 事務處理**
// ⚠️ 注意:驗證應在事務外進行
@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
|
推薦場景
|
|
使用方式 |
聲明式(註解)
|
編程式(代碼)
|
-
|
|
靈活性 |
⭐⭐
|
⭐⭐⭐⭐⭐
|
手動更靈活
|
|
代碼量 |
少
|
較多
|
簡單用註解
|
|
驗證時機 |
方法調用時
|
手動控制
|
複雜用手動
|
|
分組支持 |
✅
|
✅
|
都支持
|
|
條件驗證 |
❌
|
✅
|
手動
|
|
批量驗證 |
❌
|
✅
|
手動
|
|
適用場景 |
標準業務方法
|
批量、條件、複雜驗證
|
按需選擇
|
💡** 最佳實踐建議**:
- Controller 層:使用
@Valid驗證請求參數格式 - Service 層:使用
@Validated或手動Validator驗證業務規則 - 優先使用 @Validated:代碼簡潔,適合大多數場景
- 複雜場景用手動驗證:批量導入、條件驗證、部分屬性驗證
- 封裝工具類:簡化手動驗證操作,提高代碼複用性
7 @Validated/@Valid 不生效的常見情況
- 缺少 @Validated 註解
- Service 層方法參數使用 @Valid/@Validated,但類上未加 @Validated,Spring 不會自動觸發方法參數校驗。
- 方法不是 public 或未被 Spring 管理
- 只有 public 方法且被 Spring 容器管理的 Bean 才會觸發 AOP 校驗,private/protected 方法或 new 出來的對象不會生效。
- 參數未加 @Valid/@Validated
- 方法參數未加 @Valid/@Validated 註解時,不會自動校驗。
- DTO 未加約束註解
- DTO 字段未加 @NotNull、@Size 等約束註解,校驗不會生效。
- 嵌套對象未加 @Valid
- DTO 中嵌套對象未加 @Valid 註解,嵌套校驗不會生效。
- 未配置異常處理器
- 校驗異常未被捕獲,導致異常未能正確返回前端。
- 直接調用本類方法(this.xxx)
- Spring 的 AOP 只對代理對象生效,直接調用本類方法不會觸發校驗。
- 未使用 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);
}