مقدمة: لماذا المستودع الأحادي في مشاريع Java؟
مع تزايد تعقيد تطبيقات Java، غالبًا ما تواجه الفرق معضلة: تخزين كل الكود في مستودع واحد (مستودع أحادي) أو تقسيمه إلى العديد من مستودعات الخدمات المصغرة (مستودع متعدد). تُظهر الممارسة أن المستودع الأحادي فعال بشكل خاص للمشاريع ذات منطق الأعمال المشترك والمكتبات والإعدادات. في Java، يسمح بإدارة التبعيات والإصدارات والبناء بشكل مركزي، مما يبسط إعادة الهيكلة والتكامل.
في هذه المقالة، سنشرح كيفية تنظيم كود Java في مستودع أحادي عمليًا: من هيكل المجلدات إلى إعداد CI/CD. ستتعلم كيفية تجنب الأخطاء الشائعة وتسريع التطوير.
1. هيكل المستودع الأحادي لـ Java
المبدأ الأساسي للمستودع الأحادي هو النمطية. في Java، يتم تحقيق ذلك من خلال مشاريع متعددة الوحدات (multi-module project). كل وحدة مسؤولة عن مجال مسؤوليتها: المكتبات المشتركة، الخدمات، الأدوات المساعدة.
مثال على هيكل نموذجي:
my-java-monorepo/├── build.gradle # ملف البناء الجذر├── settings.gradle # يربط جميع الوحدات├── gradle.properties # إصدارات التبعيات المشتركة├── common-lib/ # مكتبة مشتركة (DTO، أدوات مساعدة)│ ├── build.gradle│ └── src/├── service-auth/ # خدمة المصادقة│ ├── build.gradle│ └── src/├── service-payment/ # خدمة الدفع│ ├── build.gradle│ └── src/└── integration-tests/ # اختبارات التكامل ├── build.gradle └── src/لإدارة الإصدارات، من المناسب وضع التبعيات المشتركة في ملف الجذر build.gradle أو pom.xml (لـ Maven). في Gradle، يتم ذلك من خلال كتلة subprojects:
subprojects { apply plugin: 'java' group = 'com.pythonlib' version = '1.0.0'
repositories { mavenCentral() }
dependencies { testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' }}يسمح هذا الهيكل بإضافة خدمات جديدة بسهولة دون نسخ الإعدادات، ويضمن أن جميع الوحدات تستخدم نفس إصدارات المكتبات.
2. إدارة التبعيات والإصدارات
إحدى المشكلات الرئيسية للمستودع الأحادي هي تعارض الإصدارات. في Java، يتم حلها باستخدام BOM (Bill of Materials) أو وصف مركزي للتبعيات. لنلقِ نظرة على مثال في Gradle باستخدام java-platform.
لننشئ وحدة platform التي تحدد جميع التبعيات الخارجية:
// platform/build.gradleplugins { id 'java-platform'}
javaPlatform { allowDependencies()}
dependencies { constraints { api 'com.fasterxml.jackson.core:jackson-databind:2.15.2' api 'org.springframework.boot:spring-boot-starter-web:3.1.5' api 'org.slf4j:slf4j-api:2.0.9' }}الآن في كل خدمة، يمكننا ببساطة الإشارة إلى المنصة:
// service-auth/build.gradledependencies { implementation platform(project(':platform')) implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'org.springframework.boot:spring-boot-starter-web'}يضمن هذا أن جميع الوحدات تستخدم نفس الإصدارات. عند تحديث الإصدار في platform، يتم تطبيق التغييرات تلقائيًا على جميع الخدمات.
نصيحة: لـ Maven، استخدم <dependencyManagement> في ملف pom.xml الجذر. يعمل هذا بشكل مماثل.
3. CI/CD للمستودع الأحادي
يتطلب تنظيم التكامل المستمر في المستودع الأحادي نهجًا خاصًا. لا يمكن بناء جميع الوحدات مع كل commit - فهذا سيؤدي إلى إبطاء pipeline. الحل الأمثل هو البناء التدريجي بناءً على التغييرات.
مثال على GitHub Actions باستخدام paths:
name: Java CIon: push: paths: - 'service-auth/**' - 'common-lib/**' pull_request: paths: - 'service-auth/**' - 'common-lib/**'
jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 17 uses: actions/setup-java@v3 with: java-version: '17' - name: Build with Gradle run: ./gradlew :service-auth:buildيسمح هذا النهج بتشغيل الاختبارات والبناء فقط للوحدات التي تغيرت، مما يقلل وقت pipeline بشكل كبير. بالنسبة للمشاريع الكبيرة، يمكنك أيضًا استخدام أدوات مثل Gradle Build Cache أو Bazel لتسريع البناء.