حقن التبعية في Quarkus باستخدام CDI: ArC والنطاقات والمؤهلات وحقن وقت البناء
يعد حقن التبعية (DI) حجر الأساس لتطوير تطبيقات Java للمؤسسات الحديثة. لسنوات عديدة، اعتمد مطورو Java على Spring IoC (انعكاس التحكم) أو حاويات Jakarta EE CDI القياسية (السياقات وحقن التبعية) مثل Hibernate Weld أو OpenWebBeans لفصل دورات حياة المكونات، وإدارة تبعيات الخدمة، وحقن خصائص التكوين.
ومع ذلك، مع تحول تطبيقات المؤسسات نحو الخدمات الصغيرة السحابية الأصلية، واستخدام الحاويات مع Docker وKubernetes، ونماذج التنفيذ بدون خادم (AWS Lambda، Knative)، كشفت حاويات حقن التبعية التقليدية عن خلل فادح: الانعكاس الشديد في وقت التشغيل وعبء كبير للذاكرة.
أدخل Quarkus ومحرك CDI خفيف الوزن، ArC. تم تصميم Quarkus من الألف إلى الياء للتجميع الأصلي لـ Kubernetes وGraalVM، وهو يعيد تعريف حقن التبعية عن طريق تحويل الانعكاس ومسح مسار الفصل وأسلاك الرسم البياني للفاصوليا من وقت تشغيل التطبيق إلى وقت البناء.
في هذا الدليل التفصيلي، سوف نستكشف حقن التبعية في Quarkus باستخدام CDI، الذي يغطي الآليات الداخلية لـحاوية ArC، ونطاقات CDI، والمؤهلات المخصصة، والمنتجين، والمعترضين، والمراقبين المستندين إلى الأحداث، وعمليات البحث الآلية، وأفضل الممارسات لكتابة تطبيقات Java السحابية الأصلية عالية الأداء.
1. فهم ArC: محرك Quarkus Build-Time CDI
تعمل حاويات Jakarta EE CDI القياسية بالكامل عند بدء تشغيل التطبيق. عند تشغيل تطبيق قديم، تقوم حاوية CDI بمسح كل JAR في مسار الفصل، وتوزع بيانات التعريف، وتفحص التعليقات التوضيحية باستخدام Java Reflection (java.lang.reflect)، وتنشئ وكلاء ديناميكيين في الذاكرة، وتنشئ الرسم البياني للتبعية. يستهلك هذا الانعكاس الديناميكي قدرًا كبيرًا من ذاكرة الوصول العشوائي (Metaspace) ويزيد من وقت التشغيل البارد.
يحل Quarkus عنق الزجاجة هذا باستخدام ArC (تطبيق CDI في وقت البناء استنادًا إلى مواصفات Jakarta CDI 4.0).
كيف يختلف ARC عن CDI القياسي:
- تعزيز وقت البناء: أثناء
mvn packageأو./gradlew build، يقوم ARC بمسح جميع وحدات التطبيق، والتحقق من صحة نقاط الحقن، وإنشاء الرسم البياني للتبعية مسبقًا. - ** إنشاء أكواد بايت قبل الوقت (AOT) **: يقوم ArC بإنشاء كود Java بايت مباشر لإنشاء مثيل، ووكلاء الفول، والاعتراضات مقدمًا، مما يمنع الانعكاس في وقت التشغيل.
- ** تقليم الفول غير المستخدم (إزالة الكود الميت) **: يتم التعرف على أي حبة CDI لم يتم حقنها أو الرجوع إليها ديناميكيًا أثناء التجميع ويتم تجريدها تلقائيًا من الملف الثنائي النهائي، مما يقلل من مساحة الذاكرة.
- GraalVM Native Image Optimization: نظرًا لأنه يتم حل أسلاك الفول في وقت الإنشاء، يتم تجميع تطبيقات Quarkus بسلاسة إلى ملفات تنفيذية أصلية بدون تلميحات تكوين انعكاس.
2. نطاقات CDI Bean وإدارة دورة الحياة
في CDI، يحدد النطاق دورة حياة مثيل الفول وإمكانية رؤيته. يدعم Quarkus ArC نطاقات Jakarta CDI القياسية مع تحسينات وقت البناء.
نطاقات CDI الأساسية في Quarkus:
| شرح النطاق | دورة الحياة والسلوك | إنشاء الوكيل | أفضل حالة استخدام |
|---|---|---|---|
@ApplicationScoped |
مثيل واحد لكل تطبيق. التهيئة البطيئة عند الاستدعاء الأول. الدولة مشتركة عبر جميع المواضيع. | تم إنشاء وكيل العميل | الخدمات والمستودعات والتكوينات عديمة الحالة. |
@RequestScoped |
تم ربط المثيل بطلب HTTP واحد أو استدعاء حدث واحد. دمرت عند اكتمال الطلب. | تم إنشاء وكيل العميل | جلسات المستخدم، طلب بيانات السياق، الرموز الأمنية. |
@معال |
النطاق الافتراضي إذا لم يتم تحديد أي شيء. ترتبط المثيلة مباشرة بعمر حبة الحقن. | لا يوجد وكيل (مثيل مباشر) | فئات المرافق المساعدة، أصحاب الدولة سريعة الزوال. |
@سينجلتون |
مثيل واحد لكل تطبيق. تم مثيله بفارغ الصبر أو بتكاسل. لا يوجد غلاف وكيل العميل. | لا يوجد وكيل (مثيل مباشر) | مفردات عالية الأداء حيث يجب تجنب الحمل الوكيل للعميل. |
@غير قابل للإزالة |
تعليق توضيحي للعلامة يوجه ArC بعدم إزالة الحبة أثناء تحسين وقت البناء. | يعتمد على النطاق | أهداف البحث الديناميكي أو الفاصوليا المرجعية للانعكاس. |
مثال على الكود: الإعلان عن الفاصوليا ذات النطاق
package com.ghaznix.services;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import java.util.UUID;
@ApplicationScoped
public class OrderProcessingService {
public String processOrder(String orderId) {
return "Order " + orderId + " processed successfully.";
}
}
@RequestScoped
public class RequestContextHolder {
private final String requestId = UUID.randomUUID().toString();
public String getRequestId() {
return requestId;
}
}
3. أنماط حقن التبعية في Quarkus
يدعم Quarkus أنماط حقن متعددة باستخدام التعليق التوضيحي القياسي jakarta.inject.Inject.
أ. حقن المُنشئ (أفضل الممارسات الموصى بها)
يضمن حقن المُنشئ عدم قابلية التغيير (حقول final)، ويبسط اختبار الوحدة دون الحاجة إلى حاويات CDI، ويجعل التبعيات واضحة.
package com.ghaznix.resources;
import com.ghaznix.services.OrderProcessingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/orders")
public class OrderResource {
private final OrderProcessingService orderService;
// In Quarkus, if a class has a single constructor, @Inject is OPTIONAL!
@Inject
public OrderResource(OrderProcessingService orderService) {
this.orderService = orderService;
}
@POST
@Path("/{id}")
public String checkout(@PathParam("id") String id) {
return orderService.processOrder(id);
}
}
نصيحة احترافية: في Quarkus ArC، إذا كانت الحبة تحتوي على منشئ واحد فقط، فيمكنك حذف التعليق التوضيحي
@Injectتمامًا. يتعرف عليه ArC تلقائيًا باعتباره مُنشئ الحقن.
ب. الحقن الميداني
يعد الحقن الميداني موجزًا ولكنه يجعل الحقول قابلة للتغيير ويعقد اختبار الوحدة المستقلة.
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. المؤهلات وتوضيح عمليات التنفيذ المتعددة
عندما تقوم عدة حبوب بتنفيذ نفس الواجهة، فإن CDI يلقي استثناء تبعية غامضًا في وقت الإنشاء. يستخدم Quarkus المؤهلات لحل عمليات تنفيذ الحبة المستهدفة بأمان.
تعريف المؤهلات المخصصة
package com.ghaznix.payment;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PaymentType {
Mode value();
enum Mode {
CREDIT_CARD,
PAYPAL,
CRYPTO
}
}
تنفيذ الفاصوليا المؤهلة
public interface PaymentProcessor {
void process(double amount);
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.CREDIT_CARD)
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing Credit Card payment: $" + amount);
}
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.PAYPAL)
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
حقن الفاصوليا المؤهلة
@ApplicationScoped
public class CheckoutService {
private final PaymentProcessor creditCardProcessor;
private final PaymentProcessor payPalProcessor;
@Inject
public CheckoutService(
@PaymentType(PaymentType.Mode.CREDIT_CARD) PaymentProcessor creditCardProcessor,
@PaymentType(PaymentType.Mode.PAYPAL) PaymentProcessor payPalProcessor) {
this.creditCardProcessor = creditCardProcessor;
this.payPalProcessor = payPalProcessor;
}
}
5. المنتجون والمتخلصون: حقن كائنات الطرف الثالث
تحتاج أحيانًا إلى إدخال فئات من مكتبات الجهات الخارجية (على سبيل المثال، عملاء AWS SDK، واتصالات Redis، ووحدات التكوين المخصصة) التي لم يتم إضافة تعليقات توضيحية إليها بـ @ApplicationScoped. يتعامل Quarkus مع هذا باستخدام طرق الإنتاج والمتخلصات.
package com.ghaznix.config;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ApplicationScoped
public class DatabaseConnectionProducer {
@Produces
@ApplicationScoped
public Connection createDatabaseConnection() throws SQLException {
return DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb", "user", "pass");
}
// Disposer method called automatically when bean is destroyed
public void closeConnection(@Disposes Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
6. اعتراضات CDI: البرمجة الموجهة نحو الجوانب (AOP)
تسمح أدوات الاعتراض بربط الاهتمامات الشاملة (التسجيل، ومسارات التدقيق، والمقاييس، وتحديد المعدل) بأساليب الفول دون تلويث منطق الأعمال.
الخطوة 1: تعريف ربط المعترض
package com.ghaznix.interceptors;
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface LogExecutionTime {
}
الخطوة الثانية: تنفيذ منطق الاعتراض
package com.ghaznix.interceptors;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LogExecutionTimeInterceptor {
@AroundInvoke
public Object logInvocation(InvocationContext context) throws Exception {
long startTime = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
System.out.printf("[AUDIT] Method %s executed in %d ms%n",
context.getMethod().getName(), duration);
}
}
}
الخطوة 3: قم بتعليق الفاصوليا المستهدفة
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
نظرًا لأن Quarkus ينشئ وكلاء اعتراضيين ثابتين في وقت الإنشاء، فإن حمل استدعاء الطريقة يكون صفرًا تقريبًا مقارنة بوكلاء وقت التشغيل الديناميكي CGLIB في Spring Boot.
7. بنية تعتمد على الأحداث مع أحداث CDI
يوفر CDI نشر الأحداث المتزامنة وغير المتزامنة للاتصالات المنفصلة أثناء العملية.
نشر الأحداث ومراقبتها
// 1. Define Event Payload
public record OrderCreatedEvent(String orderId, double totalAmount) {}
// 2. Publish Event
@ApplicationScoped
public class OrderService {
@Inject
Event<OrderCreatedEvent> orderEventPublisher;
public void createOrder(String orderId, double amount) {
// Create order logic...
orderEventPublisher.fire(new OrderCreatedEvent(orderId, amount));
}
}
// 3. Observe Event Synchronously or Asynchronously
@ApplicationScoped
public class InventoryObserver {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.orderId());
}
public void onOrderCreatedAsync(@ObservesAsync OrderCreatedEvent event) {
System.out.println("Async audit log for order: " + event.orderId());
}
}
أحداث دورة حياة التطبيق
يكشف Quarkus عن أحداث بدء التشغيل وإيقاف التشغيل لتنفيذ تعليمات برمجية للتهيئة عند تشغيل التطبيق أو إنهائه:
@ApplicationScoped
public class ApplicationLifecycleBean {
void onStart(@Observes StartupEvent ev) {
System.out.println("Quarkus Application starting up...");
}
void onStop(@Observes ShutdownEvent ev) {
System.out.println("Quarkus Application shutting down...");
}
}
8. البحث البرمجي عن الفول باستخدام Instance<T> وArc.container()
عندما يجب اختيار التطبيقات المستهدفة ديناميكيًا في وقت التشغيل (أو داخل مثيلات قديمة غير CDI)، استخدم البحث الثابت Instance<T> أو Quarkus Arc.container().
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. المقارنة المعمارية: Quarkus ArC vs Spring IoC vs Weld
| ميزة | كواركوس آر سي | حاوية الربيع IoC | لحام جاكرتا القياسي |
|---|---|---|---|
| ** حل التبعية ** | وقت البناء (AOT) | انعكاس وقت التشغيل | انعكاس وقت التشغيل |
| ** مساحة ذاكرة الوصول العشوائي الزائدة ** | منخفض للغاية (حوالي 0 ميجابايت للبيانات التعريفية) | عالية (ذاكرة التخزين المؤقت الديناميكية لفئة الوكيل) | معتدلة إلى عالية |
| وقت البدء | ميلي ثانية (<0.05 ثانية) | ثانية (2.5 ثانية - 8 ثانية) | ثانية (1.5 ثانية - 4 ثانية) |
| ** دعم GraalVM الأصلي ** | أصلي خارج الصندوق | يتطلب تلميحات الربيع الأصلية | التكوين المعقد |
| ** تقليم الفاصوليا غير المستخدمة ** | التقليم التلقائي في وقت البناء | أبقى في Classpath | أبقى في Classpath |
| ** حقن منشئ واحد ** | ضمني (لا حاجة إلى تعليق توضيحي) | المدعومة | يتطلب @Inject |
10. وحدة اختبار CDI Beans باستخدام @QuarkusTest و@InjectMock
يجعل Quarkus اختبار حبوب CDI أمرًا سلسًا باستخدام JUnit 5 وRestAssured. استخدم @InjectMock لاستبدال وحدات التبعية بنماذج Mockito تلقائيًا أثناء الاختبار.
package com.ghaznix;
import com.ghaznix.services.OrderProcessingService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class OrderServiceTest {
@Inject
OrderService orderService;
@InjectMock
OrderProcessingService orderProcessingService;
@Test
public void testOrderCheckout() {
Mockito.when(orderProcessingService.processOrder("ORD-101"))
.thenReturn("Mocked Success");
String result = orderService.checkout("ORD-101");
Assertions.assertEquals("Mocked Success", result);
}
}
11. الاستنتاج
يمثل حقن التبعية في Quarkus باستخدام CDI (ArC) تحولًا هائلاً في تصميم Java للمؤسسات. من خلال نقل مسح البيانات الوصفية للفاصوليا، وتوليد الوكيل، ودقة الرسم البياني للحقن إلى وقت التجميع، يوفر Quarkus سرعات بدء تشغيل تفوق سرعة الصوت، وآثار ذاكرة دون ذرية، و**دعمًا ثنائيًا أصليًا لـ GraalVM **.
سواء كنت تقوم بالترحيل من Spring Boot أو إنشاء خدمات صغيرة سحابية أصلية على Kubernetes، فإن إتقان Quarkus CDI يزودك بالأداء وإنتاجية المطورين المطلوبة لبنية البرامج الحديثة.
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.