توجيه الطلبات: الإعادة التوجيهية والتضمين
نادرًا ما ينتمي طلب HTTP واحد إلى سيرفلت بمفرده. تُوزّع التطبيقات الحقيقية العمل على مكوّنات متعددة: سيرفلت يتحقق من المدخلات ويُعدّ البيانات، وصفحة JSP تُصيّر HTML، وجزء للرأس يُعاد استخدامه في كل صفحة. RequestDispatcher هو الآلية التي تربط هذه المكوّنات معًا من جانب الخادم بشكل غير مرئي للمتصفح. يتناول هذا الدرس العمليتين الأساسيتين — forward وinclude — وقناة سمات الطلب التي تنقل البيانات بينهما.
ما هو RequestDispatcher؟
RequestDispatcher كائن تفويض من جانب الخادم. تحصل عليه بسؤال HttpServletRequest أو ServletContext عن مسار المورد الهدف، ثم إما تُعيد توجيه الطلب بالكامل إلى ذلك المورد أو تُضمّن ناتجه داخل استجابتك الخاصة. يرى المتصفح استجابة HTTP واحدة برمز حالة واحد، دون أدنى علم بعدد المكوّنات التي أنتجتها.
ثمة طريقتان للحصول على الموزّع:
// 1. من الطلب — المسار نسبي إلى جذر سياق السيرفلت الحالي
RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/product.jsp");
// 2. من ServletContext — نفس قواعد المسار، لكنه مفيد من فئات المساعدة
// التي لا تمتلك مرجعًا للطلب
RequestDispatcher rd2 = getServletContext().getRequestDispatcher("/WEB-INF/views/product.jsp");
يُفضَّل استخدام request.getRequestDispatcher(). تقبل الطريقتان مسارًا نسبيًا لسياق التطبيق (يبدأ بـ /). تدعم النسخة على مستوى الطلب أيضًا مسارات نسبية للسيرفلت الحالي — نادرة الاستخدام، لكن جيّد معرفتها.
forward() — تسليم الطلب بالكامل
تنقل forward(request, response) السيطرة الكاملة على الاستجابة إلى المورد الهدف. بعد عودة الاستدعاء، يجب ألّا يكتب السيرفلت المُستدعي أي شيء آخر في الاستجابة. إن فعل ذلك، سيرمي الحاوي إما IllegalStateException أو يتجاهل الإخراج الإضافي صامتًا، تبعًا لما إذا كانت الاستجابة قد التزمت بالفعل.
import jakarta.servlet.RequestDispatcher;
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.util.List;
@WebServlet("/products")
public class ProductListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. المنطق التجاري — جلب البيانات
List<Product> products = ProductRepository.findAll();
// 2. تمرير البيانات إلى العرض عبر سمات الطلب
request.setAttribute("products", products);
request.setAttribute("pageTitle", "Our Products");
// 3. التوجيه — تكتب JSP الاستجابة الكاملة
RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/views/product-list.jsp");
rd.forward(request, response);
// لا تكتب في الاستجابة بعد هذا السطر — JSP تملكها الآن
}
}
تستقبل JSP في /WEB-INF/views/product-list.jsp نفس كائني request وresponse، فتقرأ السمات التي وضعها السيرفلت:
<%-- product-list.jsp --%>
<html>
<body>
<h1>${pageTitle}</h1>
<ul>
<c:forEach var="p" items="${products}">
<li>${p.name} — $${p.price}</li>
</c:forEach>
</ul>
</body>
</html>
لا تُعِد التوجيه بعد التزام الاستجابة. تُعتبر الاستجابة ملتزمة متى كُتب أي ناتج فيها أو استُدعيت response.flushBuffer(). استدعاء forward على استجابة ملتزمة يرمي IllegalStateException. أعدّ جميع السمات أولًا، ثم أعِد التوجيه — واجعله آخر جملة في المعالج.
include() — تركيب الصفحة من أجزاء
تُفوّض include(request, response) إلى المورد الهدف مؤقتًا. يكتب الهدف ناتجه في مخزن الاستجابة نفسه، ثم يعود التحكم إلى السيرفلت المُستدعي الذي يمكنه الاستمرار في الكتابة. هذا مثالي للأجزاء القابلة لإعادة الاستخدام — الرأس والتذييل وشريط التنقل — التي يُضمّنها أكثر من سيرفلت في ناتجه الخاص.
@WebServlet("/dashboard")
public class DashboardServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
var out = response.getWriter();
// تضمين جزء الرأس المشترك
request.getRequestDispatcher("/WEB-INF/fragments/header.jsp")
.include(request, response);
// يكتب هذا السيرفلت قسمه الخاص
out.println("<main>");
out.println(" <h2>لوحة التحكم</h2>");
out.println(" <p>مرحبًا بعودتك!</p>");
out.println("</main>");
// تضمين جزء التذييل المشترك
request.getRequestDispatcher("/WEB-INF/fragments/footer.jsp")
.include(request, response);
}
}
الموارد المُضمَّنة لا تستطيع تغيير ترويسات الاستجابة أو حالتها. تتجاهل الحاوي صامتًا أي استدعاءات لـ setContentType() أو setStatus() أو sendRedirect() تُجرى داخل مورد مُضمَّن. السيرفلت ذو المستوى الأعلى وحده يتحكم في تلك. استخدم include فقط لأجزاء محتوى الجسم.
تمرير البيانات: سمات الطلب
سمات الطلب هي الناقل المعياري بين السيرفلت والمورد الذي يُفوّض إليه. تعيش طوال مدة طلب واحد وتُخزَّن كخريطة مفتاح-قيمة على كائن HttpServletRequest.
// تعيين السمات — أي كائن قابل للتسلسل يعمل
request.setAttribute("user", currentUser); // POJO
request.setAttribute("errors", validationErrors); // List<String>
request.setAttribute("totalItems", cartItems.size()); // int مُعبَّأ
// قراءتها في الهدف (سيرفلت أو JSP)
User user = (User) request.getAttribute("user");
// إزالتها حين لا تكون هناك حاجة إليها (نظافة جيدة في سلاسل التضمين)
request.removeAttribute("temporaryFlag");
في JSP مع EL (لغة التعبير) وJSTL تقرأ السمات دون تحويل صريح للنوع:
<p>مرحبًا، ${user.firstName}!</p>
<p>عدد العناصر في السلة: ${totalItems}</p>
<c:if test="${not empty errors}">
<ul class="errors">
<c:forEach var="e" items="${errors}"><li>${e}</li></c:forEach>
</ul>
</c:if>
forward مقابل include مقابل sendRedirect — اختيار الأداة المناسبة
- forward: من جانب الخادم، نفس الطلب/الاستجابة. استخدمه حين يُعدّ سيرفلت البيانات ثم يفوّض التصيير بالكامل إلى JSP. لا يتغير عنوان URL في المتصفح.
- include: من جانب الخادم، نفس الطلب/الاستجابة. استخدمه لتضمين ناتج أجزاء قابلة لإعادة الاستخدام (رأس، تذييل، شريط جانبي) داخل صفحة يبنيها سيرفلت متحكم.
- sendRedirect: من جانب العميل (HTTP 302). استخدمه بعد POST لمنع إعادة إرسال النموذج (نمط Post/Redirect/Get)، أو للتحويل إلى عنوان URL خارجي. يُجري المتصفح طلبًا ثانيًا، لذا تضيع سمات الطلب.
نمط MVC يتطابق مباشرةً مع forward. السيرفلت هو المتحكم (يتحقق، يستدعي النموذج، يُخزّن النتائج في سمات الطلب)، وJSP هي العرض. forward هو نقطة التسليم بين C وV — هكذا تعمل MVC الكلاسيكية في Jakarta EE قبل إدخال إطار عمل كـ Spring MVC.
نمط عملي: المتحكم الأمامي مع التوجيه
تُوجّه كثير من التطبيقات جميع الطلبات عبر سيرفلت واحد للدخول يقرر إلى أي معالج فرعي يُفوّض. هذا هو نمط المتحكم الأمامي — المفهوم ذاته الذي ينفّذه DispatcherServlet في Spring MVC داخليًا.
@WebServlet("/app/*")
public class FrontController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path = request.getPathInfo(); // مثال: "/products" أو "/orders"
String view = switch (path) {
case "/products" -> handleProducts(request);
case "/orders" -> handleOrders(request);
default -> { response.sendError(404); yield null; }
};
if (view != null) {
request.getRequestDispatcher("/WEB-INF/views/" + view + ".jsp")
.forward(request, response);
}
}
private String handleProducts(HttpServletRequest request) {
request.setAttribute("products", ProductRepository.findAll());
return "product-list";
}
private String handleOrders(HttpServletRequest request) {
request.setAttribute("orders", OrderRepository.findAll());
return "order-list";
}
}
الخلاصة
يمنحك RequestDispatcher أداتَي تفويض من جانب الخادم. استخدم forward لتسليم الاستجابة الكاملة إلى JSP أو سيرفلت آخر — يُعدّ السيرفلت البيانات كسمات طلب ثم يتنحى جانبًا. استخدم include لتضمين ناتج جزء من مورد آخر داخل صفحة تبنيها. يعمل كلاهما ضمن دورة طلب/استجابة HTTP نفسها، ويتشاركان خريطة السمات ذاتها، وكلاهما غير مرئي للمتصفح. معرفة متى تُعيد التوجيه، ومتى تُضمّن، ومتى تُحوّل العميل بدلًا من ذلك، يُعدّ أحد قرارات التوجيه الجوهرية في أي تطبيق Jakarta EE على الويب.