JavaRush /مدونة جافا /Random-AR /واجهة برمجة تطبيقات الانعكاس. انعكاس. الجانب المظلم من جا...
Oleksandr Klymenko
مستوى
Харків

واجهة برمجة تطبيقات الانعكاس. انعكاس. الجانب المظلم من جافا

نشرت في المجموعة
تحياتي لك يا شاب باداوان. سأخبرك في هذه المقالة عن القوة، التي يستخدمها مبرمجو جافا فقط في المواقف التي تبدو ميؤوس منها. لذا، الجانب المظلم لجافا هو -Reflection API
واجهة برمجة تطبيقات الانعكاس.  انعكاس.  الجانب المظلم من جافا - 1
يتم الانعكاس في Java باستخدام Java Reflection API. ما هو هذا الانعكاس؟ هناك تعريف قصير ودقيق شائع أيضًا على الإنترنت. الانعكاس (من الانعكاس اللاتيني المتأخر - العودة) هو آلية لدراسة البيانات حول البرنامج أثناء تنفيذه. يسمح لك الانعكاس بفحص المعلومات حول الحقول والأساليب ومنشئي الفصل. تتيح لك آلية الانعكاس نفسها معالجة الأنواع المفقودة أثناء التجميع، ولكنها تظهر أثناء تنفيذ البرنامج. إن الانعكاس ووجود نموذج متماسك منطقيًا للإبلاغ عن الأخطاء يجعل من الممكن إنشاء كود ديناميكي صحيح. بمعنى آخر، إن فهم كيفية عمل الانعكاس في لغة جافا يفتح لك عددًا من الفرص المذهلة. يمكنك التوفيق بين الفئات ومكوناتها حرفيًا.
واجهة برمجة تطبيقات الانعكاس.  انعكاس.  الجانب المظلم من جافا - 2
فيما يلي قائمة أساسية بما يسمح به الانعكاس:
  • اكتشف/حدد فئة الكائن؛
  • الحصول على معلومات حول معدّلات الفئة والحقول والأساليب والثوابت والمنشئات والفئات الفائقة؛
  • تعرف على الطرق التي تنتمي إلى الواجهة/الواجهات المنفذة؛
  • إنشاء مثيل لفئة، واسم الفئة غير معروف حتى يتم تنفيذ البرنامج؛
  • الحصول على قيمة حقل الكائن وتعيينه بالاسم؛
  • استدعاء أسلوب الكائن بالاسم.
يتم استخدام الانعكاس في جميع تقنيات Java الحديثة تقريبًا. من الصعب أن نتخيل ما إذا كانت Java كمنصة يمكن أن تحقق مثل هذا الاعتماد الهائل دون تفكير. على الأرجح لم أستطع. لقد أصبحت على دراية بالفكرة النظرية العامة للانعكاس، فلننتقل الآن إلى تطبيقها العملي! لن ندرس جميع أساليب Reflection API، بل سندرس فقط ما يتم مواجهته فعليًا في الممارسة العملية. بما أن آلية الانعكاس تتضمن العمل مع الصفوف، سيكون لدينا صنف بسيط - MyClass:
public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
كما نرى، هذه هي الفئة الأكثر شيوعا. تم التعليق على المنشئ ذو المعلمات لسبب ما، وسنعود إليه لاحقًا. إذا نظرت عن كثب إلى محتويات الفصل، فمن المحتمل أنك رأيت غياب getter"a" لـ name. تم وضع علامة على الحقل نفسه nameباستخدام معدل الوصول private، ولن نتمكن من الوصول إليه خارج الفصل نفسه، =>ولا يمكننا الحصول على قيمته. "إذا ما هي المشكلة؟ - قول انت. "إضافة getterأو تغيير معدّل الوصول." وستكون على حق، ولكن ماذا لو MyClassكان ذلك في مكتبة AAR مجمعة أو في وحدة مغلقة أخرى دون الوصول إلى التحرير، وفي الممارسة العملية يحدث هذا كثيرًا. وبعض المبرمجين الغافلين نسيوا الكتابة ببساطة getter. حان الوقت لنتذكر التأمل! دعنا نحاول الوصول إلى privateحقل nameالفصل MyClass:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //no getter =(
   System.out.println(number + name);//output 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name);//output 0default
}
دعونا معرفة ما حدث هنا الآن. هناك فئة رائعة في جافا Class. وهو يمثل الفئات والواجهات في تطبيق Java القابل للتنفيذ. لن نتطرق إلى العلاقة بين Classو ClassLoader. هذا ليس موضوع المقال. بعد ذلك، للحصول على حقول هذه الفئة، تحتاج إلى استدعاء الطريقة getFields()، وستعيد هذه الطريقة إلينا جميع الحقول المتاحة للفئة. هذا غير مناسب لنا، نظرًا لأن الحقل الخاص بنا هو private، لذلك نستخدم الطريقة getDeclaredFields()، تقوم هذه الطريقة أيضًا بإرجاع مصفوفة من حقول الفئات، ولكن الآن كلاهما privateو protected. في حالتنا، نعرف اسم الحقل الذي يهمنا، ويمكننا استخدام الطريقة getDeclaredField(String)، أين Stringاسم الحقل المطلوب. ملحوظة: getFields()ولا getDeclaredFields()تعيد حقول الفصل الأصلي! عظيم، لقد تلقينا كائنًا Field يحتوي على رابط إلى name. لأن لم يكن الحقل публичным(عامًا)، فيجب منح الوصول للعمل معه. هذه الطريقة setAccessible(true)تسمح لنا بمواصلة العمل. الآن أصبح الميدان nameتحت سيطرتنا بالكامل! يمكنك الحصول على قيمته عن طريق استدعاء get(Object)الكائن Fieldحيث Objectيوجد مثيل لفئتنا MyClass. نحن نلقيها Stringونخصصها لمتغيرنا name. في حالة عدم وجود setter"a" فجأة، يمكننا استخدام الطريقة لتعيين قيمة جديدة لحقل الاسم set:
field.set(myClass, (String) "new value");
تهانينا! لقد أتقنت للتو الآلية الأساسية للتفكير وتمكنت من الوصول إلى الميدان private! انتبه إلى الكتلة try/catchوأنواع الاستثناءات التي تمت معالجتها. سيشير IDE نفسه إلى وجودهم الإلزامي، لكن اسمهم يوضح سبب وجودهم هنا. تفضل! كما لاحظت، لدينا MyClassبالفعل طريقة لعرض معلومات حول بيانات الفصل:
private void printData(){
       System.out.println(number + name);
   }
لكن هذا المبرمج ترك إرثًا هنا أيضًا. هذه الطريقة موجودة ضمن معدل الوصول private، وكان علينا كتابة كود الإخراج بأنفسنا في كل مرة. الأمر ليس على ما يرام، أين انعكاسنا؟... لنكتب الدالة التالية:
public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
الإجراء هنا هو نفسه تقريبًا كما هو الحال مع الحصول على حقل - نحصل على الطريقة المطلوبة بالاسم ونمنحها حق الوصول إليها. ولاستدعاء الكائن Methodالذي نستخدمه invoke(Оbject, Args)، يوجد Оbjectأيضًا مثيل للفئة MyClass. Args- وسيطات الطريقة - ليس لدينا أي منها. الآن نستخدم الدالة لعرض المعلومات printData:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0new value
}
حسنًا، أصبح لدينا الآن إمكانية الوصول إلى الطريقة الخاصة للفصل. ولكن ماذا لو كانت الطريقة لا تزال تحتوي على وسيطات، ولماذا يوجد مُنشئ تم التعليق عليه؟ كل شيء له وقته. يتضح من التعريف في البداية أن الانعكاس يسمح لك بإنشاء مثيلات لفئة في الوضع runtime(أثناء تشغيل البرنامج)! يمكننا إنشاء كائن من فئة بالاسم المؤهل بالكامل لتلك الفئة. اسم الفئة المؤهلة بالكامل هو اسم الفئة، مع تحديد المسار إليها في package.
واجهة برمجة تطبيقات الانعكاس.  انعكاس.  الجانب المظلم من جافا - 3
في التسلسل الهرمي الخاص بي، سيكون packageالاسم الكامل " ". يمكنك أيضًا معرفة اسم الفئة بطريقة بسيطة (ستعيد اسم الفئة كسلسلة): MyClassreflection.MyClass
MyClass.class.getName()
لنقم بإنشاء مثيل للفئة باستخدام الانعكاس:
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
في وقت بدء تشغيل تطبيق Java، لا يتم تحميل كافة الفئات في JVM. إذا كان الكود الخاص بك لا يشير إلى الفئة MyClass، فإن أي شخص مسؤول عن تحميل الفئات في JVM، ClassLoaderلن يقوم بتحميلها هناك أبدًا. لذلك، نحن بحاجة إلى إجبارها ClassLoaderعلى تحميل واستلام وصف لفصلنا في شكل متغير من النوع Class. لهذه المهمة، هناك طريقة forName(String)، حيث Stringيوجد اسم الفئة التي نطلب وصفها. بعد الاستلام ، سيعود Сlassاستدعاء الطريقة ، والذي سيتم إنشاؤه وفقًا لنفس الوصف. يبقى إحضار هذا الكائن إلى صفنا . رائع! كان الأمر صعبًا، لكني آمل أن يكون مفهومًا. الآن يمكننا إنشاء مثيل لفئة حرفيًا من سطر واحد! لسوء الحظ، فإن الطريقة الموضحة ستعمل فقط مع المنشئ الافتراضي (بدون معلمات). كيفية استدعاء الأساليب مع الحجج والمنشئات مع المعلمات؟ لقد حان الوقت لإلغاء التعليق على منشئنا. كما هو متوقع، لم يتم العثور على المنشئ الافتراضي ولم يعد يعمل. دعنا نعيد كتابة إنشاء مثيل الفصل: newInstance()ObjectMyClassnewInstance()
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
للحصول على منشئي الفئة، استدعاء الأسلوب من وصف الفئة getConstructors()، وللحصول على معلمات المنشئ، استدعاء getParameterTypes():
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
بهذه الطريقة نحصل على جميع المنشئين وجميع المعلمات لهم. في المثال الخاص بي، هناك استدعاء لمنشئ محدد بمعلمات محددة ومعروفة بالفعل. ولاستدعاء هذا المنشئ نستخدم الطريقة newInstanceالتي نحدد فيها قيم هذه المعلمات. سيحدث الشيء نفسه invokeبالنسبة لطرق الاتصال. السؤال الذي يطرح نفسه: أين يمكن أن تكون الدعوة التأملية للمنشئين مفيدة؟ تقنيات جافا الحديثة، كما ذكرنا في البداية، لا يمكنها الاستغناء عن واجهة برمجة التطبيقات Reflection API. على سبيل المثال، DI (حقن التبعية)، حيث تشكل التعليقات التوضيحية المدمجة مع انعكاس الأساليب والمنشئات مكتبة Dagger الشائعة في تطوير Android. بعد قراءة هذه المقالة، يمكنك أن تعتبر نفسك بكل ثقة مستنيرًا بآليات Reflection API. ليس من قبيل الصدفة أن يُطلق على الانعكاس اسم الجانب المظلم من جافا. إنه يكسر نموذج OOP تمامًا. في جافا، يعمل التغليف على إخفاء وتقييد وصول بعض مكونات البرنامج إلى مكونات أخرى. باستخدام المعدل الخاص، نعني أن الوصول إلى هذا الحقل سيكون فقط داخل الفصل الذي يوجد فيه هذا الحقل، وبناءً على ذلك نقوم ببناء البنية الإضافية للبرنامج. في هذه المقالة، رأينا كيف يمكنك استخدام التأمل للوصول إلى أي مكان. وخير مثال في شكل حل معماري هو نمط التصميم التوليدي - Singleton. فكرته الرئيسية هي أنه طوال فترة عمل البرنامج، يجب أن يكون للفصل الذي يقوم بتنفيذ هذا القالب نسخة واحدة فقط. يتم ذلك عن طريق تعيين معدل الوصول الافتراضي إلى خاص للمنشئ. وسيكون الأمر سيئًا للغاية إذا قام بعض المبرمجين بتفكيرهم الخاص بإنشاء مثل هذه الفئات. بالمناسبة، هناك سؤال مثير للاهتمام سمعته مؤخرًا من الموظف لدي: هل يمكن للفصل الذي يطبق القالب أن يكون له Singletonورثة؟ فهل من الممكن أن يكون حتى التفكير عاجزا في هذه الحالة؟ اكتب ملاحظاتك على المقال والإجابة في التعليقات، واطرح أسئلتك أيضًا! تأتي القوة الحقيقية لـ Reflection API مع التعليقات التوضيحية لوقت التشغيل، والتي ربما سنتحدث عنها في مقال مستقبلي حول الجانب المظلم لـ Java. شكرًا لكم على اهتمامكم!
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION