تجربة نقل مشروع Maven إلى Jar متعدد الإصدار: ممكن بالفعل ، ولكن لا يزال من الصعب

لدي مكتبة StreamEx صغيرة تمتد إلى Java 8 Stream API. أقوم عادة بتجميع المكتبة من خلال مافن ، وفي الغالب أنا سعيد بكل شيء. ومع ذلك ، أردت أن التجربة.


يجب أن تعمل بعض الأشياء في المكتبة بشكل مختلف في إصدارات مختلفة من Java. المثال الأكثر وضوحًا هو طرق Stream API الجديدة مثل takeWhile ، والتي ظهرت فقط في Java 9. توفر مكتبتي تطبيقًا لهذه الطرق في Java 8 أيضًا ، ولكن عندما تقوم بتوسيع Stream API بنفسك ، فإنك تخضع لبعض القيود التي لم أذكرها هنا. أتمنى أن يكون لدى مستخدمي Java 9+ إمكانية الوصول إلى تطبيق قياسي.


لكي يستمر المشروع في تجميع استخدام Java 8 ، يتم ذلك عادة باستخدام أدوات الانعكاس: نكتشف ما إذا كانت هناك طريقة مقابلة في المكتبة القياسية ، وإذا كان الأمر كذلك ، فإننا نسميها ، وإذا لم يكن الأمر كذلك ، فإننا نستخدم تنفيذها. ومع ذلك ، فقد قررت استخدام API MethodHandle ، لأنه يعلن عن مكالمة حمولة أقل. يمكنك الحصول على MethodHandle مقدمًا وحفظه في حقل ثابت:


 MethodHandles.Lookup lookup = MethodHandles.publicLookup(); MethodType type = MethodType.methodType(Stream.class, Predicate.class); MethodHandle method = null; try { method = lookup.findVirtual(Stream.class, "takeWhile", type); } catch (NoSuchMethodException | IllegalAccessException e) { // ignore } 

ثم استخدمه:


 if (method != null) { return (Stream<T>)method.invokeExact(stream, predicate); } else { // Java 8 polyfill } 

كل هذا جيد ، لكنه يبدو قبيحًا. والأهم من ذلك ، في كل نقطة يكون فيها اختلاف التطبيقات ممكنًا ، عليك كتابة مثل هذه الشروط. تتمثل الطريقة البديلة قليلاً في فصل استراتيجيات Java 8 و Java 9 كتطبيق للواجهة نفسها. أو لحفظ حجم المكتبة ، قم ببساطة بتنفيذ كل شيء لـ Java 8 في فصل منفصل غير نهائي ، واستبدل وراثة لـ Java 9. تم القيام بشيء مثل هذا:


 //    Internals static final VersionSpecific VER_SPEC = System.getProperty("java.version", "").compareTo("1.9") > 0 ? new Java9Specific() : new VersionSpecific(); 

ثم عند نقاط الاستخدام ، يمكنك ببساطة كتابة return Internals.VER_SPEC.takeWhile(stream, predicate) . كل السحر مع مقابض الطريقة هو الآن فقط في فئة Java9Specific . هذا النهج ، بالمناسبة ، حفظ المكتبة لمستخدمي أندرويد الذين اشتكوا من قبل من أنها لا تعمل من حيث المبدأ. جهاز Android الظاهري ليس جافا ، ولا يقوم حتى بتنفيذ مواصفات Java 7. على وجه الخصوص ، لا توجد طرق مع توقيع متعدد الأشكال مثل invokeExact ، ووجود هذه المكالمة في الكود الثاني يكسر كل شيء. الآن يتم وضع هذه المكالمات في فصل لم تتم تهيئته أبدًا.


ومع ذلك ، كل هذا لا يزال قبيح. الحل الجميل (على الأقل من الناحية النظرية) هو استخدام Jar Multi Release Jar ، الذي يأتي مع Java 9 ( JEP-238 ). للقيام بذلك ، يجب أن يتم تجميع جزء من الفئات ضمن Java 9 وملفات الفئة المترجمة في META-INF/versions/9 داخل ملف Jar. بالإضافة إلى ذلك ، تحتاج إلى إضافة السطر Multi-Release: true إلى البيان. ثم يتجاهل Java 8 كل هذا بنجاح ، وسوف يقوم Java 9 والإصدارات الأحدث بتحميل فئات جديدة بدلاً من الفئات التي تحمل نفس الأسماء الموجودة في المكان المعتاد.


في المرة الأولى التي حاولت فيها القيام بذلك منذ أكثر من عامين ، قبل وقت قصير من إصدار Java 9. كان الأمر صعبًا للغاية ، وقد خرجت. حتى مجرد جعل المشروع مترجمًا مع برنامج التحويل البرمجي لـ Java 9 كان أمرًا صعبًا: لقد تعطلت الكثير من مكونات Maven الإضافية بسبب تغيير واجهات برمجة التطبيقات الداخلية أو تغيير java.version سلسلة java.version أو أي شيء آخر.


كانت محاولة جديدة هذا العام أكثر نجاحًا. تم تحديث الإضافات في معظمها وتعمل في جافا الجديد بشكل كافٍ. الخطوة الأولى التي ترجمت التجميع بالكامل إلى Java 11. لهذا ، بالإضافة إلى تحديث إصدارات المكون الإضافي ، كان علي القيام بما يلي:


  • تغيير في JavaDoc package-info.java روابط النموذج <a name="..."> إلى <a id="..."> . خلاف ذلك ، JavaDoc يشكو.
  • حدد additionalOptions = --no-module-directories في البرنامج المساعد maven-javadoc. بدون ذلك ، كانت هناك أخطاء غريبة مع ميزات بحث JavaDoc: لم يتم إنشاء الدلائل مع الوحدات النمطية ، ولكن عند التبديل إلى نتيجة البحث ، /undefined/ إضافته إلى المسار (مرحبًا ، JavaScript). لم تكن هذه الميزة في Java 8 على الإطلاق ، لذا فإن نشاطي قد حقق بالفعل نتيجة رائعة: أصبح JavaDoc عبارة عن بحث.
  • إصلاح المكوّن الإضافي لنشر نتائج تغطية الاختبار في Coveralls ( coveralls-maven-plugin ). لسبب ما ، يتم التخلي عنه ، وهو أمر غريب ، بالنظر إلى أن Coveralls تعيش من تلقاء نفسها وتقدم خدمات تجارية. اختفت واجهة برمجة تطبيقات jaxb-api التي يستخدمها البرنامج المساعد من Java 11. لحسن الحظ ، ليس من الصعب حل المشكلة باستخدام أدوات Maven: يكفي تسجيل التبعية بشكل صريح على المكون الإضافي:
     <plugin> <groupId>org.eluder.coveralls</groupId> <artifactId>coveralls-maven-plugin</artifactId> <version>4.3.0</version> <dependencies> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.2.3</version> </dependency> </dependencies> </plugin> 

والخطوة التالية هي تكييف الاختبارات. نظرًا لأن سلوك المكتبة مختلف بوضوح في Java 8 و Java 9 ، فمن المنطقي إجراء اختبارات لكلا الإصدارين. نعمل الآن على تشغيل كل شيء ضمن Java 11 ، لذلك لم يتم اختبار كود Java 8 المخصص. هذا هو رمز كبير إلى حد ما وغير تافهة. لإصلاح ذلك ، قمت بعمل قلم اصطناعي:


 static final VersionSpecific VER_SPEC = System.getProperty("java.version", "").compareTo("1.9") > 0 && !Boolean.getBoolean("one.util.streamex.emulateJava8") ? new Java9Specific() : new VersionSpecific(); 

الآن فقط اجتياز -Done.util.streamex.emulateJava8=true عند إجراء الاختبارات ،
لاختبار ما يعمل عادةً في Java 8. الآن قم بإضافة كتلة <execution> جديدة إلى تكوين argLine = -Done.util.streamex.emulateJava8=true maven-surefire-plugin باستخدام argLine = -Done.util.streamex.emulateJava8=true ، وتمرير الاختبارات مرتين.


ومع ذلك ، أود أن أفكر في اختبارات التغطية الكلية. أستخدم JaCoCo ، وإذا لم تخبره بأي شيء ، فستعمل الجولة الثانية ببساطة على استبدال نتائج الأول. كيف يعمل JaCoCo؟ إنه أولاً يدير هدف prepare-agent ، الذي يحدد خاصية argLine Maven ، ويوقع شيئًا يشبه -javaagent:blah-blah/.m2/org/jacoco/org.jacoco.agent/0.8.4/org.jacoco.agent-0.8.4-runtime.jar=destfile=blah-blah/myproject/target/jacoco.exec . أريد أن يتم تشكيل ملفين مختلفين exec. يمكنك اختراقه بهذه الطريقة. أضف destFile=${project.build.directory} تكوين prepare-agent . الخام ولكن فعالة. الآن سوف ينتهي argLine blah-blah/myproject/target . نعم ، هذا ليس ملفًا على الإطلاق ، لكنه دليل. ولكن يمكننا استبدال اسم الملف بالفعل في بداية الاختبارات. نعود إلى maven-surefire-plugin وتعيين argLine = @{argLine}/jacoco_java8.exec -Done.util.streamex.emulateJava8=true لتشغيل Java 8 و argLine = @{argLine}/jacoco_java11.exec لتشغيل Java 11. بعد ذلك ، من السهل الجمع بين هذين الملفين باستخدام هدف merge ، والذي يوفره أيضًا البرنامج المساعد JaCoCo ، ونحصل على التغطية الشاملة.


تحديث: قال godin في التعليقات أنه غير ضروري ، يمكنك الكتابة إلى ملف واحد ، وسوف الغراء تلقائيا النتيجة مع الملف السابق. الآن لست متأكدًا من السبب في أن هذا السيناريو لم يكن مناسبًا لي في البداية.


حسنًا ، نحن مستعدون جيدًا للتبديل إلى Jar متعدد الإصدار. لقد وجدت عددًا من التوصيات حول كيفية القيام بذلك. اقترح الأول استخدام مشروع Maven متعدد الوحدات. لا أشعر بذلك: هذا تعقيد كبير في بنية المشروع: هناك خمسة pom.xml ، على سبيل المثال. لخداع من أجل زوجين من الملفات التي تحتاج إلى تجميع في Java 9 يبدو مبالغا فيه. اقترح آخر بدء تجميع من خلال maven-antrun-plugin . هنا قررت أن ننظر فقط كملاذ أخير. من الواضح أن أي مشكلة في Maven يمكن حلها باستخدام Ant ، ولكن هذا أمر خرقاء إلى حد ما. أخيرًا ، رأيت توصية لاستخدام ملحق إضافي متعدد الإصدار-jar-maven-plugin . بدا بالفعل لذيذ والحق.


يوصي البرنامج المساعد بوضع أكواد المصدر الخاصة بالإصدارات الجديدة من Java في الدلائل مثل src/main/java-mr/9 ، وهو ما قمت به. ما زلت قررت تجنب الاصطدامات في أسماء الفئات إلى الحد الأقصى ، وبالتالي فإن الفئة الوحيدة (حتى الواجهة) الموجودة في كل من Java 8 و Java 9 ، لدي هذا:


 // Java 8 package one.util.streamex; /* package */ interface VerSpec { VersionSpecific VER_SPEC = new VersionSpecific(); } // Java 9 package one.util.streamex; /* package */ interface VerSpec { VersionSpecific VER_SPEC = new Java9Specific(); } 

انتقل الثابت القديم إلى مكان جديد ، لكن لم يتغير شيء آخر. الآن Java9Specific فئة Java9Specific أكثر بساطة: تم استبدال جميع القرفصاء مع MethodHandle بنجاح باستدعاءات الطريقة المباشرة.


يعد المكوّن الإضافي بالقيام بالأمور التالية:


  • استبدل maven-compiler-plugin القياسي maven-compiler-plugin وقم بترجمته في خطوتين بإصدار مستهدف مختلف.
  • استبدل maven-jar-plugin القياسي maven-jar-plugin وحزم نتيجة التجميع بالمسارات الصحيحة.
  • أضف السطر Multi-Release: true إلى MANIFEST.MF .

من أجل أن تعمل ، استغرق الأمر بضع خطوات.


  1. تغيير العبوة من jar إلى jar multi-release-jar .


  2. إضافة امتداد البناء:


     <build> <extensions> <extension> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> </extension> </extensions> </build> 

  3. نسخ التكوين من maven-compiler-plugin . كان لدي الإصدار الافتراضي فقط بروح <source>1.8</source> و <arg>-Xlint:all</arg>


  4. اعتقدت أنه يمكن الآن إزالة maven-compiler-plugin ، لكن اتضح أن المكوّن الإضافي الجديد لا يحل محل مجموعة الاختبارات ، لذلك تمت إعادة تعيين إصدار Java إلى الافتراضي (1.5!) و -Xlint:all اختفت -Xlint:all الحجة. لذلك اضطررت للمغادرة.


  5. من أجل عدم تكرار المصدر والهدف maven.compiler.source maven.compiler.target ، اكتشفت أنهما يحترمان خصائص maven.compiler.source و maven.compiler.target . قمت بتثبيتها وإزالة الإصدارات من إعدادات البرنامج المساعد. ومع ذلك ، اتضح فجأة أن maven-javadoc-plugin يستخدم source من إعدادات maven maven-compiler-plugin للعثور على عنوان URL الخاص بـ JavaDoc القياسي ، والذي يجب ربطه عند الإشارة إلى الطرق القياسية. والآن لا يحترم maven.compiler.source . لذلك ، اضطررت إلى إرجاع <source>${maven.compiler.source}</source> إلى إعدادات <source>${maven.compiler.source}</source> maven-compiler-plugin . لحسن الحظ ، لم تكن هناك حاجة لتغييرات أخرى لإنشاء JavaDoc. يمكن أن يتم إنشاؤه جيدًا من مصادر Java 8 ، نظرًا لأن الخط الدائري بأكمله مع الإصدارات لا يؤثر على واجهة برمجة تطبيقات المكتبة.


  6. maven-bundle-plugin ، والذي حول مكتبتي إلى قطعة أثرية من OSGi. لقد رفض ببساطة العمل مع packaging = multi-release-jar . من حيث المبدأ ، لم يعجبني أبدًا. يكتب مجموعة من الخطوط الإضافية للبيان ، وفي نفس الوقت يفسد ترتيب الفرز ويضيف المزيد من القمامة. لحسن الحظ ، اتضح أنه ليس من الصعب التخلص منه بكتابة كل ما تحتاجه باليد. فقط ، بالطبع ، ليس في maven-jar-plugin ، ولكن في البرنامج الجديد. أصبح التكوين الكامل multi-release-jar الإضافي multi-release-jar النهاية مثل هذا (قمت بتعريف بعض الخصائص مثل project.package بنفسي):


     <plugin> <groupId>pw.krejci</groupId> <artifactId>multi-release-jar-maven-plugin</artifactId> <version>0.1.5</version> <configuration> <compilerArgs><arg>-Xlint:all</arg></compilerArgs> <archive> <manifestEntries> <Automatic-Module-Name>${project.package}</Automatic-Module-Name> <Bundle-Name>${project.name}</Bundle-Name> <Bundle-Description>${project.description}</Bundle-Description> <Bundle-License>${license.url}</Bundle-License> <Bundle-ManifestVersion>2</Bundle-ManifestVersion> <Bundle-SymbolicName>${project.package}</Bundle-SymbolicName> <Bundle-Version>${project.version}</Bundle-Version> <Export-Package>${project.package};version="${project.version}"</Export-Package> </manifestEntries> </archive> </configuration> </plugin> 

  7. الاختبارات. لم يعد لدينا one.util.streamex.emulateJava8 ، ولكن يمكنك تحقيق نفس التأثير من خلال تعديل اختبارات مسار الفصل. الآن العكس هو الصحيح: بشكل افتراضي ، تعمل المكتبة في وضع Java 8 ، ولجافا 9 تحتاج إلى الكتابة:


     <classesDirectory>${basedir}/target/classes-9</classesDirectory> <additionalClasspathElements>${project.build.outputDirectory}</additionalClasspathElements> <argLine>@{argLine}/jacoco_java9.exec</argLine> 

    نقطة مهمة: يجب أن تتقدم classes-9 إلى ملفات الفئة العادية ، لذلك اضطررنا إلى نقل الملفات المعتادة إلى additionalClasspathElements ، والتي تتم إضافتها بعد ذلك.


  8. مصادر. سأحصل على جرة المصدر ، وسيكون من الجيد تجميع مصادر Java 9 فيه ، على سبيل المثال ، يمكن أن يعرضها مصحح الأخطاء في IDE بشكل صحيح. لست قلقًا جدًا بشأن مكررة VerSpec ، لأنه يوجد سطر واحد يتم تنفيذه فقط أثناء التهيئة. لا بأس في ترك الخيار من Java 8. فقط ، ولكن سيكون من الجيد Java9Specific.java . يمكن القيام بذلك عن طريق إضافة دليل مصدر إضافي يدويًا:


     <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <version>3.0.0</version> <executions> <execution> <phase>test</phase> <goals><goal>add-source</goal></goals> <configuration> <sou​rces> <sou​rce>src/main/java-mr/9</sou​rce> </sou​rces> </configuration> </execution> </executions> </plugin> 

    بعد جمع القطع الأثرية ، قمت بتوصيلها بمشروع اختبار والتحقق منه في مصحح أخطاء IntelliJ IDEA. كل شيء يعمل بشكل جميل: وفقًا لإصدار الجهاز الظاهري المستخدم لتشغيل مشروع الاختبار ، ينتهي بنا المطاف في مصدر مختلف عند تصحيح الأخطاء.


    سيكون من الرائع أن يتم ذلك بواسطة المكون الإضافي متعدد الجرة نفسه ، لذلك قدمت هذا الاقتراح.


  9. JaCoCo. اتضح أنه الأصعب معه ، ولم أستطع الاستغناء عن المساعدة الخارجية. الحقيقة هي أن ملفات exec التي تم إنشاؤها بشكل مثالي للملفات Java-8 و Java-9 ، عادة ما يتم لصقها في ملف واحد ، ومع ذلك ، عند إنشاء تقارير في XML و HTML ، فإنها تتجاهل بعناد المصادر من Java-9. البحث في المصدر ، رأيت أنه يقوم فقط بإنشاء تقرير لملفات الفئة الموجودة في project.getBuild().getOutputDirectory() . بالطبع ، يمكن استبدال هذا الدليل ، لكن في الحقيقة لدي اثنان منهم: classes و classes-9 . من الناحية النظرية ، يمكنك نسخ جميع الفئات في دليل واحد ، وتغيير outputDirectory وبدء JaCoCo ، ثم قم بتغيير outputDirectory مرة أخرى حتى لا يتم كسر مجموعة JAR. ولكن هذا يبدو قبيح تماما. بشكل عام ، قررت تأجيل حل هذه المشكلة في مشروعي في الوقت الحالي ، لكنني كتبت إلى اللاعبين من JaCoCo أنه سيكون من الجيد أن تكون قادرًا على تحديد عدة أدلة مع ملفات الفصل الدراسي.


    لدهشتي ، بعد بضع ساعات جاء أحد مطوري JaCoCo godin إلى مشروعي وجلب طلب سحب ، وهو ما يحل المشكلة. كيف تقرر؟ باستخدام النملة ، بالطبع! اتضح أن برنامج Ant-plugin لـ JaCoCo أكثر تقدماً ويمكنه إنشاء تقرير موجز للعديد من أدلة المصادر وملفات الفئة. لم يكن حتى في حاجة إلى خطوة merge منفصلة ، لأنه يمكنه إطعام عدة ملفات تنفيذية مرة واحدة. بشكل عام ، لا يمكن تجنب النملة ، فليكن. الشيء الرئيسي الذي نجح ، ونمت pom.xml ستة خطوط فقط.



    أنا حتى تغردت في قلوب:




لذلك حصلت على مشروع يعمل جيدًا يبني جرة متعددة الإصدار جميلة. في الوقت نفسه ، زادت نسبة التغطية ، لأنني أزلت كل أنواع catch (NoSuchMethodException | IllegalAccessException e) التي لم يكن من الممكن الوصول إليها في Java 9. لسوء الحظ ، لم يتم دعم بنية المشروع هذه بواسطة IntelliJ IDEA ، لذلك اضطررت إلى التخلي عن استيراد POM وتكوين المشروع في IDE يدويًا . آمل أنه في المستقبل سيظل هناك حل قياسي يتم دعمه تلقائيًا بواسطة جميع الأدوات الإضافية والأدوات.

Source: https://habr.com/ru/post/ar472312/


All Articles