أساسيات جافا للويب والـ Servlets

بناء الاستجابات

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

بناء الاستجابات

مهمة السيرفليت هي استقبال طلب HTTP وإنشاء استجابة HTTP مُشكَّلة بشكل صحيح. ركّز الدرس السابق على تشريح الطلب؛ أما هذا الدرس فيركّز على الجانب الآخر من التبادل: كتابة جسم الاستجابة، وتعيين نوع المحتوى الصحيح، والتحكم في رموز الحالة، وإضافة رؤوس الاستجابة. الحصول على هذه الأمور الأربعة صحيحةً هو الفارق بين سيرفليت يعمل بشكل موثوق عبر المتصفحات والبروكسيات وعملاء API، وبين سيرفليت هشّ في بيئة الإنتاج.

كائن HttpServletResponse

يُسلّم الحاوي كل تابع خدمة كائن jakarta.servlet.http.HttpServletResponse. من خلاله تتحكم في كل جانب من جوانب استجابة HTTP. أهم قرارين يجب اتخاذهما قبل كتابة بايت واحد من الجسم هما: نوع المحتوى (الذي يُعيّن أيضًا ترميز المحارف) ورمز الحالة. كلاهما يجب أن يُثبَّت قبل الجسم وإلا سيُسقَط بصمت أو يُسبّب IllegalStateException.

تعيين نوع المحتوى

استدع دائمًا response.setContentType() قبل الحصول على كاتب أو دفق. يُخبر نوع المحتوى المتصفح (أو عميل API) بكيفية تفسير البايتات التي ستُرسلها.

@WebServlet("/api/status") public class StatusServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // يجب الاستدعاء قبل getWriter() أو getOutputStream() resp.setContentType("application/json"); resp.setCharacterEncoding("UTF-8"); PrintWriter out = resp.getWriter(); out.print("{\"status\":\"ok\",\"version\":\"1.0\"}"); } }

أشيع قيم نوع المحتوى التي ستستخدمها عمليًا:

  • text/html; charset=UTF-8 — استجابات HTML القياسية
  • application/json — استجابات REST API (يكون charset افتراضيًا UTF-8 وفق RFC 8259)
  • text/plain; charset=UTF-8 — نص عادي، مفيد للتشخيص
  • application/xml — حمولات XML
  • application/octet-stream — تنزيلات الملفات الثنائية (مع Content-Disposition)
عيّن نوع المحتوى وترميز المحارف معًا صراحةً. setContentType("text/html; charset=UTF-8") اختصار لاستدعاء setContentType("text/html") وsetCharacterEncoding("UTF-8") معًا. إن أغفلت charset، قد تُعيّن الحاوية ISO-8859-1 افتراضيًا، مما يُفسد أي محارف غير ASCII تكتبها بصمت.

كتابة جسم الاستجابة

لديك خياران مُتبادَلان لكتابة الجسم: PrintWriter القائم على المحارف (للنصوص) أو ServletOutputStream القائم على البايتات (للثنائيات). محاولة الحصول على كليهما من الاستجابة ذاتها تُلقي IllegalStateException — تفرض الحاوية هذا بصرامة.

// استجابات النص — استخدم PrintWriter resp.setContentType("text/html; charset=UTF-8"); PrintWriter writer = resp.getWriter(); writer.println("<html><body><h1>Hello</h1></body></html>"); // لا تستدع writer.close() — دع الحاوية تُفرغ المخزن وتُغلقه // استجابات الثنائيات — استخدم ServletOutputStream resp.setContentType("image/png"); ServletOutputStream out = resp.getOutputStream(); Files.copy(Path.of("/var/app/images/logo.png"), out); // نفس القاعدة: لا تُغلق الدفق بنفسك
لا تستدع close() على الكاتب أو الدفق. إغلاق دفق الاستجابة داخل سيرفليتك قد يمنع الحاوية من إلحاق أي بيانات معلّقة (مثل رؤوس ملفات تعريف الارتباط للجلسة المُعيَّنة بعد إعادة الكود) ويتداخل مع إدارة اتصالات Keep-Alive. دع الحاوية تدير دورة حياة الدفق.

تعيين رمز حالة HTTP

الحالة الافتراضية هي 200 OK. لأي حالة أخرى، استدع response.setStatus(int) قبل كتابة الجسم. استخدم الثوابت المُسمَّاة في HttpServletResponse بدلًا من الأرقام الخام — فهي توثّق النية وتُقلل الأخطاء المطبعية.

@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String id = req.getParameter("id"); Product product = catalog.findById(id); if (product == null) { // 404 — المورد غير موجود resp.setStatus(HttpServletResponse.SC_NOT_FOUND); resp.setContentType("application/json"); resp.getWriter().print("{\"error\":\"Product not found\"}"); return; } resp.setStatus(HttpServletResponse.SC_OK); // 200، وهو الافتراضي أيضًا resp.setContentType("application/json"); resp.getWriter().print(product.toJson()); }

الثوابت الأكثر استخدامًا — مع مقابلاتها الرقمية للمرجعية:

  • SC_OK (200) — استجابة ناجحة مع جسم
  • SC_CREATED (201) — أنشأ طلب POST موردًا؛ اقرنه برأس Location
  • SC_NO_CONTENT (204) — نجاح بلا جسم (مثل DELETE)
  • SC_MOVED_PERMANENTLY (301) — إعادة توجيه دائمة؛ استخدم sendRedirect للحالة 302
  • SC_BAD_REQUEST (400) — أرسل العميل بيانات مشوّهة
  • SC_UNAUTHORIZED (401) — مطلوب مصادقة
  • SC_FORBIDDEN (403) — تمت المصادقة لكن لا توجد صلاحية
  • SC_NOT_FOUND (404) — المورد غير موجود
  • SC_INTERNAL_SERVER_ERROR (500) — خطأ غير معالَج في الخادم
sendError مقابل setStatus: يُعيّن resp.sendError(404, "Not found") رمز الحالة ويُشغّل آلية صفحة الخطأ للحاوية (أي تعيينات error-page في web.xml أو @WebServlet). أما setStatus فيضبط الرمز فحسب ويتيح لك كتابة جسمك. بالنسبة لواجهات API، فضّل setStatus مع جسم خطأ JSON. بالنسبة لتطبيقات HTML، فضّل sendError ليُقدّم الحاوي صفحات الخطأ المُهيَّئة.

إضافة رؤوس الاستجابة

إلى جانب سطر الحالة ونوع المحتوى، تحمل استجابات HTTP رؤوسًا تتحكم في التخزين المؤقت والأمان وإعادة التوجيه وغير ذلك. استخدم response.setHeader(name, value) لقيمة واحدة أو addHeader(name, value) لإلحاق قيم إضافية لرأس يسمح بالتعدد.

// Cache-Control: منع تخزين الاستجابات الحساسة مؤقتًا resp.setHeader("Cache-Control", "no-store"); // رأس Location — إلزامي عند إعادة الحالة 201 Created resp.setStatus(HttpServletResponse.SC_CREATED); resp.setHeader("Location", "/api/products/" + newProduct.getId()); // Content-Disposition — تشغيل تنزيل ملف في المتصفح resp.setContentType("application/pdf"); resp.setHeader("Content-Disposition", "attachment; filename=\"report.pdf\""); // رأس CORS — السماح لمصدر معين باستدعاء هذه النقطة resp.setHeader("Access-Control-Allow-Origin", "https://app.example.com"); // رأس مخصص — مثل إصدار API resp.setHeader("X-API-Version", "2");

النمط العملي: بناء استجابة JSON لواجهة API

بجمع كل شيء معًا، إليك تابع مساعد واقعي تستخرجه كثير من الفرق في فئة سيرفليت أساسية:

protected void writeJson(HttpServletResponse resp, int status, String json) throws IOException { resp.setStatus(status); resp.setContentType("application/json"); resp.setCharacterEncoding("UTF-8"); resp.setHeader("Cache-Control", "no-store"); resp.getWriter().print(json); } // الاستخدام في تابع المعالجة: writeJson(resp, HttpServletResponse.SC_OK, "{\"message\":\"Order placed\",\"orderId\":42}");

يُقلّل تمركُز هذه الأسطر الأربعة خطر نسيان ترميز المحارف أو رأس Cache-Control في أي نقطة نهاية.

التخزين المؤقت وتثبيت الاستجابة

تُخزّن الحاوية مخرجات الاستجابة مؤقتًا قبل إرسالها. طالما لم يُفرَّغ المخزن، يمكنك تعديل الرؤوس والحالة. بمجرد أن يُفرَّغ المخزن — سواء امتلأ، أو استدعيت flushBuffer()، أو ثُبّتت الاستجابة — تُقفَل الرؤوس. أي استدعاء لاحق لـ setStatus أو setHeader سيُتجاهل بصمت.

يمكنك الاستعلام عن حجم المخزن وتعديله باستخدام resp.getBufferSize() وresp.setBufferSize(int). زيادته قليلًا (مثلًا إلى 16 كيلوبايت) مفيدة حين تحتاج إلى تحديد الحالة النهائية بعد بعض العمل قبل التثبيت.

أبقِ منطق الحالة والرؤوس في مقدمة تابعك. أأمن عادة هي حساب جميع رموز الحالة والرؤوس أولًا، ثم الحصول على الكاتب وكتابة الجسم. بهذه الطريقة لن تجد نفسك في موقف قد ثُبّت فيه الرد حين تكتشف أنك بحاجة لإرسال رمز 404.

الخلاصة

بناء استجابة HTTP صحيحة يتطلب أربعة أشياء تعمل معًا: نوع المحتوى الصحيح (يُعيَّن قبل الكتابة)، وترميز المحارف الصحيح (UTF-8 ما لم يكن لديك سبب محدد خلاف ذلك)، ورمز الحالة الصحيح (استخدم ثوابت SC_*)، وأي رؤوس يحتاجها العميل لتفسير الاستجابة أو تخزينها مؤقتًا. عيّن دائمًا الحالة والرؤوس قبل كتابة الجسم، ولا تُغلق الكاتب بنفسك أبدًا، وفضّل المساعدات المُمركَزة على تشتيت هذه الأسطر الأربعة عبر كل معالج. يجمع الدرس التالي كل شيء من الدرسين الخامس والسادس لمعالجة نماذج HTML بكل من GET وPOST.