معالجة الطلبات
يبدأ كل servlet ذي معنى بالطريقة ذاتها: يسلّمك الحاوي كائن HttpServletRequest ويتوقع منك استخراج البيانات التي أرسلها العميل. هذا الكائن الواحد هو البوابة إلى معاملات سلسلة الاستعلام وحقول النماذج ورؤوس HTTP ومعلومات المسار والكوكيز وجسم الطلب الخام. إنّ معرفة كيفية القراءة منه بدقة وأمان هي من أكثر المهارات عملية في جانب الخادم بلغة Java.
كائن HttpServletRequest
تمتد HttpServletRequest من ServletRequest وتمنحك كل ما يخص بروتوكول HTTP تحديدًا. تُعبّئ الحاوي هذا الكائن قبل استدعاء doGet أو doPost، لذا يكون جاهزًا تمامًا للاستخدام فور تسلّمه. لا تُنشئ هذا الكائن بنفسك أبدًا.
أكثر مجموعات الدوال استخدامًا هي:
- الوصول إلى المعاملات — قيم سلسلة الاستعلام وجسم النموذج.
- الوصول إلى الرؤوس — رؤوس HTTP التي يرسلها المتصفح أو الوسيط الأمامي.
- بيانات الطلب الوصفية — الطريقة والعنوان ومسار السياق وعنوان IP البعيد.
- مخزن السمات — حقيبة شبيهة بـ
Map لتمرير الكائنات بين المكونات خلال طلب واحد.
قراءة معاملات سلسلة الاستعلام والنماذج
سواء وصلت البيانات عبر عنوان URL (مثل ?name=Alice&age=30) أو في جسم النموذج بنوع المحتوى application/x-www-form-urlencoded، تعرضها servlet API بشكل موحّد من خلال getParameter. هذا التوحيد مقصود: أنت تصف ما تحتاجه، لا من أين جاء.
@WebServlet("/search")
public class SearchServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// قيمة واحدة — ترجع null إذا غاب المعامل
String query = req.getParameter("q");
String page = req.getParameter("page");
// تحويل آمن إلى عدد صحيح مع قيمة افتراضية معقولة
int pageNum = 1;
if (page != null && !page.isBlank()) {
try {
pageNum = Integer.parseInt(page.trim());
} catch (NumberFormatException e) {
pageNum = 1; // تعامل مع المدخل غير الصالح كالصفحة الأولى
}
}
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().printf("البحث عن: %s (صفحة %d)%n", query, pageNum);
}
}
ترجع getParameter القيمة null وليس سلسلة فارغة حين يكون المعامل غائبًا كليًا. تحقق دائمًا من عدم كونها null قبل استدعاء أي دالة عليها. تخطّي هذا التحقق هو أحد أكثر أسباب NullPointerException شيوعًا في كود servlet.
معاملات متعددة القيم
مجموعة مربعات الاختيار أو المعامل المكرّر في عنوان URL (مثل ?tag=java&tag=web&tag=servlet) يُرسل المفتاح ذاته عدة مرات. تعيد getParameter القيمة الأولى فقط في هذه الحالة. استخدم getParameterValues لاسترداد جميع القيم بصيغة String[].
String[] tags = req.getParameterValues("tag");
if (tags != null) {
for (String tag : tags) {
// معالجة كل وسم
}
}
إن أردت صورة كاملة عن كل اسم معامل أرسله العميل، تُعيد getParameterMap() كائنًا من نوع Map<String, String[]> — كل مفتاح يُعيّن على المصفوفة الكاملة لقيم ذلك الاسم. هذا مفيد للتسجيل والتنقيح وبناء معالجات نماذج عامة.
Map<String, String[]> params = req.getParameterMap();
params.forEach((name, values) ->
System.out.println(name + " = " + Arrays.toString(values)));
ترميز المعاملات: تفك الحاوي تشفير الأحرف المشفّرة بنسبة مئوية (مثل %20 → مسافة) تلقائيًا. غير أنها تستخدم ترميز الأحرف المُعلَن للطلب. بالنسبة لبيانات النماذج غير ASCII، استدع دائمًا req.setCharacterEncoding("UTF-8") قبل أول استدعاء لـ getParameter — بمجرد قراءة المعاملات يصبح تغيير الترميز بلا أثر.
قراءة رؤوس HTTP
تحمل رؤوس HTTP البيانات الوصفية التي يُرفقها العميل بكل طلب: اسم المتصفح وأنواع المحتوى المقبولة ورموز المصادقة وتوجيهات الذاكرة المؤقتة وغيرها. تقرأها بالاسم عبر getHeader(String name). أسماء الرؤوس غير حساسة لحالة الأحرف وفق مواصفات HTTP، وتحترم servlet API ذلك.
String userAgent = req.getHeader("User-Agent");
String accept = req.getHeader("Accept");
String authHeader = req.getHeader("Authorization"); // مثال: "Bearer eyJ..."
String contentType = req.getContentType(); // اختصار لـ Content-Type
// رؤوس رقمية — استخدم getIntHeader لتجنب التحليل اليدوي
int contentLength = req.getIntHeader("Content-Length"); // يعيد -1 إن غاب
// رؤوس التاريخ (تُعيد ملي ثانية منذ epoch، أو -1 إن غاب)
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
حين تحتاج إلى فحص كل رأس أرسله العميل، استخدم getHeaderNames() التي تعيد Enumeration<String>:
Enumeration<String> names = req.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
System.out.println(name + ": " + req.getHeader(name));
}
لا تعتمد على رأسَي User-Agent وX-Forwarded-For في قرارات الأمان. يسهل تزوير كليهما بأي عميل HTTP. استخدمهما فقط في التحليلات والتسجيل أو التفاوض على المحتوى — لا في المصادقة أو التحكم في الوصول.
البيانات الوصفية للطلب
فضلًا عن المعاملات والرؤوس، تعرض HttpServletRequest معلومات هيكلية عن الطلب نفسه. هذه الدوال ضرورية عند كتابة فلاتر عامة أو برمجيات تسجيل وسيطة أو منطق توجيه URL.
String method = req.getMethod(); // "GET"، "POST"، "PUT"، …
String requestURI = req.getRequestURI(); // "/app/search"
String contextPath = req.getContextPath(); // "/app" (سلسلة فارغة لسياق ROOT)
String servletPath = req.getServletPath(); // "/search"
String queryString = req.getQueryString(); // "q=java&page=2" (null إن لم يكن)
String remoteAddr = req.getRemoteAddr(); // IP العميل (قد يكون IP الوسيط)
String scheme = req.getScheme(); // "http" أو "https"
int serverPort = req.getServerPort(); // 8080 أو 443 أو …
قراءة جسم الطلب الخام
لطلبات POST أو PUT التي تحمل JSON أو XML أو أنواع محتوى أخرى غير النماذج، يصل الجسم كتيار بايتات. تصل إليه عبر getInputStream() (للبيانات الثنائية) أو getReader() (للنصوص). الاثنان متنافيان: استدعاء كليهما في الطلب ذاته يُلقي IllegalStateException.
// قراءة جسم JSON كسلسلة نصية
if ("application/json".equals(req.getContentType())) {
req.setCharacterEncoding("UTF-8");
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = req.getReader()) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
String json = sb.toString();
// مرّر json إلى مكتبة JSON (Gson أو Jackson أو غيرها)
}
قراءة الجسم تستهلكه. على عكس المعاملات التي تُخزَّن مؤقتًا داخليًا، قراءة تيار الإدخال أو القارئ عملية تُنجز مرة واحدة. بمجرد القراءة تختفي البيانات من ذلك الطلب. هذا مهم عند كتابة فلاتر تفحص الجسم: يجب أن تُخزّنه في ذاكرة مؤقتة وتستبدل التيار، أو تستخدم غلافًا (wrapper).
جمع كل شيء: نقطة نهاية بحث واقعية
إليك servlet يجمع كل ما سبق — المعاملات والرؤوس والبيانات الوصفية — في نمط ستتعرف عليه في التطبيقات الحقيقية:
@WebServlet("/api/products")
public class ProductSearchServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
// المعاملات
String keyword = req.getParameter("keyword");
String category = req.getParameter("category");
String[] tags = req.getParameterValues("tag");
int page = parseIntOrDefault(req.getParameter("page"), 1);
int size = parseIntOrDefault(req.getParameter("size"), 20);
// الرأس — الكشف عمّا إذا كان العميل يريد JSON
String accept = req.getHeader("Accept");
boolean wantsJson = accept != null && accept.contains("application/json");
// البيانات الوصفية
String ip = req.getRemoteAddr();
// تسجيل الطلب
System.out.printf("[%s] GET /api/products keyword=%s page=%d wantsJson=%b%n",
ip, keyword, page, wantsJson);
// ... تفويض إلى طبقة الخدمة ثم كتابة الاستجابة
resp.setContentType(wantsJson ? "application/json" : "text/html");
resp.getWriter().write("{}"); // عنصر نائب
}
private int parseIntOrDefault(String value, int defaultValue) {
if (value == null || value.isBlank()) return defaultValue;
try { return Integer.parseInt(value.trim()); }
catch (NumberFormatException e) { return defaultValue; }
}
}
الخلاصة
HttpServletRequest هو نقطة اتصالك الوحيدة بكل ما أرسله العميل. استخدم getParameter للقيم المفردة وgetParameterValues حين يتكرر المفتاح. تحقق دائمًا من القيم المجهولة وتحقق من صحتها قبل التحليل. اقرأ الرؤوس بـ getHeader — وتذكر أنها استشارية لا حاسمة لأغراض الأمان. للأجسام غير النموذجية، استخدم getReader() أو getInputStream() مع الوعي بأن القراءة تُدمّر البيانات. الدرس التالي يتناول HttpServletResponse: بمجرد قراءة الطلب بشكل صحيح، ستعرف تمامًا ما الذي تكتبه في الاستجابة.