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

Spring مقابل Java الخالصة

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

Spring مقابل Java الخالصة

بعد ثمانية دروس من بناء النموذج الذهني لـ Spring، يبرز سؤال مشروع: ماذا يمنحنا الحاوي فعليًا؟ تستطيع Java الخالصة إنشاء الكائنات واستدعاء الـ setters وتمرير الاعتماديات عبر المُنشئات — كل ما يفعله Spring ميكانيكيًا. يجيب هذا الدرس عن هذا السؤال بصورة ملموسة عبر المقارنة بين ربط التطبيق يدويًا وربطه بـ Spring، ثم تسمية كل ميزة وكل تكلفة مرتبطة بالحاوي.

الأساس: الربط اليدوي

تخيّل خدمة معالجة طلبات صغيرة. ثلاثة متعاونين: ProductRepository يقرأ من قاعدة البيانات، وInventoryService يتحقق من المخزون، وOrderService ينسّق كل شيء.

// Java الخالصة — أنت مسؤول عن كل سطر ربط public class Main { public static void main(String[] args) { // 1. بناء شجرة الاعتماديات يدويًا من الأسفل للأعلى DataSource ds = buildDataSource(); ProductRepository repo = new JdbcProductRepository(ds); InventoryService inv = new InventoryService(repo); OrderService orders = new OrderService(repo, inv); // 2. استخدام الكائن الجذر orders.placeOrder(42L, 3); } private static DataSource buildDataSource() { HikariConfig cfg = new HikariConfig(); cfg.setJdbcUrl(System.getenv("DB_URL")); cfg.setUsername(System.getenv("DB_USER")); cfg.setPassword(System.getenv("DB_PASS")); return new HikariDataSource(cfg); } }

يُترجَم هذا الكود ويعمل وقابل للاختبار بالكامل. لكن لاحظ ما أنت مسؤول عنه:

  • بناء شجرة الاعتماديات بالترتيب الصحيح.
  • إنشاء كل متعاون مرة واحدة بالضبط (أو تتبّع أيها singletons بنفسك).
  • إغلاق الموارد (HikariDataSource تُنفّذ AutoCloseable) عند الإيقاف.
  • إعادة الربط يدويًا في كل مرة تتغير فيها اعتمادية أو يُطلب نطاق جديد.

النسخة المربوطة بـ Spring

// Spring 6 — الحاوي يتولى الربط @Configuration @ComponentScan("com.example.shop") public class AppConfig {} @Repository public class JdbcProductRepository implements ProductRepository { private final JdbcTemplate jdbc; public JdbcProductRepository(JdbcTemplate jdbc) { this.jdbc = jdbc; // تُحقن بواسطة Spring } // ... } @Service public class InventoryService { private final ProductRepository repo; public InventoryService(ProductRepository repo) { this.repo = repo; // تُحقن بواسطة Spring } // ... } @Service public class OrderService { private final ProductRepository repo; private final InventoryService inv; public OrderService(ProductRepository repo, InventoryService inv) { this.repo = repo; this.inv = inv; } // ... } // نقطة الدخول public class Main { public static void main(String[] args) { try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) { OrderService orders = ctx.getBean(OrderService.class); orders.placeOrder(42L, 3); } // ctx.close() تُشغّل @PreDestroy على جميع الـ beans } }

منطق التطبيق متطابق تمامًا. ما تغيّر هو من يتحمّل مسؤولية الربط — وهذا التحوّل يحمل قائمة ملموسة من الفوائد.

الفائدة الأولى: حل الاعتماديات تلقائيًا

يقرأ Spring أنواع معاملات المُنشئ، ويجد الـ beans التي تُوفّيها، ويربط كل شيء. أضف اعتمادية جديدة إلى OrderService وعليك تحديث مُنشئ واحد فقط — لا سلسلة من الاستدعاءات اليدوية في Main. في نظام يضمّ عشرات الخدمات يُحدث هذا فارقًا هائلًا.

حقن المُنشئ هو المعيار الذهبي. يجعل الاعتماديات صريحة، يُبقي الـ beans غير قابلة للتعديل (حقول final)، ويُلغي الحاجة إلى @Autowired على الـ beans ذات المُنشئ الواحد (تُحقن تلقائيًا منذ Spring 4.3). حقن الحقل يُخفي الاعتماديات ويُعطّل قابلية الاختبار — تجنّبه.

الفائدة الثانية: النمط الفردي بلا حالة عامة

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

// singleton في Java الخالصة — النمط المضادّ الكلاسيكي public class ProductRepository { private static final ProductRepository INSTANCE = new ProductRepository(); private ProductRepository() {} public static ProductRepository getInstance() { return INSTANCE; } } // singleton في Spring — نظيف وقابل للحقن والاستبدال في الاختبارات @Repository public class JdbcProductRepository implements ProductRepository { ... } // Spring ينشئ نسخة واحدة بالضبط. أي اختبار يمكنه استبدالها بـ mock.

الفائدة الثالثة: إدارة دورة الحياة

الموارد التي تحتاج إلى إغلاق (تجمّعات قواعد البيانات، تجمّعات الخيوط، الملفات المفتوحة) عرضة للأخطاء في الكود المربوط يدويًا. تُعالج خطّافات دورة حياة Spring كل ذلك تلقائيًا:

@Component public class ReportScheduler { private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @PostConstruct public void start() { scheduler.scheduleAtFixedRate(this::runReport, 0, 1, TimeUnit.HOURS); } @PreDestroy public void stop() { scheduler.shutdown(); // تُستدعى تلقائيًا عند إغلاق الحاوي } }

بدون الحاوي عليك الربط يدويًا بإيقاف تشغيل JVM (Runtime.getRuntime().addShutdownHook(...)) وتذكّر فعل ذلك لكل فئة تمتلك موارد.

الفائدة الرابعة: الـ Beans ذات النطاق المحدد

كثيرًا ما تحتاج تطبيقات الويب إلى كائنات تعيش لطلب HTTP واحد فقط. تنفيذ ذلك يدويًا يستلزم متغيرات ThreadLocal وتنظيفًا دقيقًا وانضباطًا شديدًا. نطاق الطلب في Spring يُغلّف كل ذلك:

@Component @RequestScope // نسخة واحدة لكل طلب HTTP، تُدمَّر بعد الاستجابة public class RequestContext { private String currentUserId; // getters / setters } @Service public class OrderService { private final RequestContext ctx; // Spring يحقن proxy ذا نطاق public OrderService(RequestContext ctx) { this.ctx = ctx; } public void placeOrder(long productId, int qty) { String userId = ctx.getCurrentUserId(); // القيمة الصحيحة لهذا الطلب // ... } }

ينشئ الحاوي النسخة عند وصول الطلب ويُدمّرها عند إرسال الاستجابة. صفر من كود ThreadLocal في طبقة الخدمة.

الفائدة الخامسة: AOP — المخاوف المشتركة بلا كود مُكرَّر

تظهر عمليات التسجيل وإدارة المعاملات وفحوصات الأمان في عشرات الدوال. في Java الخالصة تنسخ ذلك الكود المتكرر أو تكتب غلافًا لكل فئة. يُطبّق نموذج AOP proxy في Spring الجوانب بشفافية تامة:

// Java الخالصة — كود المعاملة يتكرر في كل مكان public void placeOrder(long productId, int quantity) { Connection conn = dataSource.getConnection(); conn.setAutoCommit(false); try { repo.deductStock(conn, productId, quantity); paymentService.charge(conn, customerId, total); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.close(); } } // Spring — أعلن عن النية، Spring يتولى التفاصيل @Transactional public void placeOrder(long productId, int quantity) { repo.deductStock(productId, quantity); paymentService.charge(customerId, total); // إذا رمت أيٌّ منهما RuntimeException، يُلغي Spring المعاملة تلقائيًا }

بدون Spring تكتب كتلة try/commit/catch/rollback في كل مكان تُحتاج فيه المعاملات — وانسَها مرة واحدة فقط، وستحصل على خطأ في اتساق البيانات في الإنتاج.

المقايضات الحقيقية

Spring ليس مجانيًا. اعرف التكاليف قبل أن تلتزم به:

  • وقت البدء: يفحص الحاوي الفئات ويُنشئ الـ proxies ويحلّ الشجرة عند الإطلاق. يمكن أن يستغرق تطبيق Spring Boot الكبير من 10 إلى 30 ثانية للبدء، وهو ما يُهمّ في أدوات سطر الأوامر والدوال الخادوم-دون. Java الخالصة تبدأ في ميلّي ثوانٍ.
  • السحر الضمني: حين يكون الربط غير مرئي، يستلزم تصحيح bean مفقود أو اعتمادية دائرية فهم الحاوي من الداخل. الربط اليدوي شفّاف تمامًا — تشير الأخطاء مباشرة إلى استدعاء المُنشئ الفاشل.
  • حجم classpath: يُضيف النظام البيئي لـ Spring عدة ميغابايت من ملفات JAR. تافهة لخدمة backend، ملحوظة لبيئات مضمّنة أو محدودة.
  • منحنى التعلم: يجب على المطوّرين فهم النطاقات وأوضاع الـ proxy وسياق التطبيق قبل أن يتمكّنوا من تشخيص المشكلات بثقة.
القاعدة العملية الصحيحة: إذا كان تطبيقك يضمّ أكثر من حفنة من الخدمات المتعاونة، أو يحتاج إلى إدارة المعاملات أو تكامل الأمان أو طبقة ويب، فإن Spring يُعوّض تكلفته فورًا. أما للسكريبت ذي الغرض الواحد أو مكتبة مُعدّة للتضمين، فإن Java الخالصة (أو إطار عمل صغير) هي الخيار الأفضل.

متى تكفي Java الخالصة

  • أدوات سطر الأوامر ذات نقطة دخول واحدة واعتماديتين أو ثلاث.
  • كود مكتبة JAR — لا تُجبر الحاوي أبدًا على مستخدمي مكتبتك.
  • المسارات الحساسة لوقت البدء حيث تُهمّ زمن الإطلاق البارد (دوال AWS Lambda وأدوات CLI التي تُستدعى بتكرار).
  • الكود الذي تهمّ فيه الشفافية الكاملة أكثر من الراحة (الوحدات الحرجة أمنيًا، برامج التشغيل منخفضة المستوى).
لا تُضف Spring هربًا من تصميم كائني سيئ. إذا كانت فئاتك مُتشابكة ويصعب ربطها يدويًا، فإن الحاوي يُخفي الألم لكنه لا يُصلحه. أعد هيكلة التصميم أولًا؛ عندئذٍ يصبح إضافة Spring مُضاعِفًا لكود نظيف بالفعل، لا ضمادةً على كود مُتشابك.

الخلاصة

يُبرّر حاوي IoC في Spring وجوده بأتمتة أربعة أشياء كنت ستتولّاها يدويًا: حل شجرة الاعتماديات، دورة حياة النمط الفردي، تنظيف الموارد، والمخاوف المشتركة عبر AOP. تبقى Java الخالصة الأداة الصحيحة للبرامج الصغيرة والمستقلة حيث تُهمّ سرعة البدء أو الشفافية أو النشر بدون اعتماديات أكثر من تلك الفوائد. للتطبيقات الاحترافية على جانب الخادم — أي شيء يحوي قاعدة بيانات أو حدودًا للمعاملات أو أكثر من حفنة من الخدمات المتعاونة — تفوق فوائد الحاوي تكاليفه بفارق كبير. مع فهم هذه المقايضة بالكامل، يضع الدرس الأخير كل شيء معًا في مشروع ربط متكامل.