بناء واجهات REST مع Spring Boot

أجسام الطلبات و @RequestBody

18 دقيقة الدرس 4 من 13

أجسام الطلبات و @RequestBody

عندما يُرسل العميل بيانات إلى واجهة برمجية — لإنشاء مستخدم جديد، أو تحديث طلب، أو إرسال نموذج — تنتقل هذه البيانات داخل جسم طلب HTTP، وعادةً ما تكون بتنسيق JSON. تُخبر الحاشية @RequestBody في Spring Boot الإطارَ بأن يحوّل ذلك JSON تلقائيًا إلى كائن Java. إنّ فهم آلية هذه العملية، وسبب ربط البيانات بـ كائن نقل البيانات (DTO) بدلًا من كيان JPA مباشرةً، يُعدّ من أهم القرارات التصميمية التي ستتخذها في أي REST API.

كيف تعمل @RequestBody

عندما يصل طلب إلى دالة في المتحكّم (controller) المحدودة بـ @RequestBody، يُفوّض DispatcherServlet في Spring المهمةَ إلى HttpMessageConverter. نظرًا لأن Spring Boot يُهيّئ Jackson تلقائيًا (عبر MappingJackson2HttpMessageConverter)، فإن كل دالة في المتحكّم تقبل طلبًا بـ Content-Type: application/json تحصل مجانًا على إمكانية تحويل JSON إلى Java.

import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/users") public class UserController { @PostMapping public String createUser(@RequestBody UserCreateRequest request) { // لقد ملأ Jackson كائن `request` من جسم JSON بالفعل return "Created user: " + request.getName(); } }

تُنشئ Spring نسخةً من الفئة المستهدفة، وتطابق اسم كل حقل JSON مع حقل Java أو setter باسمه، ثم تُعبّئه. أما الحقول الغائبة في JSON فتبقى بقيمها الافتراضية (عادةً null أو صفر).

@RequestBody إلزامية. بدونها تتعامل Spring مع المعامل كـ model attribute (بيانات من query string أو form). إن نسيت الحاشية وأرسلت JSON ستكون كل الحقول null. هذا خطأ شائع جدًا للمبتدئين.

ما هو DTO ولماذا يهم؟

كائن نقل البيانات (DTO) هو فئة Java بسيطة تهدف فقط إلى نقل البيانات عبر حدود — في هذه الحالة بين طبقة HTTP وطبقة الخدمة. لا يحتوي على حاشيات persistence، ولا منطق أعمال، ولا تبعيات للإطار. قارن بين المقاربتين:

المقاربة الأولى — الربط مباشرةً بكيان JPA (تجنّب هذا):

// خطر: ربط جسم الطلب مباشرةً بالكيان @PostMapping public User createUser(@RequestBody User user) { return userRepository.save(user); }

المقاربة الثانية — الربط بـ DTO مخصص (المقاربة الصحيحة):

// آمن: العميل يتحكم فقط بما يعرضه DTO @PostMapping public UserResponse createUser(@RequestBody UserCreateRequest request) { User user = userService.create(request); return new UserResponse(user); }

تعاني المقاربة الأولى من مشكلة الإسناد الجماعي (mass assignment) — يستطيع عميل خبيث تعيين حقول مثل id أو role أو isAdmin بمجرد تضمينها في جسم JSON. ستُسعدها Spring في الربط ما لم تحجب كل حقل يدويًا. أما المقاربة الثانية فآمنة افتراضيًا: لا يستطيع العميل سوى توفير الحقول التي وضعتها عمدًا في DTO.

لا تربط @RequestBody مباشرةً بكيان JPA أبدًا. إنه ثغرة أمنية (mass assignment / over-posting) ويُقيّد عقد API بمخطط قاعدة البيانات. استخدم DTO وقم بالتحويل في طبقة الخدمة.

تعريف فئة DTO

DTO هو POJO عادي. لطلب إنشاء مستخدم يمكن أن يبدو هكذا:

package com.example.api.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; public class UserCreateRequest { @NotBlank(message = "Name is required") @Size(max = 100) private String name; @NotBlank @Email(message = "Must be a valid email address") private String email; @Size(min = 8, message = "Password must be at least 8 characters") private String password; // getters و setters المعيارية (أو استخدم Java record) public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }

لاحظ حاشيات jakarta.validation.constraints.*. لا تفعل شيئًا بمفردها — يجب تفعيلها بـ @Valid في المتحكّم.

التحقق من صحة جسم الطلب بـ @Valid

أضف @Valid (من jakarta.validation) مباشرةً قبل معامل @RequestBody. ستُشغّل Spring Boot Bean Validation على DTO قبل تنفيذ جسم الدالة. إذا فشل أي قيد، تُعيد Spring تلقائيًا 400 Bad Request.

import jakarta.validation.Valid; @PostMapping public ResponseEntity<UserResponse> createUser(@Valid @RequestBody UserCreateRequest request) { UserResponse response = userService.create(request); return ResponseEntity.status(201).body(response); }
@Valid مقابل @Validated: تُشغّل @Valid (Jakarta EE) Bean Validation المعيارية على معامل الدالة. أما @Validated (Spring) فتدعم إضافةً مجموعات التحقق. بالنسبة لمعظم نقاط نهاية REST، @Valid هي الخيار الصحيح — احرص على البساطة.

استخدام Java Records كـ DTOs في Spring Boot 3+

Records في Java غير قابلة للتغيير وموجزة ومثالية للـ DTOs. يُحوّلها Jackson 2.12+ من JSON تلقائيًا دون أي إعداد خاص في Spring Boot 3:

package com.example.api.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; public record UserCreateRequest( @NotBlank @Size(max = 100) String name, @NotBlank @Email String email, @Size(min = 8) String password ) {}

يحلّ Record محل كل الكود المتكرر من getters وconstructor وequals/hashCode بالكامل. استخدم records للـ DTOs الخاصة بالطلبات؛ تفرض عدم القابلية للتغيير فلا يمكن لأي شيء تغيير البيانات الواردة بالصدفة.

الفصل بين DTOs للطلب والاستجابة

النمط الشائع هو استخدام فئات منفصلة للبيانات الواردة والصادرة:

  • DTO الطلب (UserCreateRequest) — الحقول المسموح للعميل بإرسالها. يحمل حاشيات التحقق.
  • DTO الاستجابة (UserResponse) — الحقول المسموح للعميل برؤيتها. يحذف البيانات الحساسة ككلمات المرور والأعلام الداخلية.
public record UserResponse(Long id, String name, String email, String createdAt) { // constructor مريح: التحويل من الكيان public UserResponse(User user) { this(user.getId(), user.getName(), user.getEmail(), user.getCreatedAt().toString()); } }

تُنشئ طبقة الخدمة الكيان من DTO الطلب، تحفظه، وتعيد DTO الاستجابة المبني من الكيان المحفوظ. لا يرى العميل فئة الكيان مطلقًا.

التعامل مع الأجسام المتداخلة والقوائم

يتعامل Jackson مع الكائنات المتداخلة والقوائم تلقائيًا. إذا احتوى JSON الخاص بك على مصفوفة في المستوى الأعلى، اعلن المعامل كـ List<YourDto>:

// يقبل: [{"name":"Alice","email":"a@x.com"}, {"name":"Bob","email":"b@x.com"}] @PostMapping("/batch") public ResponseEntity<List<UserResponse>> createBatch( @Valid @RequestBody List<@Valid UserCreateRequest> requests) { List<UserResponse> responses = requests.stream() .map(userService::create) .toList(); return ResponseEntity.status(201).body(responses); }
التحقق من عناصر القائمة: إضافة @Valid على معامل List وحده لا تُطبّق التحقق على العناصر الداخلية في بعض إعدادات Spring. ضع الحاشية على معامل النوع الجنيسي (List<@Valid UserCreateRequest>) أو تحقق يدويًا في طبقة الخدمة.

الخلاصة

تُخبر @RequestBody Spring بتحويل جسم JSON إلى كائن Java باستخدام Jackson. ارتبط دائمًا بـ DTO مخصص — وليس بكيان JPA — للحماية من هجمات الإسناد الجماعي وفصل سطح API عن مخطط قاعدة البيانات. أضف @Valid لتفعيل Bean Validation قبل تشغيل الدالة. تُعدّ Java Records أكثر صيغة DTO إيجازًا وأمانًا في Spring Boot 3. استخدم DTOs منفصلة للطلب والاستجابة لتتحكم بدقة فيما يستطيع العميل إرساله وما يستلمه. هذا النمط هو الأساس الذي يُبنى عليه كل REST API احترافي في Spring Boot.