مشروع: توصيل تطبيق عبر حاوية IoC
طوال هذا البرنامج التعليمي استكشفت أساليب ضبط Spring وفحص المكوّنات والمؤهّلات ودورة حياة ApplicationContext. الآن ستطبّق كل ذلك في مشروع صغير متكامل: تطبيق طبقي مصغّر تُدير الحاوية كل تبعياته — دون استخدام new في أي مكان خارج نقطة الدخول.
ما الذي ستبنيه
نظام إدارة مكتبة بثلاث طبقات:
- طبقة المستودع —
BookRepository: تحفظ الكتب وتسترجعها (خريطة داخل الذاكرة للتبسيط).
- طبقة الخدمة —
BookService: قواعد العمل (إضافة وبحث وعرض قائمة).
- طبقة العرض —
LibraryApp: نقطة الدخول التي تُشغّل حالة الاستخدام.
ستوصّل كل شيء باستخدام الضبط المستند إلى Java (@Configuration + @Bean) في فئة واحدة وحقن الاعتمادات المدفوع بالتوصيفات التعريفية (@Autowired / الحقن عبر المنشئ) في المكوّنات. هذا هو النمط المُستخدَم في تطبيقات Spring الحقيقية قبل أن يتولى الضبط التلقائي في Spring Boot مهمته.
لماذا لا يُستخدم Spring Boot هنا؟ Spring Boot نقطة انطلاق قوية، لكن فهم كيفية توصيل التطبيق بدونه هو ما يُفرّق بين المطوّرين القادرين على تشخيص أخطاء السياق وأولئك غير القادرين. سحر Boot ليس إلا هذا الكود يُولَّد بدلًا عنك.
هيكل المشروع
src/main/java/com/example/library/
├── AppConfig.java // فئة @Configuration
├── model/
│ └── Book.java
├── repository/
│ ├── BookRepository.java // الواجهة
│ └── InMemoryBookRepository.java
├── service/
│ ├── BookService.java // الواجهة
│ └── BookServiceImpl.java
└── LibraryApp.java // main()
الخطوة 1 — نموذج المجال
package com.example.library.model;
public class Book {
private final String isbn;
private final String title;
private final String author;
public Book(String isbn, String title, String author) {
this.isbn = isbn;
this.title = title;
this.author = author;
}
public String getIsbn() { return isbn; }
public String getTitle() { return title; }
public String getAuthor() { return author; }
@Override
public String toString() {
return "[" + isbn + "] " + title + " by " + author;
}
}
الخطوة 2 — طبقة المستودع
حدّد العقد كواجهة ثم أمدّ بتنفيذ واحد. ستحقن Spring التنفيذ أينما أُعلنت الواجهة كاعتمادية — المُستدعي لا يعلم أو يهتم بالفئة الملموسة التي يستلمها.
package com.example.library.repository;
import com.example.library.model.Book;
import java.util.List;
import java.util.Optional;
public interface BookRepository {
void save(Book book);
Optional<Book> findByIsbn(String isbn);
List<Book> findAll();
}
package com.example.library.repository;
import com.example.library.model.Book;
import java.util.*;
public class InMemoryBookRepository implements BookRepository {
private final Map<String, Book> store = new LinkedHashMap<>();
@Override
public void save(Book book) {
store.put(book.getIsbn(), book);
}
@Override
public Optional<Book> findByIsbn(String isbn) {
return Optional.ofNullable(store.get(isbn));
}
@Override
public List<Book> findAll() {
return List.copyOf(store.values());
}
}
الخطوة 3 — طبقة الخدمة
تُغلّف الخدمة قواعد العمل وتعتمد على BookRepository عبر الحقن بالمنشئ — الأسلوب الموصى به لأنه يجعل الاعتماديات صريحة ويُتيح الحقول النهائية.
package com.example.library.service;
import com.example.library.model.Book;
import java.util.List;
import java.util.Optional;
public interface BookService {
void addBook(String isbn, String title, String author);
Optional<Book> findBook(String isbn);
List<Book> listAllBooks();
}
package com.example.library.service;
import com.example.library.model.Book;
import com.example.library.repository.BookRepository;
import java.util.List;
import java.util.Optional;
public class BookServiceImpl implements BookService {
private final BookRepository repository;
// تحقن Spring اعتمادية BookRepository هنا عند الإنشاء
public BookServiceImpl(BookRepository repository) {
this.repository = repository;
}
@Override
public void addBook(String isbn, String title, String author) {
if (isbn == null || isbn.isBlank()) {
throw new IllegalArgumentException("ISBN must not be blank");
}
repository.save(new Book(isbn, title, author));
}
@Override
public Optional<Book> findBook(String isbn) {
return repository.findByIsbn(isbn);
}
@Override
public List<Book> listAllBooks() {
return repository.findAll();
}
}
برمج للواجهات لا للتطبيقات. يعتمد BookServiceImpl على BookRepository (الواجهة) لا على InMemoryBookRepository. الاستبدال لاحقًا بتنفيذ مدعوم بـ JPA يتطلب تغيير سطر واحد فقط في فئة الضبط — بقية قاعدة الكود تبقى دون تعديل.
الخطوة 4 — فئة الضبط
تقوم فئة @Configuration الواحدة بتوصيل رسم بياني كامل للكائنات. تستدعي Spring توابع @Bean وتُدير الكائنات المُعادة كفاصوليا مفردة (singleton) بشكل افتراضي.
package com.example.library;
import com.example.library.repository.BookRepository;
import com.example.library.repository.InMemoryBookRepository;
import com.example.library.service.BookService;
import com.example.library.service.BookServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public BookRepository bookRepository() {
return new InMemoryBookRepository();
}
@Bean
public BookService bookService(BookRepository bookRepository) {
// تحلّ Spring فاصوليا BookRepository وتمررها هنا
return new BookServiceImpl(bookRepository);
}
}
لاحظ أن bookService تستقبل BookRepository كمعامل تابع — تطابقها Spring بالنوع مع فاصوليا bookRepository(). هذا حقن تابع المصنع، مطابق في الأثر للحقن بالمنشئ لكنه يُعبَّر عنه في كود الضبط لا في المكوّن نفسه.
الخطوة 5 — نقطة الدخول
package com.example.library;
import com.example.library.service.BookService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class LibraryApp {
public static void main(String[] args) {
// تهيئة الحاوية بفئة الضبط
ApplicationContext ctx =
new AnnotationConfigApplicationContext(AppConfig.class);
// استرجاع فاصوليا BookService بنوع واجهتها
BookService service = ctx.getBean(BookService.class);
// تشغيل التطبيق
service.addBook("978-0-13-468599-1", "Effective Java", "Joshua Bloch");
service.addBook("978-0-13-235088-4", "Clean Code", "Robert C. Martin");
service.addBook("978-0-13-110362-7", "The C Programming Language", "Kernighan & Ritchie");
System.out.println("All books:");
service.listAllBooks().forEach(System.out::println);
System.out.println("\nLookup by ISBN:");
service.findBook("978-0-13-468599-1")
.ifPresentOrElse(System.out::println,
() -> System.out.println("Not found"));
// أغلق السياق دائمًا لتشغيل استدعاءات @PreDestroy
((AnnotationConfigApplicationContext) ctx).close();
}
}
تشغيل المشروع
أضف اعتمادية Spring Context إلى ملف البناء ثم شغّل LibraryApp:
<!-- Maven pom.xml -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.6</version>
</dependency>
المخرجات المتوقعة:
All books:
[978-0-13-468599-1] Effective Java by Joshua Bloch
[978-0-13-235088-4] Clean Code by Robert C. Martin
[978-0-13-110362-7] The C Programming Language by Kernighan & Ritchie
Lookup by ISBN:
[978-0-13-468599-1] Effective Java by Joshua Bloch
ما تفعله Spring خلف الكواليس
- يقرأ
AnnotationConfigApplicationContext الفئة AppConfig.class ويبني سجل تعريفات الفاصوليا.
- يُنشئ
InMemoryBookRepository أولًا (لا اعتماديات)، ويسجّله كمفرد باسم bookRepository.
- يُنشئ
BookServiceImpl مُحلًّا معامل BookRepository من السجل، ويسجّله كمفرد باسم bookService.
- يسترجع التابع
main فاصوليا BookService — تُعيد Spring المفرد المُنشأ مسبقًا، مُحقَنًا وجاهزًا.
أغلق السياق دائمًا في التطبيقات المستقلة. استدعاء ctx.close() يُشغّل استدعاءات @PreDestroy ويحرّر الموارد (تجمّعات الخيوط والاتصالات والملفات). في تطبيقات الويب تُدير الحاوية هذا تلقائيًا، لكن في التابع main العادي يجب أن تفعله بنفسك وإلا ستتسرّب الموارد.
توسيع المشروع — أشياء يمكنك تجربتها
- أضف تنفيذًا ثانيًا للمستودع — مثل
JdbcBookRepository — وانتقل بينهما بتغيير تابع @Bean واحد فقط. لا تعديلات في أي فئة أخرى.
- استخدم
@Profile لتفعيل المستودع المقيم في الذاكرة في الاختبارات والمستودع المدعوم بـ JDBC في الإنتاج.
- انتقل إلى فحص المكوّنات — وصّف التنفيذات بـ
@Repository و@Service، احذف توابع @Bean الصريحة، وأضف @ComponentScan إلى AppConfig. التوصيل يعمل بشكل متطابق.
- أضف
@PostConstruct لإعداد المستودع مسبقًا ببيانات تجريبية ولاحظ متى يُشغَّل نسبةً إلى الحقن.
الخلاصة
بنيت تطبيقًا طبقيًا موصّلًا بالكامل بواسطة Spring دون استخدام new يدويًا في كود الأعمال. الحاوية تمتلك الرسم البياني للكائنات: تُنشئها وتحقنها وتُدير دورة حياتها. تُفصل الواجهات الطبقات عن بعضها حتى يمكن استبدال التطبيقات بأدنى تغيير في الكود. هذا هو بالضبط المعمار الذي تستخدمه تطبيقات Spring Boot تحت الغطاء — فهمه على هذا المستوى يعني أنك تستطيع ضبط أي مشروع Spring وتشخيصه وتوسيعه بثقة تامة.