من مترجم: نحن نعمل بنشاط على نقل النظام الأساسي إلى سكك Java 11 والتفكير في كيفية تطوير مكتبات Java بكفاءة (مثل YARG ) ، مع مراعاة ميزات Java 8/11 حتى لا تضطر إلى إنشاء فروع وإصدارات منفصلة. أحد الحلول الممكنة هو JAR متعدد الإصدارات ، ولكن ليس كل شيء سلسًا.
تتضمن Java 9 خيار وقت تشغيل Java جديد يسمى JARs متعدد الإصدارات. ربما يكون هذا أحد أكثر الابتكارات إثارة للجدل في النظام الأساسي. TL ؛ DR: نجد هذا حلًا معوجًا لمشكلة خطيرة . سنشرح في هذا المنشور سبب اعتقادنا بذلك ، ونخبرك أيضًا بكيفية إنشاء JAR إذا كنت تريد ذلك حقًا.
JARs متعددة الإصدارات ، أو MR JARs ، هي ميزة جديدة لمنصة Java ، تم تقديمها في JDK 9. هنا سنصف بالتفصيل المخاطر الكبيرة المرتبطة باستخدام هذه التقنية ، وكيفية إنشاء JARs متعددة الإصدارات باستخدام Gradle ، إذا كنت لا تزال تريد.
في الواقع ، JAR متعدد الإصدارات هو أرشيف جافا يتضمن العديد من المتغيرات من نفس الفئة للعمل مع إصدارات مختلفة من وقت التشغيل. على سبيل المثال ، إذا كنت تعمل في JDK 8 ، فستستخدم بيئة Java إصدار الفئة لـ JDK 8 ، وإذا تم استخدام إصدار Java 9 في Java 9. وبالمثل ، إذا تم إنشاء الإصدار لإصدار مستقبلي من Java 10 ، فإن وقت التشغيل يستخدم هذا الإصدار بدلاً من إصدار Java 9 أو الإصدار الافتراضي (Java 8).
تحت القطة ، نفهم جهاز تنسيق JAR الجديد ومعرفة ما إذا كان هذا هو كل شيء.
متى يتم استخدام JARs متعدد الإصدارات
بدائل معروفة لـ JARs متعددة الإصدارات
الطريقة الثانية ، أبسط وأكثر قابلية للفهم ، هي إنشاء أرشيفين مختلفين لأوقات التشغيل المختلفة. من الناحية النظرية ، تقوم بإنشاء تطبيقين من نفس الفئة في IDE ، وتجميعها واختبارها وتعبئتها بشكل صحيح في صنعتين مختلفتين هي مهمة نظام البناء. هذا هو النهج الذي تم استخدامه في الجوافة أو سبوك لسنوات عديدة. ولكنها مطلوبة أيضًا للغات مثل سكالا. وكل ذلك بسبب وجود العديد من الخيارات للمترجم ووقت التشغيل بحيث يصبح التوافق الثنائي غير قابل للتحقيق.
ولكن هناك العديد من الأسباب الأخرى لاستخدام أرشيفات JAR منفصلة:
- JAR هي مجرد طريقة للتعبئة.
إنها قطعة أثرية للتجميع تتضمن فئات ، ولكن هذا ليس كل شيء: الموارد ، كقاعدة عامة ، يتم تضمينها أيضًا في الأرشيف. التعبئة والتغليف ، مثل معالجة الموارد ، لها سعر. يهدف فريق Gradle إلى تحسين جودة البناء وتقليل وقت الانتظار للمطور لتجميع النتائج والاختبارات وعملية البناء بشكل عام. إذا ظهر الأرشيف في العملية في وقت مبكر جدًا ، فسيتم إنشاء نقطة مزامنة غير ضرورية. على سبيل المثال ، لتجميع الفئات المعتمدة على واجهة برمجة التطبيقات ، فإن الشيء الوحيد المطلوب هو ملفات .class. ليست هناك حاجة إلى محفوظات جرة ولا موارد في جرة. وبالمثل ، هناك حاجة فقط إلى ملفات التقدير والموارد لتشغيل اختبارات Gradle. للاختبار ، ليست هناك حاجة لإنشاء برطمان. ستحتاجه فقط من قبل مستخدم خارجي (أي عند النشر). ولكن إذا أصبح إنشاء قطعة أثرية إلزاميًا ، فلا يمكن تنفيذ بعض المهام بالتوازي ويتم منع عملية التجميع بأكملها. إذا لم يكن هذا أمرًا بالغ الأهمية بالنسبة للمشروعات الصغيرة ، فإن هذا هو عامل التباطؤ الرئيسي لمشاريع الشركات الكبيرة.
- الأهم من ذلك بكثير ، كونها قطعة أثرية ، لا يمكن لأرشيف الجرة أن يحمل معلومات حول التبعيات.
من غير المحتمل أن تكون تبعيات كل فئة في Java 9 و Java 8 runtime هي نفسها. نعم ، في مثالنا البسيط سيكون هذا ، ولكن بالنسبة للمشروعات الأكبر ، هذا ليس صحيحًا: عادةً ما يستورد المستخدم الواجهة الخلفية للمكتبة لوظيفة Java 9 ويستخدمها لتنفيذ إصدار فئة Java 8. ولكن ، إذا قمت بتثبيت كلا الإصدارين في أرشيف واحد ، في قطعة أثرية واحدة ستكون هناك عناصر ذات أشجار تبعية مختلفة. هذا يعني أنه إذا كنت تعمل مع Java 9 ، فلديك تبعيات لن تكون هناك حاجة إليها مطلقًا. علاوة على ذلك ، تلوث مسار الفصل الدراسي ، مما يخلق تعارضات محتملة لمستخدمي المكتبة.
وأخيرًا ، في مشروع واحد يمكنك إنشاء JARs لأغراض مختلفة:
- لواجهة برمجة التطبيقات
- لجافا 8
- لجافا 9
- مع ملزم الأصلي
- الخ.
يؤدي الاستخدام غير الصحيح لمصنف التبعيات إلى تعارضات تتعلق بمشاركة نفس الآلية. عادة ما يتم تثبيت المصادر أو javadocs كمصنفات ، ولكن في الواقع ليس لديهم تبعيات.
- لا نريد توليد تناقضات ؛ يجب ألا تعتمد عملية البناء على كيفية الحصول على الفئات. وبعبارة أخرى ، فإن استخدام الجرار ذات الإصدارات المتعددة له تأثير جانبي: الاتصال من أرشيف JAR والاستدعاء من دليل الصف أصبح الآن أشياء مختلفة تمامًا. لديهم فرق كبير في دلالات!
- اعتمادًا على الأداة التي تستخدمها لإنشاء JAR ، قد ينتهي بك الأمر بأرشيفات JAR غير متوافقة! الأداة الوحيدة التي تضمن أنه عند حزم خيارين للفصل في أرشيف واحد ، سيكون لديهم واجهة برمجة تطبيقات مفتوحة واحدة - الأداة المساعدة jar نفسها. والتي ، بدون سبب ، لا تتضمن بالضرورة أدوات التجميع أو حتى المستخدمين. JAR هو في الأساس "مغلف" يشبه أرشيف ZIP. لذلك ، اعتمادًا على كيفية جمعها ، ستحصل على سلوكيات مختلفة ، أو ربما ستجمع قطعة أثرية غير صحيحة (ولن تلاحظ).
طرق أكثر كفاءة لإدارة أرشيفات JAR الفردية
السبب الرئيسي لعدم استخدام المطورين لأرشيفات منفصلة هو أنهم غير ملائمين في التجميع والاستخدام. أدوات البناء هي المسؤولة ، والتي قبل غرادل ، لم تتعامل مع هذا على الإطلاق. على وجه الخصوص ، يمكن لأولئك الذين استخدموا هذه الطريقة في Maven الاعتماد فقط على وظيفة المصنف الضعيفة لنشر التحف الإضافية. ومع ذلك ، المصنف لا يساعد في هذه الحالة الصعبة. يتم استخدامها لأغراض مختلفة ، من نشر رموز المصدر ، والتوثيق ، و javadocs ، إلى تنفيذ خيارات المكتبة (guava-jdk5 ، guava-jdk7 ، ...) أو حالات الاستخدام المختلفة (api ، جرة الدهون ، ...). في الممارسة العملية ، لا توجد طريقة لإظهار أن شجرة التبعية المصنف تختلف عن شجرة التبعية للمشروع الرئيسي. وبعبارة أخرى ، فإن تنسيق POM مكسور بشكل أساسي بسبب يمثل كيفية تجميع المكون والأدوات التي يوفرها. لنفترض أنك بحاجة إلى تنفيذ أرشيفين مختلفين من البرطمان: برطمان كلاسيكي ودهن ، والذي يتضمن جميع التبعيات. يقرر مافن أن اثنتين من القطع الأثرية لهما أشجار تبعية متطابقة ، حتى إذا كان هذا خطأ واضحًا! في هذه الحالة ، هذا أكثر من واضح ، لكن الموقف هو نفسه كما هو الحال مع JARs متعددة الإصدارات!
الحل هو التعامل مع الخيارات بشكل صحيح. يمكن لـ Gradle القيام بذلك عن طريق إدارة التبعيات بناءً على الخيارات. هذه الميزة متاحة حاليًا للتطوير على Android ، ولكننا نعمل أيضًا على إصدارها لتطبيقات Java والتطبيقات الأصلية!
تعتمد إدارة التبعية القائمة على المتغيرات على حقيقة أن الوحدات والتحف هي أشياء مختلفة تمامًا. يمكن أن يعمل نفس الرمز بشكل مثالي في أوقات التشغيل المختلفة ، مع مراعاة المتطلبات المختلفة. بالنسبة لأولئك الذين يعملون مع الترجمة الأصلية ، كان هذا واضحًا منذ فترة طويلة: نحن نجمع لـ i386 و amd64 ولا يمكننا بأي حال من الأحوال التدخل في تبعيات مكتبة i386 مع arm64 ! في سياق Java ، هذا يعني أنه بالنسبة لـ Java 8 ، تحتاج إلى إنشاء نسخة من أرشيف JAV "java 8" ، حيث يتوافق تنسيق الفئة مع Java 8. وستحتوي هذه الأداة على بيانات وصفية مع معلومات حول التبعيات التي يجب استخدامها. بالنسبة إلى Java 8 أو 9 ، سيتم تحديد التبعيات المقابلة للإصدار. الأمر بهذه البساطة (في الواقع ، السبب ليس في أن وقت التشغيل هو حقل واحد فقط من الخيارات ، يمكنك الجمع بين عدة).
بالطبع ، لم يفعل أحد ذلك من قبل بسبب التعقيد المفرط: على ما يبدو ، لن يسمح Maven أبدًا بإجراء مثل هذه العملية المعقدة. ولكن مع جرادل من الممكن. يعمل فريق Gradle حاليًا على تنسيق بيانات وصفية جديدة تخبر المستخدمين بالخيار الذي يجب استخدامه. ببساطة ، يجب أن تتعامل أداة الإنشاء مع تجميع هذه الوحدات واختبارها وتعبئتها ومعالجتها. على سبيل المثال ، يجب أن يعمل المشروع في Java 8 و Java 9. وقت التشغيل. من الناحية المثالية ، تحتاج إلى تنفيذ نسختين من المكتبة. هذا يعني أن هناك 2 مترجمين مختلفين (لتجنب استخدام Java 9 API عند العمل في Java 8) ، ودليلين من الفصول ، وفي نهاية المطاف ، 2 أرشيفات JAR مختلفة. وأيضا ، على الأرجح ، سيكون من الضروري اختبار 2 أوقات التشغيل. أو يمكنك تنفيذ أرشيفين ، ولكن تقرر اختبار سلوك إصدار Java 8 في وقت تشغيل Java 9 (لأنه يمكن أن يحدث هذا عند بدء التشغيل!).
لم يتم تنفيذ هذا المخطط حتى الآن ، ولكن فريق Gradle حقق تقدمًا كبيرًا في هذا الاتجاه.
كيفية إنشاء JAR متعدد الإصدارات باستخدام Gradle
ولكن إذا لم تكن هذه الوظيفة جاهزة بعد ، فماذا أفعل؟ الاسترخاء ، يتم إنشاء القطع الأثرية الصحيحة بنفس الطريقة. قبل ظهور الوظيفة أعلاه في نظام Java البيئي ، هناك خياران:
- طريقة قديمة جيدة باستخدام التفكير أو محفوظات JAR مختلفة ؛
- استخدام JARs متعدد الإصدارات (لاحظ أن هذا يمكن أن يكون حلاً سيئًا ، حتى إذا كانت هناك حالات استخدام جيدة).
مهما كان اختيارك ، أو الأرشيفات المختلفة أو JARs متعددة الإصدارات ، فإن المخطط سيكون هو نفسه. JARs متعددة الإصدارات هي في الأساس العبوة الخاطئة: يجب أن تكون خيارًا ، ولكن ليس الهدف. من الناحية الفنية ، فإن تخطيط المصدر هو نفسه لـ JARs الفردية والخارجية. يصف هذا المستودع كيفية إنشاء JAR متعدد الإصدارات باستخدام Gradle. يتم وصف جوهر العملية بإيجاز أدناه.
بادئ ذي بدء ، يجب أن تتذكر دائمًا عادة سيئة واحدة للمطورين: يشغلون Gradle (أو Maven) باستخدام نفس الإصدار من Java التي يتم التخطيط لإطلاق القطع الأثرية عليها. علاوة على ذلك ، في بعض الأحيان يتم استخدام إصدار أحدث لإطلاق Gradle ، ويحدث التجميع مع مستوى API سابق. ولكن لا يوجد سبب محدد للقيام بذلك. في Gradle ، يمكن تجميع روس . يسمح لك بوصف موقع JDK ، بالإضافة إلى تشغيل الترجمة كعملية منفصلة ، لتجميع المكون باستخدام JDK. أفضل طريقة لتكوين JDKs المتنوعة هي تكوين المسار إلى JDK من خلال متغيرات البيئة ، كما هو الحال في هذا الملف . ثم تحتاج فقط إلى تكوين Gradle لاستخدام JDK الصحيح ، بناءً على التوافق مع النظام الأساسي المصدر / الهدف . تجدر الإشارة إلى أنه ، بدءًا من JDK 9 ، لا يلزم وجود إصدارات سابقة من JDK للترجمة المتقاطعة. هذا يجعل ميزة جديدة ، -إصدار. يستخدم Gradle هذه الوظيفة وسوف يقوم بتكوين المترجم حسب الحاجة.
نقطة رئيسية أخرى هي مجموعة مصادر التعيين. مجموعة المصادر هي مجموعة من ملفات المصدر التي يجب تجميعها معًا. يتم الحصول على JAR من خلال تجميع مجموعة مصادر واحدة أو أكثر. لكل مجموعة ، يقوم Gradle تلقائيًا بإنشاء مهمة تجميع مخصصة مناسبة. هذا يعني أنه إذا كانت هناك مصادر لـ Java 8 و Java 9 ، فستكون هذه المصادر في مجموعات مختلفة. هذا هو بالضبط كيف يعمل في مجموعة المصادر لـ Java 9 ، حيث سيكون هناك إصدار من فصلنا. هذا يعمل حقًا ، ولا تحتاج إلى إنشاء مشروع منفصل ، كما هو الحال في Maven. ولكن الأهم من ذلك ، تسمح لك هذه الطريقة بضبط تجميع المجموعة.
إحدى صعوبات الحصول على إصدارات مختلفة من فئة واحدة هي أن رمز الفصل نادرًا ما يكون مستقلاً عن باقي الرمز (يحتوي على تبعيات مع فئات ليست في المجموعة الرئيسية). على سبيل المثال ، يمكن لواجهة برمجة التطبيقات الخاصة بها استخدام فئات لا تحتاج إلى مصادر خاصة لدعم Java 9. في نفس الوقت ، لا أريد إعادة ترجمة جميع هذه الفئات الشائعة وتعبئة إصداراتها لـ Java 9. إنها فئات مشتركة ، لذلك يجب أن تكون موجودة بشكل منفصل عن دروس لـ JDK محددة. قمنا بإعداده هنا : أضف تبعية بين مجموعة المصدر لـ Java 9 والمجموعة الرئيسية ، بحيث عند تجميع الإصدار لـ Java 9 ، تبقى جميع الفئات الشائعة في مسار التصنيف.
الخطوة التالية بسيطة : عليك أن توضح لـ Gradle أن المجموعة الرئيسية من المصادر سيتم تجميعها مع مستوى Java 8 API ، ومجموعة Java 9 مع مستوى Java 9.
سيساعدك كل ما سبق في استخدام كل من الأساليب المذكورة سابقًا: تنفيذ أرشيفات JAR منفصلة أو JARs متعددة الإصدارات. نظرًا لأن المنشور حول هذا الموضوع ، فلنلقِ نظرة على مثال لكيفية الحصول على Gradle لإنشاء JAR متعدد الإصدارات:
jar { into('META-INF/versions/9') { from sourceSets.java9.output } manifest.attributes( 'Multi-Release': 'true' ) }
يصف هذا القسم: فئات التعبئة لـ Java 9 في دليل META-INF / الإصدارات / 9 ، المستخدم في MR JARs ، وتعيين تسمية الإصدار المتعدد في البيان.
هذا كل ما في الأمر ، MR JAR الأول جاهز!
ولكن ، للأسف ، لم ينته العمل على هذا. إذا كنت تعمل مع Gradle ، فأنت تعلم أنه عند استخدام المكوّن الإضافي للتطبيق ، يمكنك تشغيل التطبيق مباشرة من خلال مهمة التشغيل . ومع ذلك ، نظرًا لحقيقة أن Gradle عادةً ما يحاول تقليل كمية العمل ، يجب أن تستخدم مهمة التشغيل كلاً من أدلة الفئة وأدلة الموارد المعالجة. بالنسبة إلى JARs متعددة الإصدارات ، هذه مشكلة لأن JARs مطلوبة على الفور! لذلك ، بدلاً من استخدام المكون الإضافي ، سيتعين عليك إنشاء مهمتك الخاصة ، وهذه حجة ضد استخدام JARs متعددة الإصدارات.
وأخيرًا وليس آخرًا ، ذكرنا أننا سنحتاج إلى اختبار نسختين من الفصل. لهذا ، يمكنك استخدام VM فقط في عملية منفصلة ، لأنه لا يوجد ما يعادلها من علامة -إصدار وقت تشغيل Java. الفكرة هي أنه يجب كتابة اختبار واحد فقط ، ولكن سيتم تنفيذه مرتين: في Java 8 و Java 9. هذه هي الطريقة الوحيدة للتأكد من عمل الفئات الخاصة بوقت التشغيل بشكل صحيح. بشكل افتراضي ، يقوم Gradle بإنشاء مهمة اختبار واحدة ، ويستخدم دلائل الفئة بنفس الطريقة بدلاً من JAR. لذلك ، سنقوم بأمرين: إنشاء مهمة اختبار لـ Java 9 وتكوين كلتا المهمتين بحيث يستخدمان JAR وأوقات تشغيل Java المحددة. سيبدو التنفيذ كما يلي:
test { dependsOn jar def jdkHome = System.getenv("JAVA_8") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println "$name runs test using JDK 8" } } task testJava9(type: Test) { dependsOn jar def jdkHome = System.getenv("JAVA_9") classpath = files(jar.archivePath, classpath) - sourceSets.main.output executable = file("$jdkHome/bin/java") doFirst { println classpath.asPath println "$name runs test using JDK 9" } } check.dependsOn(testJava9)
الآن ، عندما تبدأ المهمة ، تحقق من أن Gradle سيقوم بتجميع كل مجموعة من المصادر باستخدام JDK المطلوب ، وإنشاء JAR متعدد الإصدارات ، ثم تشغيل الاختبارات باستخدام JAR على كل من JDKs. ستساعدك الإصدارات المستقبلية من Gradle على القيام بذلك بشكل أكثر وضوحًا.
الخلاصة
لتلخيص. لقد علمت أن JARs متعددة الإصدارات هي محاولة لحل المشكلة الحقيقية التي يواجهها العديد من مطوري المكتبات. ومع ذلك ، يبدو أن حل المشكلة غير صحيح. إدارة التبعية الصحيحة ، وربط القطع الأثرية والخيارات ، والاهتمام بالأداء (القدرة على تنفيذ أكبر عدد ممكن من المهام بالتوازي) - كل هذا يجعل MR JAR حلاً للفقراء. يمكن حل هذه المشكلة بشكل صحيح باستخدام الخيارات. ومع ذلك ، في حين أن إدارة التبعية باستخدام خيارات من Gradle قيد التطوير ، فإن JARs متعددة الإصدارات ملائمة تمامًا في الحالات البسيطة. في هذه الحالة ، ستساعدك هذه المشاركة على فهم كيفية القيام بذلك ، وكيف تختلف فلسفة Gradle عن Maven (مجموعة المصادر مقابل المشروع).
أخيرًا ، لا ننكر أن هناك حالات يكون فيها JARs متعددة الإصدارات منطقية: على سبيل المثال ، عندما لا يكون معروفًا في أي بيئة سيتم تنفيذ التطبيق (وليس مكتبة) ، ولكن هذا استثناء. في هذا المنشور ، قمنا بوصف المشاكل الرئيسية التي يواجهها مطورو المكتبات ، وكيف تحاول JARs متعددة الإصدارات حلها. النمذجة الصحيحة للاعتمادية كخيارات تحسن الأداء (من خلال التوازي الدقيق) وتقلل تكاليف الصيانة (تجنب التعقيد غير المتوقع) مقارنة مع JARs متعددة الإصدارات. في وضعك ، قد تكون هناك حاجة أيضًا إلى MR JARs ، لذلك اعتنى Gradle بالفعل بهذا الأمر. ألق نظرة على هذا المشروع عينة وجربه بنفسك.