الجلسات والكوكيز والمرشّحات

مقدمة إلى الفلاتر (Filters)

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

مقدمة إلى الفلاتر (Filters)

فلتر السيرفلت (servlet filter) هو مكوّن قابل لإعادة الاستخدام يعترض كل طلب HTTP قبل وصوله إلى السيرفلت المستهدف (أو JSP) وكل استجابة HTTP قبل مغادرتها الخادم. تُشكّل الفلاتر سلسلة — تُعرف بـسلسلة الفلاتر (filter chain) — ويقرر كل فلتر باستقلالية تامة ما إذا كان سيمرّر الطلب، أو يعدّله، أو يوقفه كليًّا. تفصل هذه البنية بشكل نظيف الاهتمامات المشتركة (كالتسجيل، والتحقق من المصادقة، والضغط، ورؤوس CORS) عن السيرفلتات التجارية الخاصة بك.

عقد واجهة Filter

كل فلتر ينفّذ الواجهة jakarta.servlet.Filter التي تُعلن عن ثلاثة توابع:

  • init(FilterConfig config) — يُستدعى مرة واحدة عندما ينشئ الحاوي الفلتر. استخدمه لقراءة معاملات التهيئة أو اقتناء الموارد.
  • doFilter(ServletRequest request, ServletResponse response, FilterChain chain) — يُستدعى في كل طلب مطابق. هنا تعيش منطق عملك.
  • destroy() — يُستدعى مرة واحدة قبل إزالة الحاوي للفلتر. حرّر أي موارد اقتنيتها في init.
التوابع الافتراضية منذ Servlet 6 (Jakarta EE 10): تمتلك كل من init() وdestroy() تنفيذات افتراضية فارغة في الواجهة، لذا تتجاوزهما فقط عندما يكون لديك ما تفعله. يبقى doFilter مجردًا ويجب دائمًا تنفيذه.

سلسلة الفلاتر

يبني الحاوي سلسلة مرتّبة من جميع الفلاتر التي يتطابق نمط URL الخاص بها مع الطلب الحالي. عندما تستدعي chain.doFilter(request, response) داخل فلترك، ينتقل التحكم إلى الفلتر التالي في السلسلة (أو إلى السيرفلت المستهدف إذا لم تتبقَّ فلاتر). يمكنك تشغيل منطق قبل هذا الاستدعاء (معالجة مسبقة) ومنطق بعد عودته (معالجة لاحقة). إذا لم تستدعِ chain.doFilter أبدًا، فإنك تقطع السلسلة — ولن يُستدعى السيرفلت المستهدف قط. هذه هي الآلية التي تستخدمها فلاتر المصادقة لحجب الطلبات غير الموثّقة.

package com.example.filters; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/*") // يطابق كل URL في التطبيق public class TimingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long start = System.currentTimeMillis(); // --- المعالجة المسبقة --- chain.doFilter(request, response); // تمرير التحكم للفلتر التالي / السيرفلت long elapsed = System.currentTimeMillis() - start; // --- المعالجة اللاحقة --- System.out.printf("[TimingFilter] %s ms%n", elapsed); } }

لاحظ البنية: كل شيء قبل chain.doFilter ينفَّذ عند الدخول، وكل شيء بعده ينفَّذ عند الخروج. ينفَّذ السيرفلت المستهدف داخل تلك الفجوة.

تسجيل الفلتر باستخدام @WebFilter

التعليق التوضيحي @WebFilter (متاح منذ Servlet 3.0) هو الطريقة الحديثة والخالية من XML لتعريف الفلتر. أبرز خصائصه:

  • value / urlPatterns — أنماط URL التي يُطبَّق عليها هذا الفلتر. تدعم أحرف البدل: "/api/*"، "*.json".
  • servletNames — استهداف أسماء سيرفلت محددة بدلًا من أنماط URL.
  • filterName — تجاوز الاسم الافتراضي (اسم الفئة).
  • initParams — تمرير أزواج مفتاح-قيمة @WebInitParam القابلة للقراءة في init(FilterConfig).
  • dispatcherTypes — التحكم في ما إذا كان الفلتر يُشغَّل عند الإرسال REQUEST أو FORWARD أو INCLUDE أو ERROR أو ASYNC. الافتراضي هو REQUEST فقط.
@WebFilter( urlPatterns = { "/admin/*", "/api/*" }, filterName = "AuthFilter", initParams = { @WebInitParam(name = "loginPage", value = "/login") } ) public class AuthFilter implements Filter { private String loginPage; @Override public void init(FilterConfig cfg) { loginPage = cfg.getInitParameter("loginPage"); // "/login" } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { var httpReq = (jakarta.servlet.http.HttpServletRequest) req; var httpRes = (jakarta.servlet.http.HttpServletResponse) res; if (httpReq.getSession(false) != null && httpReq.getSession().getAttribute("user") != null) { chain.doFilter(req, res); // موثَّق — استمرار } else { httpRes.sendRedirect(httpReq.getContextPath() + loginPage); // حجب وإعادة توجيه } } }
حوّل الكائنات إلى HttpServletRequest / HttpServletResponse عندما تحتاج إلى واجهات برمجية خاصة بـ HTTP. يستخدم توقيع doFilter أنواع ServletRequest/ServletResponse الأساسية (التي تغطي بروتوكولات غير HTTP أيضًا)، لكن في تطبيق الويب تكون الكائنات دائمًا متغيرات HTTP. حوّل مبكرًا وخصّص لمتغير محلي بدلًا من التحويل مرارًا.

ترتيب الفلاتر

عندما تتطابق فلاتر متعددة مع نفس URL، على الحاوي اختيار ترتيب. القاعدة هي:

  • الفلاتر المسجَّلة بالتعليقات التوضيحية (@WebFilter): لا تضمن مواصفة Servlet أي ترتيب بينها. الترتيب يعتمد على التنفيذ.
  • الفلاتر المسجَّلة في web.xml: تنفَّذ بالترتيب الذي تظهر فيه عناصر <filter-mapping> في الملف.
  • مختلطة: تُنفَّذ الفلاتر المُعلَنة في XML أولًا، ثم الفلاتر المُعلَنة بالتعليقات التوضيحية بترتيب غير محدد.

إذا كان الترتيب مهمًا — وهو كذلك بالنسبة لفلاتر الأمان دائمًا تقريبًا — فأعلن فلاترك في web.xml أو استخدم نهج ServletContextListener عبر ServletContext.addFilter().

<!-- web.xml — ترتيب مضمون --> <filter> <filter-name>TimingFilter</filter-name> <filter-class>com.example.filters.TimingFilter</filter-class> </filter> <filter-mapping> <filter-name>TimingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.example.filters.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping>

في هذا الإعداد يلتف TimingFilter حول AuthFilter: يدخل الطلب TimingFilter، ثم (إذا تطابق URL) AuthFilter، ثم السيرفلت المستهدف. يُفكّ الاستجابة بترتيب عكسي — السيرفلت، ثم معالجة AuthFilter اللاحقة، ثم معالجة TimingFilter اللاحقة.

تغليف الطلب أو الاستجابة

لا تستطيع الفلاتر الفحص فحسب، بل يمكنها أيضًا استبدال كائنات الطلب أو الاستجابة بتغليفها. توفّر الواجهة البرمجية HttpServletRequestWrapper وHttpServletResponseWrapper لهذا الغرض. حالة الاستخدام الشائعة هي التقاط جسم الاستجابة (للتسجيل أو الضغط) بإحلال غلاف يخزّن الإخراج مؤقتًا قبل إرساله.

// غلاف بسيط يفرض ترميزًا محددًا على كل طلب public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); // تمرير الكائنات الأصلية دون تغيير } }
لا تنسَ أبدًا استدعاء chain.doFilter(). إذا كان فلترك يتخطاها شرطيًا (مثلًا عند فشل المصادقة)، تأكد أن الفرع الآخر لا يزال يكتب استجابة كاملة (إعادة توجيه، أو حالة خطأ، أو جسم). إرجاع doFilter دون استدعاء السلسلة ودون كتابة استجابة يترك العميل معلّقًا مع 200 فارغ.

متى تستخدم Filters مقابل Interceptors

الفلاتر هي ميزة Servlet API وتعمل على طبقة HTTP — ترى الطلبات والاستجابات الخام. يوفّر Spring MVC كائن HandlerInterceptor الخاص به يعمل على طبقة المُرسِّل، بعد أن يحلّ Spring تابع المعالج. اختر الفلاتر للاهتمامات التي هي حقًا على مستوى النقل (رؤوس الأمان، الترميز، التسجيل) والمعترضات للاهتمامات المرتبطة بدورة حياة طلبات Spring. كلا الآليتين تكمّلان بعضهما وتُستخدمان معًا في الإنتاج.

الخلاصة

تُعدّ واجهة Filter وسلسلة الفلاتر إجابة Servlet API على الاهتمامات المشتركة. نفّذ doFilter، واستدعِ chain.doFilter لتمرير التحكم للأمام، وضع المنطق قبل هذا الاستدعاء أو بعده للمعالجة المسبقة/اللاحقة. استخدم @WebFilter للحالات البسيطة وweb.xml (أو التسجيل البرمجي) عندما يكون الترتيب حاسمًا. يبني الدرس التالي على هذا الأساس بتنفيذات فلاتر عملية وجاهزة للإنتاج.