استهلاك واجهات REST البرمجية
نادرًا ما تعمل تطبيقات Java الحديثة في عزلة. فهي تستدعي خدمات الطقس وبوابات الدفع والخدمات المصغّرة الداخلية والمنصات الخارجية — كلّها عبر HTTP بصيغة JSON. في هذا الدرس ستتعلّم كيفية استهلاك واجهة REST برمجية من البداية إلى النهاية: بناء الطلب، وإرساله عبر HttpClient، وتحليل استجابة JSON إلى كائنات Java مكتوبة بأنواع محدّدة، ومعالجة الأخطاء بشكل صحيح، وهيكلة كود التكامل ليبقى قابلًا للصيانة مع نمو الواجهة البرمجية.
سير العمل الأساسي
تتّبع كل استدعاء لواجهة REST البرمجية الخطوات الخمس ذاتها:
- بناء
HttpRequest (الرابط، الترويسات، الأسلوب، الجسم الاختياري).
- إرساله عبر
HttpClient واستقبال HttpResponse<String>.
- التحقّق من رمز حالة HTTP.
- تحليل جسم استجابة JSON إلى نموذج مكتوب بأنواع محدّدة.
- إعادة النموذج إلى المُستدعي (أو رمي استثناء خاص بالنطاق).
تعلّمت الخطوات 1-3 في الدروس السابقة. يُركّز هذا الدرس على الخطوتين 4-5 وعلى تصميم فئة عميل API احترافية صالحة للإنتاج.
اختيار مكتبة JSON
لا تحتوي Java SE على مُحلّل JSON مدمج. الخياران الرئيسيان هما:
- Jackson (
com.fasterxml.jackson.core:jackson-databind) — المعيار الفعلي في Java المؤسّسي. سريع جدًا، ويتعامل مع البنى المتداخلة المعقّدة، وله نموذج تعليقات توضيحية غني.
- Gson (
com.google.code.gson:gson) — واجهة برمجية أبسط، أقل قابلية للتهيئة قليلًا، مناسب للتعيينات المباشرة.
تستخدم الأمثلة أدناه Jackson لأنّه ما ستصادفه أكثر في قواعد الكود الحقيقية. المفاهيم تنتقل مباشرةً إلى Gson.
ObjectMapper واحد، مشترك دائمًا. ObjectMapper آمن للخيوط بعد الإعداد ومُكلف الإنشاء. أنشئه مرة واحدة (حقل ثابت أو كائن مفرد مُحقَن عبر DI) وأعد استخدامه في كل مكان. إنشاء نسخة جديدة لكل طلب هو أحد أشيع أخطاء الأداء في عملاء HTTP بـ Java.
تعريف النموذج (Record أو POJO)
ابدأ بنمذجة ما تُعيده الواجهة البرمجية. في الأمثلة أدناه، تخيّل أننا نستدعي واجهة إدارة مستخدمين عامة تُعيد:
// JSON من GET /users/1
{
"id": 1,
"name": "Leanne Graham",
"username": "Bret",
"email": "Sincere@april.biz",
"address": {
"city": "Gwenborough"
}
}
مع Java 17+ تُعدّ السجلات (Records) أنظف طريقة لنمذجة استجابات الواجهات البرمجية للقراءة فقط:
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true) // تتسامح مع الحقول الإضافية من الواجهة البرمجية
public record Address(String city) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record User(long id, String name, String username,
String email, Address address) {}
@JsonIgnoreProperties(ignoreUnknown = true) ضروري في الإنتاج: تتطوّر واجهات REST وتُضيف حقولًا جديدة. بدون هذا التعليق التوضيحي، يرمي Jackson استثناءً عند أي حقل لا يُعلن عنه سجلّك. دائمًا علّم نماذج استجابة الواجهة البرمجية به.
بناء فئة عميل الواجهة البرمجية
احصر كل منطق HTTP في فئة عميل مخصّصة. يُبقي ذلك مخاوف HTTP بعيدة عن منطق العمل ويجعل الواجهة البرمجية سهلة الاختبار.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class UserApiClient {
private static final String BASE_URL = "https://jsonplaceholder.typicode.com";
private static final ObjectMapper MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final HttpClient http;
public UserApiClient() {
this.http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
}
public User getUser(long id) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/users/" + id))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new ApiException(
"GET /users/" + id + " failed — HTTP " + response.statusCode(),
response.statusCode());
}
return MAPPER.readValue(response.body(), User.class);
}
}
حدّد دائمًا كلا المهلتين الزمنيتين. connectTimeout على HttpClient يُقيّد المدة التي تنتظرها لتأسيس اتصال TCP. timeout() على HttpRequest يُقيّد المدة الإجمالية للطلب. حذف أي منهما يعني أن خادمًا بطيئًا أو معطوبًا قد يُعلّق خيطك إلى الأبد.
استثناء واجهة برمجية مخصّص
لا تُعرّض IOException الخام أو رموز حالة HTTP للمُستدعين. التفّها في استثناء خاص بالنطاق يحمل رمز الحالة:
public class ApiException extends RuntimeException {
private final int statusCode;
public ApiException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
public int getStatusCode() { return statusCode; }
}
يستطيع المُستدعون حينئذٍ كتابة catch (ApiException e) والتفرّع على e.getStatusCode() دون معرفة أي شيء عن داخليات HTTP.
إلغاء تسلسل المجموعات
تُعيد كثير من النقاط النهائية مصفوفات JSON. استخدم TypeReference للحفاظ على النوع العام وقت التشغيل (وإلا فإن محو نوع Java سيفقده):
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;
public List<User> getAllUsers() throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/users"))
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new ApiException("GET /users failed — HTTP " + response.statusCode(),
response.statusCode());
}
return MAPPER.readValue(response.body(),
new TypeReference<List<User>>() {});
}
إرسال طلب POST بجسم JSON
لإنشاء مورد، تسلسل كائن طلبك إلى JSON وأرسله كجسم الطلب:
public record CreateUserRequest(String name, String username, String email) {}
public User createUser(CreateUserRequest payload) throws Exception {
String json = MAPPER.writeValueAsString(payload);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/users"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response =
http.send(request, HttpResponse.BodyHandlers.ofString());
// 201 Created هي نجاح مُتوقَّع لـ POST
if (response.statusCode() != 201) {
throw new ApiException("POST /users failed — HTTP " + response.statusCode(),
response.statusCode());
}
return MAPPER.readValue(response.body(), User.class);
}
إضافة ترويسات المصادقة
تتطلّب معظم الواجهات البرمجية الحقيقية مصادقة — عادةً رمز Bearer أو ترويسة مفتاح API. مرّرها عبر بانٍ الطلب:
private final String apiKey;
public User getAuthenticatedUser(long id) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + "/users/" + id))
.header("Accept", "application/json")
.header("Authorization", "Bearer " + apiKey)
.timeout(Duration.ofSeconds(10))
.GET()
.build();
// ... نفس منطق الإرسال والتحليل
}
لا تُضمّن بيانات الاعتماد في الكود أبدًا. حمّل مفاتيح API من متغيّرات البيئة أو مدير الأسرار — لا من الملفات المُدارة بالتحكّم في الإصدار. مفتاح مُسرَّب في مستودع git حادثة أمنية خطيرة لا يمكن التراجع عنها بالكامل حتى بعد التدوير.
الجمع في صورة كاملة
إليك دالة main كاملة تختبر العميل:
public class Main {
public static void main(String[] args) {
var client = new UserApiClient();
try {
User user = client.getUser(1);
System.out.println("Fetched: " + user.name() + " <" + user.email() + ">");
var all = client.getAllUsers();
System.out.println("Total users: " + all.size());
var newUser = new CreateUserRequest("Ada Lovelace", "ada", "ada@example.com");
User created = client.createUser(newUser);
System.out.println("Created with id: " + created.id());
} catch (ApiException e) {
System.err.println("API error " + e.getStatusCode() + ": " + e.getMessage());
} catch (Exception e) {
System.err.println("Unexpected error: " + e.getMessage());
}
}
}
الخلاصة
لاستهلاك واجهة REST برمجية باحترافية في Java: نمذج الاستجابات بسجلات مُعلَّقة توضيحيًا، وشارك ObjectMapper واحدًا، والتف منطق HTTP في فئة عميل مخصّصة، وتحقّق من رموز الحالة صراحةً وارمِ استثناءات مكتوبة، واستخدم TypeReference للمجموعات، ولا تُضمّن بيانات الاعتماد في الكود أبدًا. يُوسَّع هذا النمط من نقطة نهائية واحدة إلى SDK كامل للواجهة البرمجية دون تغييرات هيكلية.