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

أول Servlet لك

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

أول Servlet لك

الـ Servlet هو فئة Java تعيش داخل حاوية ويب (كـ Tomcat أو Jetty)، تستقبل طلبات HTTP وتكتب استجابات HTTP. أنت تعرف بالفعل كيف تصمم الفئات وتتعامل مع الاستثناءات وتُهيكل برامج Java — الـ Servlet مجرد فئة متخصصة تتوصّل بالبنية التحتية للحاوية. يشرح هذا الدرس بالضبط ما يعنيه ذلك، بدءًا من البنية الدنيا للفئة وصولًا إلى التعليقات التوضيحية (annotations) التي تُسجّل الـ Servlet مع الحاوية.

الفئة الأساسية HttpServlet

كل Servlet يتعامل مع HTTP يمتد من jakarta.servlet.http.HttpServlet. هذه الفئة المجردة هي جزء من مواصفة Jakarta Servlet (التي كانت تُعرف سابقًا بـ javax.servlet) وتوفّر الإطار الذي تُعيد تعريفه بدلًا من بنائه من الصفر. تبدو هيكلية الفئات كما يلي:

java.lang.Object └── jakarta.servlet.GenericServlet (implements Servlet, ServletConfig, Serializable) └── jakarta.servlet.http.HttpServlet └── your.package.HelloServlet // كودك يذهب هنا

تتولى GenericServlet الربط بالحاوية: تخزّن ServletConfig، وتُنفّذ getServletName()، وتوفر تطبيقات افتراضية فارغة لتوابع دورة الحياة. أما HttpServlet فتُضيف الإرسال الخاص بـ HTTP: يفحص تابعها service() طريقة الطلب (GET، POST، PUT، إلخ) ويستدعي التابع doXxx() المناسب في فئتك الفرعية. لا تستدعي service() أبدًا بشكل مباشر — الحاوية هي من تفعل ذلك.

Jakarta مقابل javax: منذ Jakarta EE 9 (2020)، تغيّر بادئة الحزمة من javax.servlet إلى jakarta.servlet. يستخدم Tomcat 10+ وأي خادم Jakarta EE 10/11 فضاء الأسماء jakarta.*. أما خوادم Tomcat 9 / Java EE 8 الأقدم فلا تزال تستخدم javax.*. تحقق دائمًا من الإصدار الذي تستهدفه حاويتك واستورد وفقًا لذلك.

التعليق التوضيحي @WebServlet

قبل وجود التعليقات التوضيحية، كان عليك الإعلان عن كل Servlet في web.xml بـ XML مطوّل. منذ Servlet 3.0، أصبح التعليق التوضيحي @WebServlet على الفئة نفسها هو الأسلوب المُفضَّل — إذ يُبقي ربط URL بجانب الكود الذي يتعامل معه.

package com.example.web; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet(name = "helloServlet", urlPatterns = "/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { response.setContentType("text/html;charset=UTF-8"); try (PrintWriter out = response.getWriter()) { out.println("<!DOCTYPE html>"); out.println("<html><body>"); out.println("<h1>Hello from a Servlet!</h1>"); out.println("</body></html>"); } } }

لنفكّك خصائص التعليق التوضيحي:

  • name — معرّف منطقي تستخدمه الحاوية داخليًا (ويُستخدم في getServletName()). اختياري؛ يُعيَّن افتراضيًا إلى الاسم الكامل للفئة.
  • urlPatterns — نمط URL واحد أو أكثر تتعامل معه هذه الـ Servlet. يُطابق النمط /hello المسار http://host:port/yourApp/hello بالضبط. يمكنك أيضًا استخدام أنماط البادئة (/api/*) أو ربط الامتدادات (*.do).
  • الصيغة المختصرة @WebServlet("/hello") (سلسلة واحدة) تعادل urlPatterns = "/hello" وهي الشكل الأكثر شيوعًا في الممارسة.
جذر السياق مقابل نمط URL: النمط الذي تكتبه في @WebServlet نسبي إلى جذر سياق التطبيق، لا إلى جذر الخادم. إذا نشرت myapp.war، فإن النمط /hello يُحلَّل إلى /myapp/hello. أثناء التطوير مع IDE أو إضافة Maven، غالبًا ما تضبط جذر السياق على / للإبقاء على عناوين URL قصيرة.

تابع doGet بالتفصيل

تستدعي الحاوية doGet عند استقبال طلب HTTP GET المطابق لنمط URL الخاص بالـ Servlet. يمنحك المعاملان كل ما تحتاجه للتعامل مع الطلب:

  • HttpServletRequest request — يُغلّف رسالة HTTP الواردة. يمنحك الوصول إلى معاملات URL والترويسات وجسم الطلب والكوكيز والجلسة والإعداد الإقليمي وعنوان IP للعميل.
  • HttpServletResponse response — يُغلّف رسالة HTTP الصادرة. يتيح لك ضبط رمز الحالة وترويسات الاستجابة وكتابة جسم الاستجابة إما عبر PrintWriter (نص) أو ServletOutputStream (بيانات ثنائية).

يُعلن توقيع التابع عن throws IOException. تُعلن HttpServlet.doGet أيضًا عن throws ServletException — يمكنك إضافتها أيضًا لكن يجب استيراد jakarta.servlet.ServletException. يبدو التعريف الواقعي كما يلي:

@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. قراءة المدخلات String name = request.getParameter("name"); // ?name=Alice من URL if (name == null || name.isBlank()) { name = "World"; } // 2. منطق الأعمال (استدعاء خدمة، الاستعلام من قاعدة بيانات، إلخ) String greeting = "Hello, " + name + "!"; // 3. كتابة المخرجات response.setContentType("text/html;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_OK); // 200 — اختياري؛ هو القيمة الافتراضية try (PrintWriter out = response.getWriter()) { out.printf("<!DOCTYPE html><html><body><p>%s</p></body></html>%n", greeting); } }

لاحظ النمط المؤلف من ثلاث خطوات: قراءة المدخلات → تطبيق المنطق → كتابة المخرجات. إن الحفاظ على هذا الفصل يجعل الـ Servlet سهل الاختبار: منطق الأعمال ينتمي إلى فئات خدمة Java بسيطة، لا داخل الـ Servlet نفسه. المهمة الوحيدة للـ Servlet هي ترجمة HTTP إلى استدعاءات توابع، وترجمة نتائج التوابع مجددًا إلى HTTP.

قراءة كائن الطلب

تعرض واجهة HttpServletRequest طلب HTTP الكامل. أكثر التوابع استخدامًا في معالج doGet:

  • request.getParameter("key") — يُعيد القيمة الأولى لمعامل سلسلة الاستعلام أو النموذج، أو null إذا كان غائبًا.
  • request.getParameterValues("key") — يُعيد جميع قيم معامل متعدد القيم (كمربعات الاختيار) كـ String[].
  • request.getHeader("Accept-Language") — يُعيد ترويسة طلب مُسمّاة.
  • request.getMethod() — يُعيد "GET" أو "POST" إلخ.
  • request.getRequestURI() — الجزء المسار من URL، مثلًا /myapp/hello.
  • request.getAttribute("key") / request.setAttribute("key", value) — تخزين نطاق الطلب المُستخدم عند التوجيه إلى JSP أو Servlet آخر.
لا تثق أبدًا بمدخلات المستخدم. تُعيد getParameter() سلاسل خام من المتصفح. تحقق دائمًا من صحة أي قيمة تُضمّنها في الاستجابة وعقّمها وترمّز HTML لها. إن إهمال ذلك هو السبب الجذري لثغرات البرمجة النصية عبر المواقع (XSS). على الأقل، اهرب من < و> و& قبل كتابة السلاسل التي يوفرها المستخدم في مخرجات HTML.

كتابة كائن الاستجابة

قبل كتابة جسم الاستجابة يجب استدعاء response.setContentType(). يضبط هذا ترويسة HTTP Content-Type حتى يعرف المتصفح كيف يفسّر البايتات. القيم الشائعة:

  • "text/html;charset=UTF-8" — صفحة HTML.
  • "application/json;charset=UTF-8" — استجابة واجهة برمجية JSON.
  • "application/octet-stream" — تنزيل ملف ثنائي.

يجب ضبط نوع المحتوى قبل استدعاء getWriter() أو getOutputStream(). بمجرد بدء كتابة الجسم، تُلتزم الترويسات (تُرسل إلى العميل) ولا يمكن تغييرها بعد ذلك.

للاستجابات النصية استخدم response.getWriter() الذي يُعيد PrintWriter. للاستجابات الثنائية (الصور وملفات PDF وملفات ZIP) استخدم response.getOutputStream() الذي يُعيد ServletOutputStream. يمكنك الحصول على أحدهما فقط لكل طلب — استدعاء الاثنين يرمي IllegalStateException.

مثال كامل قابل للتشغيل

إليك Servlet مكتفية بذاتها تُحيّي زائرًا بالاسم وتُوضح جميع المفاهيم أعلاه بطريقة واقعية:

package com.example.web; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet("/greet") public class GreetServlet extends HttpServlet { // تُنشئ الحاوية هذه الفئة مرة واحدة؛ doGet تُستدعى مع كل طلب. // متغيرات النسخة يجب أن تكون آمنة للخيوط — يُفضَّل استخدام servlets عديمة الحالة. @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String name = request.getParameter("name"); if (name == null || name.isBlank()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing required parameter: name"); return; // sendError تُلزم الاستجابة — توقف هنا } // تعقيم: التطبيق الحقيقي سيستخدم مكتبة مثل OWASP Java Encoder String safeName = name.replace("&", "&amp;") .replace("<", "&lt;") .replace(">", "&gt;"); response.setContentType("text/html;charset=UTF-8"); try (PrintWriter out = response.getWriter()) { out.println("<!DOCTYPE html>"); out.println("<html lang=\"ar\"><head><meta charset=\"UTF-8\">"); out.println("<title>تحية</title></head><body>"); out.printf( "<h1>مرحبًا، %s!</h1>%n", safeName); out.println("</body></html>"); } } }

يُنتج طلب GET إلى /greet?name=Alice صفحة HTML بـ 200 OK تقول "مرحبًا، Alice!". يستقبل طلب بدون المعامل استجابة خطأ 400 Bad Request — يتولّاها المحرّف الافتراضي للأخطاء في الحاوية.

ما تفعله الحاوية نيابةً عنك

من المفيد أن نكون واضحين بشأن العمل الذي تؤدّيه الحاوية (Tomcat أو Jetty أو WildFly إلخ) تلقائيًا:

  • تُحلّل بايتات TCP الخام إلى كائن HttpServletRequest.
  • تُنشئ فئة الـ Servlet الخاصة بك مرة واحدة وتستدعي init().
  • تُخصص خيطًا من تجمّعها لكل طلب وارد وتستدعي doGet الخاصة بك على ذلك الخيط.
  • تبني ترويسات استجابة HTTP مما ضبطته على HttpServletResponse وتُسلسل الجسم إلى المقبس.
  • تستدعي destroy() عند إلغاء نشر التطبيق.

هذا تقسيم العمل — الحاوية تتعامل مع سباكة البروتوكول، وكودك يتعامل مع منطق التطبيق — هو العقد الأساسي لمواصفة Servlet وأساس كل إطار عمل ويب Java مبني فوقها.

الخلاصة

الـ Servlet هي فئة تمتد من HttpServlet وتُعيد تعريف توابع doXxx المطابقة لأفعال HTTP التي تتعامل معها. يُسجّلها التعليق التوضيحي @WebServlet مع الحاوية ويُعلن نمط URL. يعرض المعامل HttpServletRequest كل تفاصيل الطلب الوارد؛ بينما يُتيح لك المعامل HttpServletResponse التحكم في كل تفاصيل الرد. أبقِ كود الـ Servlet ضئيلًا — اقرأ، وفوّض، وردّ — وضع المنطق الحقيقي في فئات Java بسيطة. في الدرس القادم ستطّلع على دورة الحياة الكاملة: كيف تُنشئ الحاوية الـ Servlet وتُهيّئها وتدمّرها في نهاية المطاف.