إطار Spring وحاوية IoC

ApplicationContext في العمل التطبيقي

18 دقيقة الدرس 7 من 13

ApplicationContext في العمل التطبيقي

قرأت عن حاوية IoC وتعريفات الـ beans بصورة نظرية. حان الآن وقت كتابة كود حقيقي: تشغيل ApplicationContext وسحب الـ beans منه وفهم أي تنفيذ ملموس تختار في كل موقف. يتركز هذا الدرس على الشيئين اللذين يقوم بهما المطورون يوميًا مع الحاوية — اختيار فئة ApplicationContext المناسبة واسترجاع الـ beans بشكل صحيح.

التطبيقات الثلاثة الأساسية لـ ApplicationContext

يأتي Spring مزودًا بعدة تطبيقات لـ ApplicationContext. ستصطدم بثلاثة منها باستمرار:

  • AnnotationConfigApplicationContext — الخيار المعياري لتطبيقات Java المستقلة واختبارات التكامل وأي مشروع يُهيّئ Spring بفئات @Configuration و/أو فحص المكونات. لا يعتمد على حاوية servlet أو مورد على مسار الفئات.
  • ClassPathXmlApplicationContext — يحمّل تعريفات beans بصيغة XML من مسار الفئات. ستراه أساسًا في قواعد الكود القديمة. فهمه يساعدك على قراءة المشاريع القديمة وترحيلها.
  • GenericWebApplicationContext / AnnotationConfigWebApplicationContext — يُستخدم داخل حاوية Servlet (Tomcat أو Jetty). ينشئ DispatcherServlet الخاص بـ Spring MVC أحد هذين تلقائيًا؛ نادرًا ما تبنيه بنفسك.
ملاحظة Spring Boot: عند استخدام Spring Boot، تُشغّل SpringApplication.run() كائن AnnotationConfigServletWebServerApplicationContext (أو النسخة التفاعلية) خلف الكواليس. لن تستدعي المنشئ بنفسك، لكن يمكنك تحويل الـ ConfigurableApplicationContext المُعاد لفحصه. كل ما يرد هنا ينطبق مباشرةً.

تشغيل السياق: AnnotationConfigApplicationContext

أبسط طريقة لبدء حاوية Spring هي تمرير فئة @Configuration الخاصة بك إلى المنشئ:

import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { // تشغيل الحاوية — تسجيل الـ beans وتنفيذ @PostConstruct وحل التبعيات try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) { OrderService orders = ctx.getBean(OrderService.class); orders.placeOrder("item-42", 3); } // ctx.close() تُشغّل @PreDestroy وتُدمّر الـ beans المفردة } }

يقبل المنشئ فئة @Configuration واحدة أو أكثر، أو يمكنك تمرير سلسلة اسم الحزمة الأساسية لتفعيل فحص المكونات:

// فحص كل شيء تحت com.example AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.example");

استخدم try-with-resources متى ما تحكمت في دورة الحياة. ApplicationContext يُوسّع Closeable، لذا تستدعي كتلة try الـ close() تلقائيًا، مما يُشغّل الإيقاف السلس لجميع الـ beans المفردة.

استرجاع الـ Beans: ثلاثة أشكال من getBean()

تُعرض واجهة BeanFactory (التي تُوسّعها ApplicationContext) عدة أشكال مُحمَّلة. معرفة متى يُناسب كل شكل تتجنّب أخطاء يصعب اكتشافها:

// 1. بالنوع فقط — مُفضَّل في معظم الحالات OrderService svc = ctx.getBean(OrderService.class); // 2. بالاسم فقط — يُعيد Object ويستلزم تحويلًا غير آمن OrderService svc2 = (OrderService) ctx.getBean("orderService"); // 3. بالاسم والنوع معًا — البديل الآمن للشكل الثاني OrderService svc3 = ctx.getBean("orderService", OrderService.class);
فضّل الاسترجاع بالنوع. إنه الشكل الأكثر إيجازًا وأمانًا من ناحية النوع، وهو ما يستخدمه Spring داخليًا. الاسترجاع بالاسم مفيد فقط حين يكون لديك عدة beans من النوع ذاته مُسجَّلة بأسماء مختلفة، أو حين تكتب كود مستوى إطار العمل الذي لا يعرف النوع في وقت الترجمة.

ما يحدث عند استدعاء getBean()

لـ bean مفرد (singleton) — النطاق الافتراضي — يُعيد Spring نفس المثيل المهيّأ بالكامل في كل مرة. لا توجد تكلفة أداء لاستدعاء getBean() مرارًا — إنها مجرد بحث في خريطة بعد التهيئة الأولى:

OrderService a = ctx.getBean(OrderService.class); OrderService b = ctx.getBean(OrderService.class); System.out.println(a == b); // true — نفس المثيل

لـ bean من نوع prototype، يُنشئ Spring مثيلًا جديدًا في كل استدعاء لـ getBean(). لا يُخزَّن هذا المثيل مؤقتًا وتنتهي إدارة دورة حياته بعد الإنشاء — فأنت المسؤول عن استدعاء أي منطق تنظيف بنفسك.

لا تستدعِ getBean() داخل beans الخاصة بك. سحب الـ beans مباشرةً من السياق داخل كود التطبيق يُسمّى نمط خدمة الموقع المضاد (Service Locator anti-pattern). فهو يُقيّد فئتك بـ Spring API ويُخفي التبعية ويُعطّل قابلية الاختبار. أعلن دائمًا عن التبعيات كمعاملات في المنشئ ودع Spring يحقنها. الأماكن الوحيدة المشروعة لاستدعاء getBean() هي نقاط دخول التطبيق (مثل main()) وكود تهيئة إطار العمل والاختبارات.

سرد الـ Beans وفحصها

تُوفّر ApplicationContext عدة توابع للفحص قيّمة في التصحيح والأدوات:

// جميع أسماء الـ beans المُسجَّلة في السياق String[] names = ctx.getBeanDefinitionNames(); System.out.println("Total beans: " + names.length); // التحقق من وجود bean من نوع معين boolean hasRepo = ctx.getBeanNamesForType(UserRepository.class).length > 0; // الحصول على مرشح الـ autowire عند معرفة النوع UserRepository repo = ctx.getBean(UserRepository.class); // هل هذا الاسم مفرد (singleton)؟ System.out.println(ctx.isSingleton("userRepository")); // true System.out.println(ctx.isPrototype("userRepository")); // false

يمكنك أيضًا التحقق مما إذا كان الـ bean قد هُيّئ أو الاستعلام عن نوعه دون تشغيل التهيئة باستخدام ctx.getType("beanName"). هذه أدوات قوية خلال التصحيح — ضعها في CommandLineRunner مؤقت في تطبيق Spring Boot لفحص السياق الحي.

مثال عملي واقعي

إليك إعداد مدمج لكنه واقعي — أداة سطر أوامر لمعالجة الطلبات — يُوضّح كيف تتفاعل التشغيل والاسترجاع والإغلاق:

// AppConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public InventoryRepository inventoryRepository() { return new InMemoryInventoryRepository(); } @Bean public OrderService orderService(InventoryRepository repo) { return new OrderService(repo); // حقن بالمنشئ } } // OrderService.java public class OrderService { private final InventoryRepository repo; public OrderService(InventoryRepository repo) { this.repo = repo; } public void placeOrder(String sku, int qty) { if (repo.reserve(sku, qty)) { System.out.println("Order placed: " + qty + "x " + sku); } else { System.out.println("Insufficient stock for: " + sku); } } } // Main.java import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) { OrderService orders = ctx.getBean(OrderService.class); orders.placeOrder("SKU-100", 2); orders.placeOrder("SKU-999", 500); } } }

لاحظ أن main() تستدعي getBean() مرة واحدة فقط — للحصول على الخدمة الجذرية. كل شيء آخر يتدفق عبر حقن المنشئ. هذا هو النمط الصحيح.

استخدام ClassPathXmlApplicationContext (للقراءة في الكود القديم)

إن ورثت قاعدة كود من عصر ما قبل Spring 3 أو احتجت إلى استخدام ملف إعداد XML قديم، فكود التشغيل يبدو شبه متطابق:

import org.springframework.context.support.ClassPathXmlApplicationContext; try (var ctx = new ClassPathXmlApplicationContext("applicationContext.xml")) { DataSource ds = ctx.getBean("dataSource", DataSource.class); // ... }

يقع ملف XML على مسار الفئات (مثل src/main/resources/applicationContext.xml). الواجهة البرمجية لاسترجاع الـ beans متطابقة — فقط سطر التشغيل يتغير. عند تحديث مثل هذا المشروع، تكون خطوتك الأولى عادةً استبدال هذا السطر بـ AnnotationConfigApplicationContext بعد ترحيل الـ beans إلى فئات @Configuration.

الخلاصة

تُعدّ ApplicationContext الحاوية التي تتعامل معها يوميًا، حتى وإن كان Spring Boot يُخفيها خلف SpringApplication.run(). اعلم أن AnnotationConfigApplicationContext هو خيارك الافتراضي للتطبيقات المبنية على التعليقات التوضيحية خارج Boot؛ وأنك تسترجع الـ beans بالنوع في المقام الأول؛ وأن استدعاءات getBean() المباشرة تنتمي فقط إلى نقطة الدخول الخارجية. تبني الدروس التالية على هذا الأساس بفحص التسمية والمحددات وكيفية التعامل مع عدة beans من النوع ذاته.