جافا المكونات في دون ألم

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

  • لا نحتاج إلى مكتبات أو أطر عمل خاصة (OSGi ، Guice ، إلخ.)
  • لن نستخدم تحليل bytecode مع ASM والمكتبات المشابهة.
  • لن نكتب محمل الصف لدينا.
  • لن نستخدم التفكير والشروح.
  • لا حاجة للتركيز على classpath للعثور على المكونات الإضافية. نحن لن نلمس classpath على الإطلاق.
  • أيضًا ، لن نستخدم XML أو YAML أو أي لغة تعريفية أخرى لوصف نقاط الامتداد (نقاط الامتداد في الإضافات).

ومع ذلك ، لا يزال هناك شرط واحد - مثل هذا الحل لن يعمل إلا على Java 9 أو أعلى. لأنه سوف يعتمد على الوحدات والخدمات .

لذلك دعونا نبدأ. نقوم بصياغة المشكلة بشكل أكثر تحديدًا:
تحتاج إلى تطبيق الحد الأدنى من إطار التطبيق ، والذي عند بدء التشغيل سوف يقوم بتحميل مكونات المستخدم الإضافية من مجلد plugins .

بمعنى ، يجب أن يبدو التطبيق المجمّع مثل هذا:

 plugin-app/ plugins/ plugin1.jar plugin2.jar ... core.jar … 

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

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

 git clone https://github.com/orionll/plugin-app cd plugin-app mvn verify cd core/target java --module-path core-1.0-SNAPSHOT.jar --module core 

قم بإنشاء ملفات Java الأربعة التالية في الوحدة النمطية:

 core/ src/main/java/ org/example/pluginapp/core/ IService.java BasicService.java Main.java module-info.java 

الملف الأول ، IService.java هو الملف الذي يصف نقطة الامتداد الخاصة بنا. ستتمكن المكونات الإضافية الأخرى من المساهمة في نقطة التوسع هذه ("المساهمة"). هذا هو المبدأ القياسي لإنشاء تطبيقات المكونات الإضافية ، والتي تسمى مبدأ انعكاس التبعية ( انعكاس التبعية). يعتمد هذا المبدأ على حقيقة أن النواة لا تعتمد على فئات محددة ، ولكن على واجهات.

أعطيت نقطة الامتداد الاسم التجريدي IService ، حيث أعرض الآن مفهومًا حصريًا. في الواقع ، يمكن أن تكون أي نقطة امتداد محددة ، على سبيل المثال ، إذا كنت تكتب محرر رسومات ، فقد يكون تأثير معالجة الصور ، على سبيل المثال ، IEffectProvider أو IEffectContribution أو أي شيء آخر ، اعتمادًا على الطريقة التي تفضل بها تسمية نقاط الامتداد. في الوقت نفسه ، سيحتوي التطبيق نفسه على مجموعة من التأثيرات الأساسية ، وسيتمكن مطورو الطرف الثالث من كتابة تأثيرات أكثر تطوراً وإيصالها في شكل مكونات إضافية. يحتاج المستخدم فقط إلى وضع هذه التأثيرات في مجلد plugins وإعادة تشغيل التطبيق.

ملف IService.java كالتالي:

 public interface IService { void doJob(); static List<IService> getServices(ModuleLayer layer) { return ServiceLoader .load(layer, IService.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } } 

وبالتالي ، فإن IService هي مجرد واجهة تقوم ببعض أعمال doJob() المجردة doJob() أكرر ، التفاصيل ليست مهمة ، في الواقع ستكون شيئًا ملموسًا).

انتبه أيضًا إلى طريقة getServices() الثانية. تُرجع هذه الطريقة جميع تطبيقات واجهة IService التي وجدت في طبقة الوحدة النمطية هذه والآباء. سنتحدث عن هذا بمزيد من التفصيل في وقت لاحق.

الملف الثاني ، BasicService.java ، هو التطبيق الأساسي لواجهة IService . سيكون دائمًا موجودًا ، حتى إذا لم يكن هناك مكونات إضافية في التطبيق. بمعنى آخر ، لا يعد core هو core فقط ، ولكنه أيضًا في نفس الوقت مكوّن إضافي خاص به ، وسيتم تحميله دائمًا. يبدو ملف BasicService.java كما يلي:

 public class BasicService implements IService { @Override public void doJob() { System.out.println("Basic service"); } } 

من أجل البساطة ، يقوم doJob() فقط بطباعة السلسلة "Basic service" وهذه هي.

وبالتالي ، في الوقت الحالي لدينا الصورة التالية:



الملف الثالث ، Main.java ، هو المكان الذي يتم فيه تطبيق الطريقة main() . هناك القليل من السحر ، لفهم ما تحتاج إلى معرفته ما هي طبقة الوحدة النمطية.

حول طبقات الوحدة النمطية


عندما يقوم Java بتشغيل التطبيق ، فإن كل وحدات النظام الأساسي + الوحدات النمطية المدرجة في الوسيطة --module-path (وأيضًا classpath ، إن وجدت) تقع في ما يسمى طبقة Boot . في حالتنا ، إذا قمنا بتجميع الوحدة النمطية core.jar وقمنا بتشغيل java --module-path core.jar --module core من سطر الأوامر ، java.base الوحدات core java.base و core على الأقل في طبقة Boot :



طبقة Boot موجودة دائمًا في أي تطبيق Java ، وهذا هو أصغر تكوين ممكن. توجد معظم التطبيقات في طبقة واحدة من الوحدات. ومع ذلك ، في حالتنا ، نريد القيام بالتحميل الديناميكي للمكونات الإضافية من مجلد plugins . يمكننا فقط إجبار المستخدم على تصحيح خط إطلاق التطبيق بحيث يضيف هو نفسه المكونات الإضافية الضرورية إلى --module-path ، لكن هذا لن يكون الحل الأفضل. خاصة أولئك الأشخاص الذين ليسوا مبرمجين ولا يفهمون سبب حاجتهم للتسلق في مكان ما وإصلاح شيء لمثل هذا الشيء البسيط.

لحسن الحظ ، يوجد حل: يتيح لك Java إنشاء طبقات الوحدات الخاصة بك في وقت التشغيل ، والتي ستقوم بتحميل الوحدات من المكان الذي نحتاج إليه. لأغراضنا ، ستكون طبقة جديدة للإضافات كافية ، والتي سيكون لها طبقة Boot كأحد الوالدين (يجب أن يكون لأي طبقة أحد الوالدين):



حقيقة أن طبقة المكوّن الإضافي تحتوي على طبقة Boot باعتبارها الأصل تعني أن الوحدات من طبقة المكوّن الإضافي يمكن أن تشير إلى الوحدات النمطية من طبقة Boot ، لكن ليس العكس.

لذلك ، ومع العلم الآن ما هي طبقة الوحدة النمطية ، يمكنك أخيرًا الاطلاع على محتويات ملف Main.java :

 public final class Main { public static void main(String[] args) { Path pluginsDir = Paths.get("plugins"); //      plugins ModuleFinder pluginsFinder = ModuleFinder.of(pluginsDir); //  ModuleFinder      plugins       List<String> plugins = pluginsFinder .findAll() .stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.toList()); //  ,      (   ) Configuration pluginsConfiguration = ModuleLayer .boot() .configuration() .resolve(pluginsFinder, ModuleFinder.of(), plugins); //      ModuleLayer layer = ModuleLayer .boot() .defineModulesWithOneLoader(pluginsConfiguration, ClassLoader.getSystemClassLoader()); //     IService       Boot List<IService> services = IService.getServices(layer); for (IService service : services) { service.doJob(); } } } 

إذا كانت هذه هي المرة الأولى التي تنظر فيها إلى هذا الرمز ، فقد يبدو الأمر معقدًا للغاية ، لكن هذا شعور خاطئ بسبب العدد الكبير من الفصول الجديدة غير المعروفة. إذا كنت تتفهم قليلاً معنى فصول ModuleFinder ، والتكوين ، و ModuleLayer ، فسيصبح كل شيء في مكانه الصحيح . وإلى جانب ذلك ، لا يوجد سوى بضع عشرات من الخطوط! هذا هو كل المنطق المكتوب مرة واحدة.

واصف وحدة


يوجد ملف (رابع) واحد لم module-info.java الاعتبار: module-info.java . هذا هو أقصر ملف يحتوي على إعلان وحدتنا ووصف للخدمات (نقاط التمديد):

 module core { exports org.example.pluginapp.core; uses IService; provides IService with BasicService; } 

يجب أن يكون معنى سطور هذا الملف واضحًا:

  • أولاً ، تصدر الوحدة النمطية الحزمة org.example.pluginapp.core يمكن أن تكتسب المكونات الإضافية من واجهة IService (وإلا لن يكون IService متاحًا خارج الوحدة core ).
  • ثانياً ، يعلن أنه يستخدم IService .
  • ثالثًا ، يقول إنه يوفر تطبيق خدمة IService خلال فئة BasicService .

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

لذلك ، الإطار جاهز. دعنا نحاول تشغيله:

 > java --module-path core.jar --module core Basic service 

ماذا حدث

  1. حاول Java العثور على الوحدات النمطية في مجلد plugins ولم يعثر على أي منها.
  2. تم إنشاء طبقة فارغة.
  3. بدأ IService البحث عن جميع IService .
  4. في الطبقة الفارغة ، لم يعثر على أي تطبيقات خدمة ، حيث لا توجد وحدات هناك.
  5. بعد هذه الطبقة ، واصل البحث في الطبقة BasicService (أي طبقة Boot ) ووجد تطبيقًا واحدًا من BasicService في الوحدة النمطية core .
  6. كل التطبيقات التي تم العثور عليها بها طريقة doJob() تسمى. منذ العثور على تطبيق واحد فقط ، تمت طباعة "Basic service" .

كتابة البرنامج المساعد


بعد أن كتبنا جوهر طلبنا ، حان الوقت لمحاولة كتابة المكونات الإضافية لذلك. دعنا نكتب اثنين من المكونات الإضافية plugin1 و plugin2 : دع أول طباعة "Service 1" ، والثاني - "Service 2" . للقيام بذلك ، تحتاج إلى توفير تطبيقين آخرين IService في plugin1 و plugin2 على التوالي:



قم بإنشاء المكون الإضافي الأول بملفين:

 plugin1/ src/main/java/ org/example/pluginapp/plugin1/ Service1.java module-info.java 

ملف Service1.java :

 public class Service1 implements IService { @Override public void doJob() { System.out.println("Service 1"); } } 

ملف module-info.java :

 module plugin1 { requires core; provides IService with Service1; } 

لاحظ أن البرنامج plugin1 يعتمد على core . هذا هو مبدأ انقلاب التبعية الذي ذكرته سابقًا: النواة لا تعتمد على المكونات الإضافية ، ولكن بالعكس.

المكوّن الإضافي الثاني مشابه تمامًا للمكوّن الأول ، لذا لن أعطيها هنا.

الآن ، دعونا نجمع الإضافات ونضعها في مجلد plugins التطبيق:

 > java --module-path core.jar --module core Service 1 Service 2 Basic service 

الصيحة ، تم التقاط الإضافات! كيف حدث هذا:

  1. عثر Java على وحدتين في مجلد plugins .
  2. تم إنشاء طبقة باستخدام وحدتي plugins1 و plugins2 .
  3. بدأ IService البحث عن جميع IService .
  4. في طبقة البرنامج المساعد ، وجد IService لخدمة IService .
  5. بعد ذلك ، واصل البحث في الطبقة BasicService (أي طبقة Boot ) ووجد تطبيقًا واحدًا لـ BasicService في الوحدة النمطية core .
  6. كل التطبيقات التي تم العثور عليها بها طريقة doJob() تسمى.

لاحظ أنه على وجه التحديد لأن البحث عن موفري الخدمة يبدأ بطبقات فرعية ، ثم ينتقل إلى الطبقات الأصل ، ثم "Service 2" طباعة "Service 1" و "Service 2" أولاً ، ثم "Basic Service" . إذا كنت ترغب في فرز الخدمات بحيث تبدأ الخدمات الأساسية أولاً ، ثم الإضافات ، فيمكنك تعديل طريقة IService.getServices() عن طريق إضافة الفرز هناك (قد تحتاج إلى إضافة طريقة int getOrdering() إلى واجهة IService ).

النتائج


لذلك ، أوضحت كيف يمكنك تنظيم تطبيق Java الإضافي بسرعة وكفاءة مع الخصائص التالية:

  • البساطة: بالنسبة لنقاط الامتداد وربطها ، يتم استخدام ميزات Java الأساسية فقط (واجهات ، وفئات ، و ServiceLoader ) ، بدون أطر عمل ، وانعكاسات ، وتعليقات توضيحية ، ومحملين للفئة.
  • التصريح: يتم توضيح نقاط التمديد في واصفات الوحدة. مجرد إلقاء نظرة على module-info.java وفهم نقاط التمديد الموجودة وما المكونات الإضافية التي تساهم في هذه النقاط.
  • ضمانات ثابتة: في حالة وجود أخطاء في واصفات الوحدة ، لن يتم تجميع البرنامج. أيضًا ، كمكافأة ، إذا كنت تستخدم IntelliJ IDEA ، فستتلقى تحذيرات إضافية (على سبيل المثال ، إذا نسيت استخدام uses واستخدام ServiceLoader.load() )
  • الأمان: يتحقق نظام Java المعياري عند بدء التشغيل من أن تكوين الوحدات صحيح ويرفض تنفيذ البرنامج في حالة وجود أخطاء.

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

قررت إثارة هذا الموضوع لأني على مدار الأعوام السبعة الماضية كنت أكتب تطبيقًا معياريًا باستخدام Eclipse RCP ، حيث يتم استخدام OSGi الشهير كنظام في المكونات ، ويتم كتابة واصفات المكونات الإضافية في XML. لدينا أكثر من مائة مكون إضافي وما زلنا جالسين على Java 8. ولكن حتى لو انتقلنا إلى الإصدار الجديد من Java ، فمن غير المرجح أن نستخدم وحدات Java ، نظرًا لأنهم مرتبطون بشدة بـ OSGi.

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

باختصار عني


أقوم بالبرمجة لأكثر من 10 سنوات (8 منها في Java) ، أجيب على StackOverflow وأدير قناتي في Telegram المخصصة لجافا.

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


All Articles