دورة حياة السيرفلت
دورة حياة السيرفلت
كل سيرفلت تكتبه يعيش داخل حاوية (container) — سواء كانت Tomcat أو Jetty أو WildFly أو أي محرك متوافق مع Jakarta EE. تملك الحاوية الكائن: هي التي تقرر متى تُنشئ سيرفلتك، ومتى تُدمّره، وكم عدد الخيوط (threads) المسموح لها باستدعائه في آنٍ واحد. إنّ فهم هذا التعاقد بعمق هو ما يُفرّق بين المطوّرين الذين يعالجون مشكلات التزامن في خمس دقائق وأولئك الذين يطاردونها لأيام.
توابع دورة الحياة الثلاثة
تُحدّد مواصفة Jakarta Servlet ثلاثة توابع تُعلّم مراحل وجود السيرفلت. الحاوية هي التي تستدعيها — وأنت تُعيد تعريفها (override).
init(ServletConfig config)— يُستدعى مرة واحدة فور أن تُنشئ الحاوية السيرفلت. استخدمه للإعداد المُكلف الذي يُنفَّذ مرة واحدة فحسب: فتح تجمّع اتصالات قاعدة البيانات، أو تحميل ملف إعداد، أو تهيئة ذاكرة تخزين مؤقت.service(ServletRequest req, ServletResponse res)— يُستدعى لكل طلب، وربما من خيوط متعددة في الوقت ذاته. تُوزّعHttpServletكل استدعاء علىdoGetوdoPostوما شابهها، لذا ستُعيد تعريف تلك التوابع الملائمة بدلًا منserviceمباشرةً في الغالب.destroy()— يُستدعى مرة واحدة حين تُغلق الحاوية أو يُزال السيرفلت. حرّر ما اقتناهinit: أغلق التجمّع، وافرغ مخزن الكتابة، وألغِ الخيوط الخلفية.
init قبل أول استدعاء لـ service. ولا يُستدعى destroy إلا بعد انتهاء جميع استدعاءات service الجارية. الحاوية تفرض ذلك — تحصل على حدود واضحة دون أن تكتب كود تزامن لانتقالات المرحلة بنفسك.
مثال عملي كامل لدورة الحياة
يُوضّح السيرفلت التالي التوابع الثلاثة في سياقها. يفتح تجمّع اتصال وهميًا في init، ويستعلم منه لكل طلب، ويُغلقه في destroy.
لاحظ استدعاء super.init(config). يخزّن HttpServlet.init كائن ServletConfig حتى تعمل getServletConfig() والتابع المساعد getInitParameter() لاحقًا. إن نسيت super.init ستُعيد تلك الاستدعاءات null وستواجه NullPointerException بعيدًا عن السبب الحقيقي.
نموذج المثيل الواحد متعدد الخيوط
تُنشئ الحاوية مثيلًا واحدًا فقط من فئة سيرفلتك (افتراضيًا) وتُوجّه جميع الطلبات المتزامنة عبر ذلك الكائن الواحد. عشرة مستخدمين متزامنين تعني عشرة خيوط تُنفّذ doGet جميعها — على نفس المثيل، تقرأ وتكتب في نفس الحقول.
يُعظّم هذا التصميم الإنتاجية: إنشاء الكائنات مُكلف؛ إنشاء الخيوط أرخص؛ ومشاركة سيرفلت واحد تتجنّب الأمرين معًا. لكنه يُلقي بعبء سلامة الخيوط عليك أنت كليًا.
private int count = 0; مع count++ داخل doGet هو سباق بيانات (data race). يمكن لخيطين أن يقرآ نفس القيمة، وكلاهما يزيدها، ثم يكتبان نفس النتيجة — مع فقدان أحد التزايدين بصمت. استخدم AtomicInteger أو LongAdder أو التزامن المناسب.
ما هو آمن وما ليس آمنًا في السيرفلت
القاعدة الحاسمة بسيطة: مَيّز بين البيانات التي تنتمي للسيرفلت وتلك التي تنتمي لطلب واحد.
- آمن تخزينه كحقول مثيل: الكائنات غير القابلة للتغيير (سلاسل نصية، أغلفة أوليّة، مراجع final تُضبط مرة في
init)، الكائنات الآمنة بين الخيوط (AtomicInteger،ConcurrentHashMap)، الكائنات المساعدة عديمة الحالة (ObjectMapperمن Jackson يُضبط عند بدء التشغيل). - لا تُخزّن أبدًا كحقول مثيل: بيانات خاصة بالطلب مثل مدخلات المستخدم، أو مرجع
HttpServletRequestأوHttpServletResponse، أو مجموعة نتائج، أو أي حساب محلي. صرّح عنها كـمتغيرات محلية داخل التابع — كل خيط يحصل على إطار مكدسه الخاص.
التهيئة الكسولة مقابل الفورية مع load-on-startup
افتراضيًا تُهيّئ الحاوية السيرفلت عند أول طلب يصلها — التهيئة الكسولة. يدفع المُستدعي الأول تكلفة init. للسيرفلت التي تحمل عبئًا ثقيلًا عند البدء (بناء ذاكرة تخزين، تدفئة تجمّع اتصال) يكون هذا التأخير غير مرغوب فيه. تفرض السمة load-on-startup التهيئة الفورية عند وقت النشر:
تتحكّم قيمة العدد الصحيح في ترتيب التهيئة حين تستخدم سيرفلتات متعددة loadOnStartup: القيمة 1 تبدأ قبل القيمة 2 وهكذا. القيم السالبة (أو حذف السمة) تُبقي السلوك الكسول الافتراضي.
init() عديم الوسيطات. توفّر HttpServlet تابع init() بلا وسيطات يستدعيه الحاوية بعد اكتمال init(ServletConfig) الكامل وتخزين config. تجاوزه يعني أنك لست بحاجة لتذكّر super.init(config) — فـconfig مُخزَّن بالفعل حين يعمل كودك.
التابع destroy عمليًا
يُعدّ destroy خطّافك للتنظيف، غير أن له قيودًا. لا تُقدّم الحاوية ضمانًا صارمًا على المدة التي ستنتظرها حتى اكتمال destroy قبل إيقاف JVM أثناء إيقاف التشغيل المفاجئ (SIGKILL أو OOM kill). للعمل الحساس عند الإيقاف — إرسال الرسائل، إتمام المعاملات المالية — استخدم خطّاف إيقاف JVM أو آلية إيقاف تشغيل متحكّم بها على مستوى الإطار كطبقة ثانية من الدفاع.
لاحظ أيضًا: إذا رمى init استثناء ServletException، تُعلّم الحاوية السيرفلت على أنها غير متاحة ولا تستدعي destroy أبدًا. نظّف أي شيء تمّت تهيئته جزئيًا داخل نفس تابع init باستخدام كتلة try/finally.
الخلاصة
تمنحك دورة حياة السيرفلت ثلاثة خطّافات محدودة بوضوح: init (مرة واحدة عند بدء التشغيل)، وservice/doGet/doPost (لكل طلب، على خيوط متعددة)، وdestroy (مرة واحدة عند الإيقاف). يعني نموذج المثيل الواحد أن كل حقل مثيل مشترك بين جميع الطلبات المتزامنة — فقط الكائنات الآمنة بين الخيوط أو البيانات المُهيَّأة عند بدء التشغيل الثابتة تنتمي إليه. بيانات نطاق الطلب يجب أن تعيش في المتغيرات المحلية. فهم هذا النموذج أساسي لكتابة تطبيقات ويب صحيحة وذات إنتاجية عالية في Java.