التحقق من البيانات
التحقق من البيانات هو خط الدفاع الأول لواجهة API—الإدخال غير الصالح يجب ألا يصل إلى منطق الأعمال، وكلما رُفض مبكراً كان ذلك أفضل.
1. ما ستتعلمه
- التعليقات
@Valid/@Validatedوآلية تفعيل التحقق - تعليقات القيود الشائعة:
@NotNull/@Size/@Pattern/@Email/@Min/@Max - تنفيذ
ConstraintValidator<A, T>للتحقق المخصص - التحقق المجمع
groupsيميز قواعد التحقق حسب السيناريو (إنشاء مقابل تحديث) - التنسيق الموحد لاستجابات أخطاء التحقق
2. قصة حقيقية من مطوّر واجهات
(1) نقطة الألم: بيانات قذرة مُخزنة في قاعدة البيانات
اكتشفت Alice بيانات غريبة في قاعدة بيانات OrderFlow: كمية طلب بقيمة -5، عنوان بريد إلكتروني بصيغة "abc"، وسعر منتج بقيمة 0. أبلغ Bob أن مستخدماً قد أرسل منتجاً بمخزون سلبي عبر واجهة API، مما تسبب في شذوذ في إحصائيات التقارير. كانت Alice قد كتبت سابقاً الكثير من كود التحقق if-else في طبقة الخدمة، وهو كود مطول ومعرض للسهو.
(2) حل Bean Validation
أعلن قواعد التحقق باستخدام التعليقات، وSpring يُفعّل التحقق تلقائياً:
public record CreateOrderRequest(
@NotNull Long productId,
@Min(1) @Max(100) Integer quantity,
@Email String customerEmail
) {}
الطلبات غير الصالحة تُرفض قبل أن تصل إلى المتحكم.
(3) العوائد
استبدلت Alice جميع كود التحقق اليدوي بـ Bean Validation، مما قلل حجم الكود في المتحكم بنسبة 40% وضمن عدم تفويت أي قاعدة تحقق. لم يعد هناك بيانات قذرة في قاعدة البيانات.
3. نظام تعليقات Bean Validation
(1) تدفق تنفيذ التحقق
graph TD
A["طلب العميل<br/>@RequestBody"] --> B{"@Valid<br/>مُفعّل؟"}
B -->|نعم| C["Hibernate Validator<br/>فحص القيود"]
C --> D{"الكل صالح؟"}
D -->|نعم| E["طريقة المتحكم<br/>تُنفّذ"]
D -->|لا| F["MethodArgumentNotValidException<br/>400 Bad Request"]
B -->|لا| G["تخطي التحقق<br/>بيانات قذرة محتملة"]
(3) طريقة تفعيل التحقق
| التعليق | الغرض | الموقع |
|---|---|---|
@Valid |
تفعيل التحقق المتسلسل (بما في ذلك الكائنات المتداخلة) | معاملات الطريقة، الحقول |
@Validated |
يدعم التحقق المجمع | الصنف ومعاملات الطريقة |
@Validated(Group.class) |
تحديد مجموعة التحقق | معاملات الطريقة |
(1) ▶ مثال: التحقق من معاملات المتحكم
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@PostMapping
public ResponseEntity<Order> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
// إذا فشل التحقق، يتم إطلاق MethodArgumentNotValidException
// قبل الوصول إلى هذا السطر
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
}
public record CreateOrderRequest(
@NotNull(message = "Product ID is required")
Long productId,
@Min(value = 1, message = "Quantity must be at least 1")
@Max(value = 100, message = "Quantity cannot exceed 100")
Integer quantity,
@Email(message = "Invalid email format")
String customerEmail
) {}
الناتج:
// التنفيذ ناجح
4. تعليقات القيود الشائعة
(1) مرجع سريع لتعليقات القيود
| التعليق | النوع المناسب | الوصف | مثال |
|---|---|---|---|
@NotNull |
جميع الأنواع | لا يمكن أن يكون null | @NotNull Long id |
@NotBlank |
String | لا يمكن أن يكون فارغاً/null/مسافات فقط | @NotBlank String name |
@NotEmpty |
String/Collection | لا يمكن أن يكون فارغاً/null | @NotEmpty List<String> tags |
@Size |
String/Collection | نطاق الطول/الحجم | @Size(min=2, max=100) |
@Min / @Max |
أنواع البيانات | نطاق القيمة | @Min(0) @Max(99999) |
@Positive |
أنواع رقمية | رقم موجب | @Positive BigDecimal price |
@Email |
String | تنسيق البريد الإلكتروني | @Email String email |
@Pattern |
String | مطابقة تعبير نمطي | @Pattern(regexp="^[A-Z]") |
@Past / @Future |
أنواع التاريخ | ماضٍ/مستقبل | @Past LocalDate birthDate |
(1) ▶ مثال: التحقق من DTO للمنتج
public record CreateProductRequest(
@NotBlank(message = "Product name is required")
@Size(min = 2, max = 200, message = "Name must be 2-200 characters")
String name,
@NotNull(message = "Price is required")
@Positive(message = "Price must be positive")
@DecimalMin(value = "0.01", message = "Price must be at least 0.01")
BigDecimal price,
@NotNull(message = "Stock is required")
@Min(value = 0, message = "Stock cannot be negative")
Integer stock,
@Email(message = "Supplier email must be valid")
String supplierEmail,
@Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU format: XXX-0000")
String sku
) {}
الناتج:
// التنفيذ ناجح
| مقارنة التعليقات | null | "" | " " | "abc" |
|---|---|---|---|---|
@NotNull |
❌ | ✅ | ✅ | ✅ |
@NotBlank |
❌ | ❌ | ❌ | ✅ |
@NotEmpty |
❌ | ❌ | ✅ | ✅ |
@NotBlank بدلاً من @NotNull، لأن السلسلة الفارغة عادةً غير صالحة أيضاً.
5. تحققات مخصصة
(1) خطوات التنفيذ
- تعريف تعليق القيد
- تنفيذ
ConstraintValidator<A, T> - الاستخدام على حقول DTO
(1) ▶ مثال: تحقق مخصص @ValidOrderQuantity
// الخطوة 1: تعريف تعليق القيد
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = OrderQuantityValidator.class)
public @interface ValidOrderQuantity {
String message() default "Order quantity exceeds product stock limit";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// الخطوة 2: تنفيذ ConstraintValidator
public class OrderQuantityValidator
implements ConstraintValidator<ValidOrderQuantity, Integer> {
private static final int MAX_QUANTITY_PER_ITEM = 100;
@Override
public boolean isValid(Integer quantity, ConstraintValidatorContext context) {
if (quantity == null) {
return true; // دع @NotNull يتولى فحص null
}
return quantity >= 1 && quantity <= MAX_QUANTITY_PER_ITEM;
}
}
// الخطوة 3: الاستخدام في DTO
public record CreateOrderRequest(
@NotNull Long productId,
@ValidOrderQuantity Integer quantity
) {}
الناتج:
// التنفيذ ناجح
6. التحقق المجمع
(1) تصنيف قواعد التحقق حسب السيناريو
سيناريو "الإنشاء" و"التحديث" يتطلبان عادةً قواعد تحقق مختلفة:
| السيناريو | مُعرّف المنتج | الاسم | السعر |
|---|---|---|---|
| إنشاء | يُولّد تلقائياً؛ غير مطلوب | مطلوب | مطلوب |
| تحديث | مطلوب (للدلالة على من أجرى التغيير) | اختياري | اختياري |
(1) ▶ مثال: التحقق المجمع
// تعريف واجهات المجموعة
public interface Create {}
public interface Update {}
// DTO مع تحقق واعٍ بالمجموعة
public record ProductRequest(
@Null(groups = Create.class, message = "ID must be null for creation")
@NotNull(groups = Update.class, message = "ID is required for update")
Long id,
@NotBlank(groups = Create.class, message = "Name is required for creation")
@Size(min = 2, max = 200)
String name,
@NotNull(groups = Create.class, message = "Price is required for creation")
@Positive BigDecimal price
) {}
الناتج:
// التنفيذ ناجح
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
@PostMapping
public ResponseEntity<Product> create(
@Validated(Create.class) @RequestBody ProductRequest request) {
// يتم تطبيق تحققات مجموعة Create فقط
// ...
return ResponseEntity.status(HttpStatus.CREATED).build();
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(
@PathVariable Long id,
@Validated(Update.class) @RequestBody ProductRequest request) {
// يتم تطبيق تحققات مجموعة Update فقط
// ...
return ResponseEntity.ok().build();
}
}
(2) ▶ مثال: التحقق المتداخل
public record CreateOrderRequest(
@NotNull Long productId,
@ValidOrderQuantity Integer quantity,
@Valid @NotNull ShippingAddress shippingAddress
) {}
public record ShippingAddress(
@NotBlank String street,
@NotBlank String city,
@NotBlank String zipCode,
@Pattern(regexp = "^[A-Z]{2}$") String country
) {}
الناتج:
// التنفيذ ناجح
@Valid؛ وإلا فلن يسري التحقق من الحقول المتداخلة. @Validated لا يدعم التحقق المتسلسل المتداخل.
7. تنسيق استجابات أخطاء التحقق
(1) توحيد تنسيق استجابة الخطأ
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(
MethodArgumentNotValidException ex) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("timestamp", Instant.now());
body.put("status", HttpStatus.BAD_REQUEST.value());
List<Map<String, String>> errors = ex.getBindingResult()
.getFieldErrors().stream()
.map(fe -> Map.of(
"field", fe.getField(),
"message", fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "",
"rejectedValue", fe.getRejectedValue() != null ? fe.getRejectedValue().toString() : "null"
))
.toList();
body.put("errors", errors);
return ResponseEntity.badRequest().body(body);
}
}
{
"timestamp": "2024-01-15T10:00:00Z",
"status": 400,
"errors": [
{"field": "quantity", "message": "Quantity must be at least 1", "rejectedValue": "0"},
{"field": "customerEmail", "message": "Invalid email format", "rejectedValue": "abc"}
]
}
8. مثال شامل: نظام التحقق الكامل لـ OrderFlow
// مجموعات التحقق
package com.orderflow.validation;
public interface Create {}
public interface Update {}
// تحقق مخصص: @ValidShippingAddress
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ShippingAddressValidator.class)
public @interface ValidShippingAddress {
String message() default "Invalid shipping address";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class ShippingAddressValidator
implements ConstraintValidator<ValidShippingAddress, String> {
@Override
public boolean isValid(String address, ConstraintValidatorContext ctx) {
if (address == null || address.isBlank()) return false;
return address.length() >= 10 && address.length() <= 500;
}
}
// DTOs
public record CreateOrderRequest(
@NotNull(groups = Create.class) Long productId,
@Min(value = 1, message = "Quantity must be at least 1")
@Max(value = 100, message = "Quantity cannot exceed 100")
Integer quantity,
@Email String customerEmail,
@ValidShippingAddress String shippingAddress
) {}
public record CreateProductRequest(
@Null(groups = Create.class) @NotNull(groups = Update.class) Long id,
@NotBlank(groups = Create.class) @Size(min = 2, max = 200) String name,
@NotNull(groups = Create.class) @Positive BigDecimal price,
@Min(0) Integer stock,
@Pattern(regexp = "^[A-Z]{3}-\\d{4}$", message = "SKU: XXX-0000") String sku
) {}
// المتحكم
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@PostMapping
public ResponseEntity<Void> create(
@Validated(Create.class) @RequestBody CreateOrderRequest req) {
// التحقق ناجح، الانتقال إلى منطق الأعمال
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}
❓ أسئلة شائعة
public interface Create extends Default {}.resources/ValidationMessages.properties، مثل order.quantity.invalid=Order quantity must be between {min} and {max}؛ يتم دعم ملفات متعددة اللغات.📖 ملخص
@Valid/@Validatedيُفعّلان التحقق؛ إذا فشل التحقق، يُطلقMethodArgumentNotValidException- التعليقات الشائعة: استخدم
@NotBlankللسلاسل،@Min/@Max/@Positiveللأرقام،@Emailلعناوين البريد الإلكتروني - ثلاث خطوات لإنشاء تحقق مخصص: تعريف تعليق → تنفيذ ConstraintValidator → استخدامه
- التحقق المجمع يميز بين سيناريو الإنشاء والتحديث؛
@Validated(Group.class)يحدد المجموعة - يجب إضافة
@Validإلى الكائنات المتداخلة لإجراء التحقق المتسلسل @RestControllerAdviceيُوحّد تنسيق استجابات الأخطاء
📝 تمارين
-
تمرين أساسي (الصعوبة: ⭐): أضف تعليقات Bean Validation إلى
CreateOrderRequestوCreateProductRequestفي OrderFlow للتحقق من الإدخال غير الصالح وإرجاع خطأ 400. -
تمرين متقدم (الصعوبة ⭐⭐): نفّذ تحققاً مجمعاً—لعمليات
Create، يكونnameوpriceمطلوبين؛ لعملياتUpdate، يكونidمطلوباً وnameوpriceاختياريين. نفّذ تحققاً مخصصاً@ValidShippingAddress. -
تحدٍ (الصعوبة: ⭐⭐⭐): أنشئ تحققاً
@UniqueProductSkuيحقنProductRepositoryللتحقق مما إذا كان SKU موجوداً مسبقاً، لتنفيذ التحقق من التفرد على مستوى قاعدة البيانات. فكّر في الحد الفاصل بين مسؤوليات التحقق وطبقة الأعمال.



