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

الـ Beans وتعريفاتها

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

الـ Beans وتعريفاتها

في Spring، أي كائن تديره حاوية IoC يُسمّى bean. هذا هو التعريف بأكمله — غير أنّ تداعياته عميقة. حين تُسلّم كائنًا إلى Spring ليديره، تمتلك Spring دورة حياته الكاملة: تُنشئه، وتصل تبعياته، وتطبّق أي معالجة لاحقة عليه، ثم تُتلفه عند إغلاق التطبيق. إنّ فهم ما يجعل شيئًا ما bean، وكيف تُوصَف الـ beans للحاوية، وما يحدث لها في وقت التشغيل هو أساس كل تطبيق Spring ستكتبه.

ما هو الـ Bean تحديدًا؟

الـ bean هو ببساطة كائن Java تُنشئه ApplicationContext الخاصة بـ Spring وتضبطه وتديره. لا يوجد شيء خاص في الفئة نفسها — لا تحتاج إلى تنفيذ واجهة Spring أو توسيع فئة أساسية فيها. UserRepository أو EmailService أو Clock مخصص — أي منها يمكن أن يكون bean. الفرق بين الـ bean والكائن العادي هو من يُنشئه: Spring تفعل ذلك، لا كودك أنت.

لماذا يهم هذا؟ حين تُنشئ Spring كائنًا، يمكنها حقن تبعياته تلقائيًا، وتطبيق الاهتمامات المشتركة (المعاملات، الأمان، التخزين المؤقت) عبر وكلاء (proxies)، وضمان وجود نسخة واحدة فقط عبر التطبيق — دون أن تكتب سطرًا واحدًا من كود نمط Singleton.

تعريف الـ Bean — مخطط Spring

قبل أن تُنشئ Spring bean، تحتاج إلى تعريف bean: سجل بيانات وصفية يصف فئة الـ bean ونطاقه وتبعياته وأي دوال تهيئة أو إتلاف وتفاصيل تهيئة أخرى. نادرًا ما تتعامل مع تعريفات الـ bean مباشرةً — فأنت تُعبّر عنها عبر التعليقات التوضيحية (annotations) أو إعداد Java، وتُترجمها Spring داخليًا إلى كائنات BeanDefinition. لكن معرفة المعلومات التي يحتوي عليها تعريف الـ bean يساعدك على فهم كل خيار إعداد تعرضه Spring.

يلتقط تعريف الـ bean:

  • الفئة — فئة Java التي يجب إنشاؤها.
  • الاسم / المعرّف — المعرّف المستخدم للبحث عن الـ bean.
  • النطاقsingleton (نسخة واحدة مشتركة، وهو الافتراضي) أو prototype (نسخة جديدة عند كل طلب)، من بين أنواع أخرى.
  • وسيطات المُنشئ وقيم الخصائص — ما يجب حقنه.
  • استدعاءات التهيئة والإتلاف — الدوال التي تُستدعى بعد الربط وقبل الإغلاق.
  • علامة التهيئة الكسولة — هل تُنشئ الـ bean فور بدء تشغيل السياق أم عند أول استخدام فقط.

تعريف Beans باستخدام @Bean داخل فئة @Configuration

الطريقة الأكثر صراحةً لتعريف الـ bean هي دالة @Bean داخل فئة @Configuration. تستدعي Spring هذه الدالة مرة واحدة (للـ beans ذات نطاق singleton)، وتخزّن الكائن المُعاد، وتسجّله باسم الدالة كاسم للـ bean.

import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { // اسم الـ bean: "userRepository" @Bean public UserRepository userRepository() { return new JdbcUserRepository(dataSource()); } // اسم الـ bean: "dataSource" @Bean public javax.sql.DataSource dataSource() { var cfg = new com.zaxxer.hikari.HikariConfig(); cfg.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb"); cfg.setUsername(System.getenv("DB_USER")); cfg.setPassword(System.getenv("DB_PASS")); return new com.zaxxer.hikari.HikariDataSource(cfg); } // اسم الـ bean: "userService" — يعتمد على userRepository @Bean public UserService userService() { // تعترض Spring هذا الاستدعاء وتُعيد كائن الـ singleton الموجود بالفعل return new UserService(userRepository()); } }
سحر وكيل CGLIB: حين تستدعي userService() دالةَ userRepository()، قد تتوقع إنشاء JdbcUserRepository ثانٍ. لكن هذا لا يحدث. تُنشئ Spring فئةً فرعية من فئة @Configuration باستخدام CGLIB وتعترض استدعاءات الدوال بين الـ beans، مُعيدةً كائن الـ singleton المُنشأ بالفعل. لهذا يجب ألا تكون فئات @Configuration نهائية (final).

تعريف Beans باستخدام @Component (مدفوع بالتعليقات التوضيحية)

لفئاتك الخاصة، يكون وسم الفئة بـ @Component (أو أحد تصنيفاتها الفرعية) وتفعيل فحص المكونات عادةً أكثر إيجازًا. تفحص Spring الحزم المحددة، وتجد الفئات المُوسَمة، وتُسجّل تعريف bean لكل منها تلقائيًا.

import org.springframework.stereotype.Repository; @Repository // تصنيف فرعي من @Component — يُفعّل أيضًا ترجمة استثناءات الثبات public class JdbcUserRepository implements UserRepository { private final javax.sql.DataSource dataSource; // ترى Spring مُنشئًا واحدًا: تحقن DataSource bean تلقائيًا public JdbcUserRepository(javax.sql.DataSource dataSource) { this.dataSource = dataSource; } @Override public User findById(long id) { // ... منطق JDBC return null; } }
import org.springframework.stereotype.Service; @Service // تصنيف فرعي من @Component — يُشير إلى نية طبقة الأعمال public class UserService { private final UserRepository repo; public UserService(UserRepository repo) { this.repo = repo; } public User getUser(long id) { return repo.findById(id); } }

مع تفعيل فحص المكونات (يُغطّى بالتفصيل في الدرس السادس)، تُصبح الفئتان أعلاه beans تلقائيًا. يكتشف الحاوية نمط المُنشئ الفردي ويحقن التبعية المتطابقة دون أي تعليمات ربط صريحة.

نطاق الـ Bean: Singleton مقابل Prototype

يتحكم النطاق في عدد النسخ التي تحتفظ بها الحاوية ومتى تُنشأ.

  • Singleton (الافتراضي): نسخة مشتركة واحدة لكل حاوية. تُنشأ عند بدء التشغيل (ما لم تكن كسولة)، وتُعاد استخدامها لكل نقطة حقن. مناسبة للخدمات عديمة الحالة والمستودعات ومعظم مكونات التطبيق.
  • Prototype: تُنشأ نسخة جديدة في كل مرة يُطلب فيها الـ bean أو يُحقَن. مناسبة للكائنات ذات الحالة (stateful) وغير الآمنة للخيوط التي يجب ألا تُشارَك.
import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @Component @Scope("prototype") // أو: @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class ReportBuilder { private final List<String> lines = new ArrayList<>(); public void addLine(String line) { lines.add(line); } public String build() { return String.join("\n", lines); } }
Prototype مُحقَن داخل Singleton — فخ كلاسيكي: إذا احتفظ bean من نوع singleton بمرجع إلى bean من نوع prototype، فإنه يستقبل نسخة prototype واحدة عند بدء التشغيل ويحتفظ بها إلى الأبد — مما يُبطل الغرض من نطاق prototype. استخدم ApplicationContext.getBean() أو Provider<ReportBuilder> لطلب نسخة جديدة في كل مرة.

استدعاءات دورة الحياة: @PostConstruct و@PreDestroy

يمكن لـ Spring استدعاء دوال على الـ bean بعد حقن جميع التبعيات وقبل إغلاق السياق. هذه هي الخطاطيف القياسية لتهيئة الموارد (فتح تجمّع اتصالات، تسخين ذاكرة تخزين مؤقت) وتحريرها (إغلاق الاتصالات، تفريغ المخازن المؤقتة).

import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; import org.springframework.stereotype.Component; @Component public class CacheWarmer { private final ProductRepository repo; public CacheWarmer(ProductRepository repo) { this.repo = repo; } @PostConstruct public void warmUp() { // تُستدعى بعد الحقن، قبل أن يعالج التطبيق أي طلبات repo.findAll().forEach(this::cache); System.out.println("تمّ تسخين الذاكرة المؤقتة"); } @PreDestroy public void shutdown() { // تُستدعى عند إغلاق ApplicationContext System.out.println("تفريغ الذاكرة المؤقتة قبل الإغلاق"); } private void cache(Object p) { /* ... */ } }
jakarta لا javax: منذ Spring 6 (وSpring Boot 3)، انتقلت التعليقات التوضيحية من javax.annotation.* إلى jakarta.annotation.*. استخدم دائمًا استيراد jakarta في المشاريع الجديدة.

التهيئة الكسولة

بشكل افتراضي، تُنشأ الـ beans ذات نطاق singleton بحرص — عند بدء تشغيل السياق. يكتشف هذا أخطاء الإعداد فورًا (تبعية bean مفقودة تفشل بسرعة). في بعض الأحيان تريد إنشاء الـ bean عند أول استخدام فقط، مثلًا إذا كانت تتصل بخدمة خارجية قد لا تكون متاحة عند بدء التشغيل. أضف @Lazy:

import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; @Lazy @Service public class ExternalPaymentClient { // تهيئة مكلفة — تعمل فقط عند أول حقن أو بحث عن هذا الـ bean public ExternalPaymentClient() { System.out.println("الاتصال ببوابة الدفع..."); } }

في التطبيقات الكبيرة يمكنك جعل الكسل افتراضيًا لجميع الـ beans عبر @ComponentScan(lazyInit = true) أو spring.main.lazy-initialization=true في application.properties، مما يُحسّن وقت البدء على حساب تأجيل الأخطاء من وقت البدء إلى وقت الاستخدام الأول.

الخلاصة

الـ bean في Spring هو أي كائن تُدير دورة حياته حاوية IoC. يستند كل bean إلى تعريف bean — مخطط يُحدد الفئة والنطاق والتبعيات واستدعاءات دورة الحياة. تُعبّر عن تعريفات الـ bean إما صراحةً عبر دوال @Bean في فئات @Configuration، أو ضمنيًا بوسم فئاتك الخاصة بـ @Component (أو تصنيف فرعي) وتفعيل فحص المكونات. نطاق singleton الافتراضي صحيح للمتعاونين عديمي الحالة؛ ونطاق prototype موجود للكائنات ذات الحالة بطبيعتها. تمنحك خطاطيف دورة الحياة (@PostConstruct، @PreDestroy) نقاط دخول وخروج نظيفة لإدارة الموارد. كل ميزة أخرى في Spring — حقن التبعيات، والبرمجة الموجهة بالجوانب (AOP)، والمعاملات — تبني على نموذج الـ bean هذا.