بناء الخدمات المصغّرة بـ Spring Boot

العملاء التصريحيون مع OpenFeign

18 دقيقة الدرس 4 من 12

العملاء التصريحيون مع OpenFeign

في الدرس السابق أنشأت استدعاءات بين الخدمات باستخدام WebClient. يعمل هذا النهج، لكنه يُلزمك بتجميع عناوين URL يدويًا وإدارة الترويسات بشكل صريح وكتابة سلاسل تفاعلية إلزامية. مع نمو منظومة الخدمات المصغّرة لديك، يتراكم هذا الكود المتكرر بسرعة. يحلّ Spring Cloud OpenFeign هذه المشكلة بالسماح لك بتعريف عميل HTTP كواجهة Java بسيطة — نفس النموذج الذهني الذي تستخدمه بالفعل مع مستودعات Spring Data — ويُولّد الإطار التنفيذَ نيابةً عنك عند بدء التشغيل.

ما هو OpenFeign وكيف يعمل

OpenFeign (المعروف في الأصل بـ Netflix Feign، وهو الآن تحت إشراف مجتمع OpenFeign) هو عميل HTTP تصريحي. تُضيف تعليقات توضيحية إلى واجهة بـ @FeignClient وتعليقات Spring MVC القياسية (@GetMapping و@PostMapping ومتغيرات المسار ومعاملات الطلب وأجسام الطلبات). عند بدء التشغيل، يفحص Spring Cloud تلك الواجهات ويُنشئ وكلاء ديناميكيين JDK تُترجم كل استدعاء للأسلوب إلى طلب HTTP حقيقي.

التصريحي مقابل الإلزامي: مع WebClient تكتب الكيفية (بناء طلب، إضافة ترويسات، الاشتراك). مع Feign تكتب الماذا (استدعاء getOrder(id))، والإطار يتولى الباقي. النتيجة كود أقل بكثير وسطح تدقيق أسهل.

إضافة الاعتمادية

أضف مُشغّل Spring Cloud OpenFeign إلى ملف pom.xml الخاص بخدمتك. تحتاج أيضًا إلى BOM الخاص بـ Spring Cloud في كتلة إدارة الاعتمادية:

<!-- في <dependencyManagement> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2023.0.2</version> <type>pom</type> <scope>import</scope> </dependency> <!-- في <dependencies> --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>

تفعيل Feign في تطبيقك

أضف تعليق @EnableFeignClients إلى فئتك الرئيسية (أو أي فئة @Configuration). بدون هذا، لن يفحص Spring أي واجهة @FeignClient.

package com.example.orderservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }

تعريف أول عميل Feign

لنفترض أن خدمة الطلبات تحتاج إلى البحث عن المنتجات من خدمة المخزون. تعرض خدمة المخزون نقطة نهاية GET /products/{id}. إليك التعريف الكامل لعميل Feign:

package com.example.orderservice.client; import com.example.orderservice.dto.ProductResponse; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; import java.util.List; @FeignClient(name = "inventory-service", url = "${services.inventory.url}") public interface InventoryClient { @GetMapping("/products/{id}") ProductResponse getProduct(@PathVariable("id") Long id); @GetMapping("/products") List<ProductResponse> listProducts(@RequestParam("category") String category); }

بعض الملاحظات المهمة:

  • name هو اسم منطقي يُستخدم للمقاييس و(عند دمجه مع سجل الخدمات) للبحث بالتوازن.
  • url هو عنوان URL الأساسي، يُقرأ هنا من application.yml. في بيئة اكتشاف الخدمات (Eureka أو Consul) يمكنك حذف url ويحلّ Feign عنوانها من السجل بالاستناد إلى name.
  • تعكس تواقيع الأساليب أساليب المتحكم في الخدمة الهدف — نفس التعليقات التوضيحية، نفس الأنواع.

حقن العميل واستخدامه

يُسجّل Spring الوكيل المُولَّد كحبة (bean) بنوع الواجهة. احقنه تمامًا كأي حبة Spring أخرى:

package com.example.orderservice.service; import com.example.orderservice.client.InventoryClient; import com.example.orderservice.dto.ProductResponse; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class OrderService { private final InventoryClient inventoryClient; public void placeOrder(Long productId, int quantity) { ProductResponse product = inventoryClient.getProduct(productId); // product مُفكَّك بالكامل — لا سلاسل تفاعلية، لا تحليل يدوي if (product.getStock() < quantity) { throw new IllegalStateException("مخزون غير كافٍ للمنتج: " + product.getName()); } // ... حفظ الطلب } }
احتفظ بواجهة العميل في حزمة client مخصصة وضع بجانبها فئات DTO الخاصة بها أيضًا. يرسم هذا حدًا واضحًا: تمتلك حزمة client كل ما يتطلبه عقد الخدمة الخارجية، مما يُسهّل استبداله أو إصدار نسخة جديدة منه لاحقًا.

إرسال جسم الطلب

تعمل طلبات POST و PUT بنفس السلاسة. أضف تعليق @RequestBody إلى معاملة الجسم:

@FeignClient(name = "notification-service", url = "${services.notification.url}") public interface NotificationClient { @PostMapping("/notifications") NotificationResponse sendNotification(@RequestBody NotificationRequest request); }

تخصيص ترويسات الطلب

كثيرًا ما تحتاج إلى تمرير الترويسات — معرّفات الارتباط، ورموز التفويض، ومعرّفات المستأجر — إلى الخدمات البعيدة. يوفر Feign آليتين:

١. تعليق الترويسة لكل أسلوب — لقيم الترويسة الثابتة أو المُقدَّمة من المُستدعي:

@GetMapping(value = "/internal/orders/{id}", headers = "X-Internal-Key=secret-key") OrderResponse getOrderInternal(@PathVariable Long id);

٢. معترض الطلب (Request Interceptor) — للترويسات التي تريد إضافتها لكل استدعاء من عميل معين (أو بشكل عالمي):

package com.example.orderservice.config; import feign.RequestInterceptor; import feign.RequestTemplate; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @Component public class CorrelationIdInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { var attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { String correlationId = attributes.getRequest() .getHeader("X-Correlation-ID"); if (correlationId != null) { template.header("X-Correlation-ID", correlationId); } } } }

بما أن هذه الحبة هي @Component فهي تُطبَّق بشكل عالمي — كل عميل Feign في التطبيق سيُمرّر معرّف الارتباط. لتقييدها بعميل واحد، عرّفها داخل فئة @Configuration وأشر إليها عبر @FeignClient(configuration = MyConfig.class).

لا تُمرّر ترويسة Authorization من الطلبات الواردة بصورة عمياء. إذا استقبلت الخدمة A رمز JWT لمستخدم وأرسلته دون تغيير إلى الخدمة B، فأنت أوجدت ثغرة أمنية: تقبل الخدمة B الآن رموزًا على مستوى المستخدم لما يجب أن يكون استدعاءً داخليًا بين الخدمات. استخدم آلية منفصلة — سرًا مشتركًا أو mTLS أو رمز حساب خدمة مخصص — للاتصال الداخلي، ولا تدع بيانات اعتماد المستخدم تتسرب بين الخدمات.

معالجة الأخطاء مع ErrorDecoder

افتراضيًا، تُثير أي استجابة 4xx أو 5xx من الخدمة البعيدة استثناء FeignException عامًا. يمكنك تعيين رموز أخطاء HTTP إلى استثناءات النطاق باستخدام ErrorDecoder مخصص:

package com.example.orderservice.config; import com.example.orderservice.exception.ProductNotFoundException; import feign.Response; import feign.codec.ErrorDecoder; import org.springframework.stereotype.Component; @Component public class InventoryErrorDecoder implements ErrorDecoder { private final ErrorDecoder defaultDecoder = new Default(); @Override public Exception decode(String methodKey, Response response) { return switch (response.status()) { case 404 -> new ProductNotFoundException( "المنتج غير موجود — بعيد: " + response.request().url()); case 503 -> new ServiceUnavailableException("خدمة المخزون غير متاحة"); default -> defaultDecoder.decode(methodKey, response); }; } }

سجّله في فئة تهيئة Feign وأشر إليه من تعليق العميل لتقييد النطاق، أو عرّفه كـ @Component للتطبيق العالمي.

ضبط المهلة الزمنية وإعدادات الاتصال

القيم الافتراضية لمهلة Feign مرتفعة بشكل خطر في بعض الإصدارات. عيّن قيمًا صريحة في application.yml:

spring: cloud: openfeign: client: config: default: # يُطبَّق على جميع العملاء connectTimeout: 2000 # مللي ثانية لإنشاء اتصال TCP readTimeout: 5000 # مللي ثانية للانتظار حتى جسم الاستجابة inventory-service: # تجاوزات لهذا العميل تحديدًا readTimeout: 10000

OpenFeign مقابل WebClient — اختيار الأداة المناسبة

  • استخدم OpenFeign حين تريد كودًا موجزًا وقابلًا للقراءة بأسلوب متزامن وإمكانية لحظر الخيط المُستدعي. يتكامل بسلاسة مع تطبيقات Spring MVC (servlet-stack) وهو الخيار الافتراضي لمعظم فرق الخدمات المصغّرة.
  • استخدم WebClient حين يكون تطبيقك تفاعليًا (Spring WebFlux، إدخال/إخراج غير محجوب) أو عندما تحتاج إلى تحكم دقيق في البث أو الاستجابات الجزئية أو الضغط الخلفي.
  • دمج استدعاءات Feign المحجوبة داخل خط أنابيب تفاعلي سيعطّل خيوط حلقة الأحداث ويُدهور التطبيق بأكمله — اختر نموذجًا واحدًا والتزم به لكل خدمة.

الخلاصة

يحوّل OpenFeign استدعاءات HTTP بين الخدمات إلى استدعاءات لأساليب واجهة Java بسيطة. تُعلن العقد، وتُضيف التعليقات التوضيحية للأساليب، وتحقن الحبة، وتستدعيها. تتولى معترضات الطلب نشر الترويسات العرضية؛ يُترجم ErrorDecoder رموز أخطاء HTTP إلى استثناءات النطاق؛ وتتحكم خصائص YAML في المهل الزمنية. تمنحك هذه القطع مجتمعةً طبقة اتصال نظيفة وآمنة من حيث الأنواع وقابلة للتدقيق، وتتسع بسلاسة مع نمو عدد الخدمات. في الدرس القادم ستُضيف أنماط المرونة — إعادة المحاولة وقاطعات الدوائر والحلول البديلة — لحماية هذه الاستدعاءات من أعطال الخدمات البعيدة.