مشروع: عميل REST API
على امتداد هذا البرنامج التعليمي بنيتَ كل قطعة تحتاجها: المقابس الخام، وعميل HttpClient الحديث، والطلبات غير المتزامنة، وتحليل JSON، وأفضل الممارسات. هذا الدرس الأخير يجمع كل ذلك في تطبيق كونسول متكامل وعالي الجودة يستهلك واجهة برمجية REST حقيقية ومتاحة للعموم. الهدف ليس مجرد كود يعمل — بل كود منظّم وصلب وقابل للاختبار، لا تتردد في شحنه للإنتاج.
ما الذي نبنيه؟
أداة سطر أوامر تستعلم واجهة JSONPlaceholder العامة (https://jsonplaceholder.typicode.com) — خادم REST مجاني لا يتطلب مصادقة، يُستخدم على نطاق واسع في النماذج الأولية. سيتمكن عميلنا من:
- جلب قائمة المنشورات وطباعة جدول ملخّص.
- جلب منشور واحد مع تعليقاته بشكل متزامن (طلبان HTTP غير متزامنَين، مُدمَجان).
- إنشاء منشور جديد عبر طلب
POST وعرض استجابة الخادم.
- تغليف كل منطق الشبكة والتحليل في طبقة خدمة خفيفة لإبقاء منطق الأعمال قابلًا للاختبار.
هيكل المشروع
src/
model/
Post.java
Comment.java
service/
JsonPlaceholderClient.java
util/
JsonParser.java
Main.java
إبقاء النماذج والخدمات والأدوات المساعدة في حزم منفصلة يتّبع مبدأ الفصل بين المسؤوليات ذاته الذي طبّقته في الوحدات السابقة.
الخطوة الأولى — تعريف نماذج النطاق
استخدم سجلات Java للكائنات غير القابلة للتغيير والتي تحمل البيانات فقط — فهي الخيار الاصطلاحي منذ Java 16 لكائنات نقل البيانات.
// model/Post.java
package model;
public record Post(int userId, int id, String title, String body) {}
// model/Comment.java
package model;
public record Comment(int postId, int id, String name, String email, String body) {}
لماذا السجلات (records)؟ توفّر السجلات دوال equals وhashCode وtoString وأساليب الوصول مجانًا وبدون أي نمطية متكررة. كما تُشير لكل قارئ أن هذه الكائنات هي ناقلات بيانات صرفة — وليست كيانات ذات حالة قابلة للتغيير.
الخطوة الثانية — أداة تحليل JSON مبسّطة
الكود الإنتاجي سيستخدم Jackson أو Gson. في هذا المشروع المعتمد على نفسه نكتب محلّل بسيط يدوي لأشكال البيانات التي نحتاجها فقط. هذا يعزّز ما تعلّمته في الدرس الثامن ويبقي المشروع بلا تبعيات خارجية.
// util/JsonParser.java
package util;
import model.Comment;
import model.Post;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class JsonParser {
private JsonParser() {}
/** استخرج قيمة نصية لحقل JSON مسمّى. */
public static String extractString(String json, String field) {
Pattern p = Pattern.compile("\"" + field + "\"\\s*:\\s*\"(.*?)\"");
Matcher m = p.matcher(json);
return m.find() ? m.group(1) : "";
}
/** استخرج قيمة صحيحة لحقل JSON مسمّى. */
public static int extractInt(String json, String field) {
Pattern p = Pattern.compile("\"" + field + "\"\\s*:\\s*(\\d+)");
Matcher m = p.matcher(json);
return m.find() ? Integer.parseInt(m.group(1)) : 0;
}
/** قسّم مصفوفة JSON إلى سلاسل كائنات منفردة. */
public static List<String> splitObjects(String jsonArray) {
List<String> objects = new ArrayList<>();
int depth = 0;
int start = -1;
for (int i = 0; i < jsonArray.length(); i++) {
char c = jsonArray.charAt(i);
if (c == '{') { if (depth++ == 0) start = i; }
else if (c == '}') { if (--depth == 0) objects.add(jsonArray.substring(start, i + 1)); }
}
return objects;
}
public static Post parsePost(String json) {
return new Post(
extractInt(json, "userId"),
extractInt(json, "id"),
extractString(json, "title"),
extractString(json, "body")
);
}
public static Comment parseComment(String json) {
return new Comment(
extractInt(json, "postId"),
extractInt(json, "id"),
extractString(json, "name"),
extractString(json, "email"),
extractString(json, "body")
);
}
}
الخطوة الثالثة — خدمة عميل الواجهة البرمجية
كل منطق HTTP يقطن في فئة خدمة واحدة. تحتفظ بنسخة مشتركة من HttpClient (آمنة للخيوط ومدركة لمجموعة الاتصالات) وتعرض أساليب نظيفة ذات أنواع محددة.
// service/JsonPlaceholderClient.java
package service;
import model.Comment;
import model.Post;
import util.JsonParser;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class JsonPlaceholderClient {
private static final String BASE = "https://jsonplaceholder.typicode.com";
private final HttpClient http;
public JsonPlaceholderClient() {
this.http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
}
// ── متزامن: جلب كل المنشورات ───────────────────────────────────────────
public List<Post> fetchPosts() throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/posts"))
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
requireSuccess(res);
return JsonParser.splitObjects(res.body())
.stream()
.map(JsonParser::parsePost)
.toList();
}
// ── غير متزامن: جلب منشور وتعليقاته في آنٍ واحد ──────────────────────
public CompletableFuture<Post> fetchPostAsync(int id) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/posts/" + id))
.GET()
.build();
return http.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(res -> { requireSuccessUnchecked(res); return JsonParser.parsePost(res.body()); });
}
public CompletableFuture<List<Comment>> fetchCommentsAsync(int postId) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/posts/" + postId + "/comments"))
.GET()
.build();
return http.sendAsync(req, HttpResponse.BodyHandlers.ofString())
.thenApply(res -> {
requireSuccessUnchecked(res);
return JsonParser.splitObjects(res.body())
.stream()
.map(JsonParser::parseComment)
.toList();
});
}
// ── POST: إنشاء منشور جديد ──────────────────────────────────────────────
public Post createPost(int userId, String title, String body) throws Exception {
String json = """
{"userId":%d,"title":"%s","body":"%s"}
""".formatted(userId, title, body).strip();
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/posts"))
.timeout(Duration.ofSeconds(10))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
requireSuccess(res);
return JsonParser.parsePost(res.body());
}
// ── أدوات مساعدة ─────────────────────────────────────────────────────────
private void requireSuccess(HttpResponse<?> res) throws Exception {
if (res.statusCode() < 200 || res.statusCode() >= 300) {
throw new Exception("خطأ HTTP " + res.statusCode());
}
}
private void requireSuccessUnchecked(HttpResponse<?> res) {
if (res.statusCode() < 200 || res.statusCode() >= 300) {
throw new RuntimeException("خطأ HTTP " + res.statusCode());
}
}
}
نسخة واحدة مشتركة من HttpClient، لا نسخة لكل طلب. يدير HttpClient مجموعة اتصالات داخليًا. إنشاء نسخة جديدة لكل طلب يُبطل تجميع الاتصالات، ويهدر الخيوط، وقد يُنهك واصفات ملفات نظام التشغيل تحت الحِمل. شارك دائمًا نسخة واحدة — على مستوى فئة الخدمة، أو عبر حاوية حقن التبعيات.
الخطوة الرابعة — نقطة الدخول الرئيسية
// Main.java
import model.Comment;
import model.Post;
import service.JsonPlaceholderClient;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) throws Exception {
JsonPlaceholderClient client = new JsonPlaceholderClient();
// 1. جلب وعرض أول 5 منشورات
System.out.println("=== المنشورات (أول 5) ===");
List<Post> posts = client.fetchPosts();
posts.stream().limit(5).forEach(p ->
System.out.printf("[%d] %s%n", p.id(), p.title())
);
// 2. جلب المنشور رقم 1 وتعليقاته بشكل متزامن
System.out.println("\n=== المنشور #1 مع التعليقات (غير متزامن) ===");
CompletableFuture<Post> postFuture = client.fetchPostAsync(1);
CompletableFuture<List<Comment>> commentFuture = client.fetchCommentsAsync(1);
CompletableFuture.allOf(postFuture, commentFuture).join();
Post post = postFuture.join();
List<Comment> comments = commentFuture.join();
System.out.println("العنوان : " + post.title());
System.out.println("المؤلف : userId " + post.userId());
System.out.println("التعليقات (" + comments.size() + "):");
comments.forEach(c -> System.out.println(" - " + c.name() + " <" + c.email() + ">"));
// 3. إنشاء منشور جديد
System.out.println("\n=== إنشاء منشور ===");
Post created = client.createPost(1, "منشوري الجديد", "مرحبًا من Java HttpClient");
System.out.println("المعرّف المُنشأ: " + created.id() + "، العنوان: " + created.title());
}
}
الخطوة الخامسة — استراتيجية معالجة الأخطاء
يحتاج العميل الإنتاجي إلى أكثر من المسار السعيد. طبّق هذه الطبقات:
- على مستوى الشبكة: غلّف الاستدعاءات في try-catch للتقاط
java.io.IOException (اتصال مرفوض، انتهاء مهلة). سجّل وأبرز رسالة مفهومة للمستخدم.
- على مستوى HTTP: تحقق من رمز الحالة قبل التحليل. جسم الاستجابة عند
404 أو 503 ليس كائن نطاق صالح.
- على مستوى التحليل: احمِ من أشكال JSON غير المتوقعة بفحوص null أو باستخدام
Optional.
- إعادة المحاولة: للأخطاء العابرة (5xx، المهلات) نفّذ التراجع الأسّي — على الأقل حدّد عدد المحاولات بثلاث.
لا تبتلع الاستثناءات بصمت. كتلة catch لا تفعل شيئًا — أو تطبع فقط تتبع المكدس وترجع null — تُنتج أخطاء مربكة تُفسد البيانات في مراحل لاحقة. إما عالج الفشل بطريقة ذات معنى، أو غلّفه في استثناء نطاق، أو دعه ينتشر للأعلى.
مقايضات ينبغي معرفتها
قبل الإعلان عن هذا العميل جاهزًا للإنتاج، كن صريحًا بشأن قيوده والمقايضات التي قبلتها:
- محلّل JSON اليدوي مقابل مكتبة: محلّلنا المعتمد على التعبيرات النمطية هش أمام الكائنات المتداخلة والأحرف المُهرَّبة ومصفوفات القيم الأولية. يتعامل Jackson أو Gson مع كل الحالات الطرفية في سطر واحد. استخدم مكتبة في الكود الحقيقي.
- لا منطق إعادة محاولة: يفشل العميل فور أول خطأ. يغلّف العميل الحقيقي الاستدعاءات بمُزيّن إعادة محاولة أو يستخدم Resilience4j.
- لا مصادقة: لا تحتاجها JSONPlaceholder. الواجهات البرمجية الحقيقية تتطلب رموز Bearer أو مفاتيح API أو OAuth — أضفها كترويسات
Authorization في أسلوب مركزي يشبه المعترض.
- نموذج الخيوط: يستخدم
sendAsync مجموعة الانقسام والانضمام المشتركة افتراضيًا. للتزامن العالي، وفّر Executor مخصصًا عبر HttpClient.newBuilder().executor(...).
الخلاصة
أصبح لديك الآن عميل REST API يعمل بالكامل، منظّم جيدًا، بلغة Java الصرفة. المبادئ الرئيسية: شارك نسخة واحدة من HttpClient؛ افصل ميكانيكيات HTTP عن منطق الأعمال في فئة خدمة؛ استخدم السجلات للكائنات غير القابلة للتغيير؛ تحقق من رموز الحالة قبل التحليل؛ تعامل مع الأخطاء في كل طبقة. هذه الأنماط تنطبق سواء كنت تستدعي بوابة دفع، أو واجهة طقس، أو خدمة مصغّرة داخلية. تهانينا على إكمال برنامج الشبكات — أصبحت تمتلك الأدوات اللازمة لبناء تطبيقات Java شبكية احترافية.