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

الردود وResponseEntity

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

الردود وResponseEntity

في الدروس السابقة تعلّمت كيف ترسم خرائط الطلبات الواردة وتربط بياناتها. الآن ينصبّ التركيز على الجانب الصادر: كيف تُترجم Spring Boot قيمة الإرجاع إلى استجابة HTTP، وكيف يمنحك ResponseEntity تحكمًا دقيقًا في كل جزء من تلك الاستجابة — رمز الحالة والترويسات والجسم.

الافتراضي: إرجاع كائن POJO ودع Spring يتولى الأمر

حين تُرجع إحدى توابع @RestController كائن Java عاديًا، تقوم بنية HttpMessageConverter في Spring تلقائيًا بتسلسله إلى JSON (أو XML). تكون حالة HTTP الافتراضية 200 OK وتُضبط ترويسة Content-Type على application/json.

@GetMapping("/products/{id}") public Product getProduct(@PathVariable Long id) { return productService.findById(id); // 200 OK، الجسم = JSON }

هذا مناسب تمامًا للمسار السعيد، لكن ماذا حين لا يُعثر على المورد؟ أو حين تنشئ شيئًا وتحتاج إلى إرجاع 201 Created مع ترويسة Location؟ لتلك الحالات تحتاج إلى ResponseEntity.

ResponseEntity — تحكم كامل في الاستجابة

ResponseEntity<T> غلاف عام يجمع جسمًا من النوع T وحالة HttpStatus ومجموعة HttpHeaders في قيمة إرجاع واحدة. وهو جزء من org.springframework.http ويعمل في أي متحكم Spring MVC أو Spring WebFlux.

import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/products") public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } @GetMapping("/{id}") public ResponseEntity<Product> getProduct(@PathVariable Long id) { return productService.findById(id) .map(ResponseEntity::ok) // 200 OK مع جسم .orElse(ResponseEntity.notFound() // 404 Not Found بلا جسم .build()); } }
لماذا تُرجع ResponseEntity<T> بدلًا من T مباشرةً؟ لأن HTTP أكثر من مجرد ناقل لـ JSON. رموز الحالة تُبلّغ العملاء والوسيط وأنظمة المراقبة بالنية. إرجاع كائن برمز 200 ثابت حين لم يُعثر على المورد يُجبر العميل على فحص الجسم لاكتشاف الفشل — وهذا عقد مُسرِّب.

واجهة برمجة البناء (Builder API)

بناء ResponseEntity عبر مُنشئاته (constructors) مباشرةً مطوّل. واجهة البناء الطليقة (المتاحة منذ Spring 4) أكثر وضوحًا:

// 200 OK مع جسم ResponseEntity.ok(product); // 200 OK بلا جسم ResponseEntity.ok().build(); // 201 Created مع ترويسة Location وجسم ResponseEntity .created(URI.create("/api/v1/products/" + saved.getId())) .body(saved); // 204 No Content (نمطي لـ DELETE) ResponseEntity.noContent().build(); // 404 Not Found بلا جسم ResponseEntity.notFound().build(); // أي حالة + ترويسات مخصصة + جسم ResponseEntity .status(HttpStatus.ACCEPTED) .header("X-Request-Id", requestId) .contentType(MediaType.APPLICATION_JSON) .body(dto);

ضبط الترويسات المخصصة

تحمل الترويسات بيانات وصفية ليست جزءًا من الجسم: مؤشرات ترقيم الصفحات، ومعلومات حدود المعدل، وتوجيهات التخزين المؤقت، وغيرها. تضيفها عبر HttpHeaders أو مباشرةً على البنّاء:

import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import java.net.URI; @PostMapping public ResponseEntity<ProductDto> createProduct(@RequestBody CreateProductRequest req) { ProductDto saved = productService.create(req); HttpHeaders headers = new HttpHeaders(); headers.add("X-Created-By", "product-service"); return ResponseEntity .created(URI.create("/api/v1/products/" + saved.getId())) .headers(headers) .body(saved); }

ترويسة Location التي يضبطها .created(uri) تُخبر العميل بالضبط أين يجد المورد المُنشأ حديثًا. تعتمد عليها عملاء REST وأطر hypermedia وبوابات API.

التحكم في رمز الحالة صراحةً

استخدم ResponseEntity.status(HttpStatus.XXX) حين لا تُغطّي الأساليب المُختصرة الرمز الصحيح. مثلًا، إرجاع 202 Accepted لمهمة غير متزامنة:

@PostMapping("/reports") public ResponseEntity<Map<String, String>> scheduleReport(@RequestBody ReportRequest req) { String jobId = reportService.enqueue(req); return ResponseEntity .status(HttpStatus.ACCEPTED) .body(Map.of("jobId", jobId, "status", "queued")); }

إرجاع جسم فارغ

ينبغي لعدة عمليات HTTP أن تُرجع جسمًا فارغًا: الحذف الناجح بـ DELETE، والتحديث الناجح بـ PUT حين لا تُعيد المورد المحدّث، وإشعارات الاستلام. استخدم ResponseEntity<Void> للتعبير عن ذلك بوضوح على مستوى النوع:

@DeleteMapping("/{id}") public ResponseEntity<Void> deleteProduct(@PathVariable Long id) { productService.delete(id); return ResponseEntity.noContent().build(); // 204 No Content }
فضّل ResponseEntity<Void> على ResponseEntity<?> للاستجابات ذات الجسم الفارغ. يجعل ذلك العقد واضحًا في توقيع التابع: يعرف المُستدعون وأدوات توليد الكود أنه لا يوجد جسم عن قصد، بدلًا من التساؤل عمّا إذا كانت علامة الاستفهام تُخفي شيئًا.

الاستجابات الشرطية باستخدام ETags وIf-None-Match

للنقاط الطرفية كثيفة القراءة يمكنك دعم الطلبات الشرطية لـ HTTP لتقليل النطاق الترددي. توفر Spring مرشّح ShallowEtagHeaderFilter لتوليد ETag تلقائيًا، لكن يمكنك أيضًا ضبط ترويسات ETag يدويًا عبر ResponseEntity حين تحتاج إلى تحكم دقيق:

@GetMapping("/{id}") public ResponseEntity<Product> getProductWithEtag( @PathVariable Long id, @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) { Product product = productService.findByIdOrThrow(id); String etag = "\"" + product.getVersion() + "\""; if (etag.equals(ifNoneMatch)) { return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build(); } return ResponseEntity.ok() .eTag(etag) .body(product); }

ResponseEntity المكتوب مقابل ذي البدل

ستشاهد أحيانًا توابع تُصرّح بـ ResponseEntity<?> أو ResponseEntity<Object>. هذا في معظم الحالات رائحة كود سيئة. فهو يتخلى عن سلامة النوع وقت الترجمة، ويُربك أدوات بيئة التطوير، ويعطّل توليد مخطط OpenAPI. الاستخدام المشروع الوحيد هو حين تُرجع نقطة طرفية واحدة أنواع جسم مختلفة حقًا بحسب النتيجة — وحتى في هذه الحالة يكون معالج الاستثناءات المناسب (@ControllerAdvice) التصميم الأفضل عادةً.

تجنّب ResponseEntity<?> كاختصار كسول. حين تجد نفسك تريد إرجاع إما ProductDto أو سلسلة خطأ من التابع نفسه، فهذه إشارة إلى أن حالة الخطأ ينبغي أن تكون استثناءً مُرمَيًا من الخدمة ويُترجمه صف @ControllerAdvice — لا مسار كود ثانٍ داخل التابع.

النمط العملي: يُرجع الخدمة Optional

نمط نظيف وإيديوماتيكي لنقاط طرفية GET هو أن يُرجع الخدمة Optional وتُعيّنه إلى ResponseEntity المناسبة في المتحكم:

@GetMapping("/{id}") public ResponseEntity<ProductDto> getProduct(@PathVariable Long id) { return productService.findById(id) // Optional<ProductDto> .map(ResponseEntity::ok) // موجود → 200 OK .orElseGet(() -> // غائب → 404 ResponseEntity.notFound().build()); }

يُبقي هذا النمط المتحكم نحيفًا، ومعالجة القيم الفارغة صريحة، ويتجنّب فرعًا شرطيًا يسهل نسيانه.

الخلاصة

تُسلسل Spring Boot قيم الإرجاع العادية إلى JSON برمز حالة افتراضي 200. يرفع ResponseEntity<T> هذا الافتراضي، مانحًا إياك تحكمًا من الدرجة الأولى في رمز الحالة والترويسات والجسم في كائن واحد آمن النوع. استخدم واجهة البناء الطليقة — ResponseEntity.ok() و.created(uri) و.noContent() و.status(HttpStatus.X) — لكود واضح يُعبّر عن النية. اكتب نوع المعامل في ResponseEntity بنوع محدد؛ والجأ إلى <Void> حين يكون الجسم فارغًا عن قصد. في الدرس القادم ستعمّق فهمك لرموز حالة HTTP نفسها ودلالات REST التي تحملها.