CodeGym /مدونة جافا /Random-AR /واجهة برمجة تطبيقات الانعكاس: الانعكاس. الجانب المظلم من ...
John Squirrels
مستوى
San Francisco

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

نشرت في المجموعة
تحياتي لك يا شاب باداوان. في هذه المقالة، سأخبرك عن القوة، وهي قوة يستخدمها مبرمجو Java فقط في المواقف التي تبدو مستحيلة. الجانب المظلم من Java هو Reflection API. في Java، يتم تنفيذ الانعكاس باستخدام Java Reflection API.

ما هو انعكاس جافا؟

يوجد تعريف قصير ودقيق وشائع على الإنترنت. الانعكاس ( من أواخر اللاتينية reflexio - العودة إلى الوراء ) هو آلية لاستكشاف البيانات حول البرنامج أثناء تشغيله. يتيح لك الانعكاس استكشاف المعلومات حول الحقول والأساليب ومنشئي الفصل. يتيح لك الانعكاس العمل مع الأنواع التي لم تكن موجودة في وقت الترجمة، ولكنها أصبحت متاحة أثناء وقت التشغيل. إن الانعكاس والنموذج المتسق منطقيًا لإصدار معلومات الخطأ يجعل من الممكن إنشاء كود ديناميكي صحيح. بمعنى آخر، إن فهم كيفية عمل الانعكاس في Java سيفتح لك عددًا من الفرص المذهلة. يمكنك التوفيق بين الفئات ومكوناتها حرفيًا. فيما يلي قائمة أساسية بما يسمح به الانعكاس:
  • تعلم/تحديد فئة الكائن؛
  • الحصول على معلومات حول معدّلات الفصل والحقول والأساليب والثوابت والمنشئات والفئات الفائقة؛
  • تعرف على الأساليب التي تنتمي إلى الواجهة (الواجهات) المنفذة؛
  • إنشاء مثيل لفئة اسم الفئة الخاص بها غير معروف حتى وقت التشغيل؛
  • الحصول على قيم حقول الكائن وتعيينها بالاسم؛
  • استدعاء أسلوب الكائن بالاسم.
يتم استخدام الانعكاس في جميع تقنيات 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 لحقل الاسم . يتم تمييز حقل الاسم نفسه باستخدام معدل الوصول الخاص : لا يمكننا الوصول إليه خارج الفئة نفسها مما يعني أنه لا يمكننا استرداد قيمته . " إذا ما هي المشكلة ؟" قول انت. "أضف أداة getter أو قم بتغيير معدّل الوصول". وستكون على حق، إلا إذا كنت في مكتبة AAR مجمعة أو في وحدة نمطية خاصة أخرى دون القدرة على إجراء تغييرات. في الممارسة العملية، يحدث هذا طوال الوقت. وقد نسي بعض المبرمجين المهملين كتابة حرف getter . هذا هو الوقت المناسب لتذكر التفكير! دعنا نحاول الوصول إلى حقل الاسم الخاص للفصل : MyClassMyClass
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
}
دعونا نحلل ما حدث للتو. يوجد في Java فئة رائعة تسمى Class. وهو يمثل الفئات والواجهات في تطبيق Java القابل للتنفيذ. لن نغطي العلاقة بين Classو ClassLoader، لأن هذا ليس موضوع هذه المقالة. بعد ذلك، لاسترداد حقول هذه الفئة، تحتاج إلى استدعاء getFields()الأسلوب. ستعيد هذه الطريقة جميع الحقول التي يمكن الوصول إليها لهذا الفصل. هذا لا يناسبنا، لأن مجالنا خاص ، لذلك نستخدم getDeclaredFields()الطريقة. تقوم هذه الطريقة أيضًا بإرجاع مجموعة من حقول الفئات، ولكنها الآن تتضمن حقولاً خاصة ومحمية . في هذه الحالة، نعرف اسم الحقل الذي نهتم به، حتى نتمكن من استخدام الطريقة ، أين هو اسم الحقل المطلوب. getDeclaredField(String)Stringملحوظة: getFields()ولا getDeclaredFields()تقم بإرجاع حقول الفصل الأصلي! عظيم. لقد حصلنا على Fieldكائن يشير إلى اسمنا . نظرًا لأن الحقل لم يكن عامًا ، فيجب علينا منح حق الوصول للعمل به. تتيح لنا الطريقة setAccessible(true)المضي قدمًا. الآن أصبح حقل الاسم تحت سيطرتنا الكاملة! يمكنك استرداد قيمته عن طريق استدعاء أسلوب Fieldالكائن get(Object)، حيث Objectيوجد مثيل لفئتنا MyClass. نقوم بتحويل النوع إلى متغير الاسمString وتعيين القيمة له . إذا لم نتمكن من العثور على أداة ضبط لتعيين قيمة جديدة لحقل الاسم، فيمكنك استخدام التابع set :
field.set(myClass, (String) "new value");
تهانينا! لقد أتقنت للتو أساسيات التفكير ووصلت إلى مجال خاص ! انتبه إلى try/catchالكتلة وأنواع الاستثناءات التي تتم معالجتها. سيخبرك IDE بأن وجودهم مطلوب من تلقاء نفسه، ولكن يمكنك أن تعرف بوضوح من خلال أسمائهم سبب وجودهم هنا. المضي قدما! كما لاحظت، فإن فصلنا MyClassلديه بالفعل طريقة لعرض معلومات حول بيانات الفصل:
private void printData(){
       System.out.println(number + name);
   }
لكن هذا المبرمج ترك بصمات أصابعه هنا أيضاً. تحتوي الطريقة على معدل وصول خاص ، وعلينا كتابة الكود الخاص بنا لعرض البيانات في كل مرة. ما هذه الفوضى. أين ذهب تفكيرنا؟ اكتب الدالة التالية:
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(Object, Args)الطريقة، حيث Objectيوجد أيضًا مثيل للفئة MyClass. Argsهي وسيطات الطريقة، على الرغم من أن وسيطاتنا لا تحتوي على أي منها. الآن نستخدم printDataالدالة لعرض المعلومات:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 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
}
يا هلا! الآن لدينا إمكانية الوصول إلى الطريقة الخاصة للفصل. ولكن ماذا لو كانت الطريقة تحتوي على وسيطات، ولماذا يتم التعليق على المنشئ؟ كل شيء في وقته المناسب. يتضح من التعريف في البداية أن الانعكاس يتيح لك إنشاء مثيلات لفئة في وقت التشغيل (أثناء تشغيل البرنامج)! يمكننا إنشاء كائن باستخدام الاسم الكامل للفئة. الاسم الكامل للفئة هو اسم الفئة، بما في ذلك مسار الحزمة الخاصة بها .
واجهة برمجة تطبيقات الانعكاس: الانعكاس.  الجانب المظلم من جافا - 2
في التسلسل الهرمي لحزمتي ، سيكون الاسم الكامل لـ MyClass هو "reflection.MyClass". هناك أيضًا طريقة بسيطة لمعرفة اسم الفصل (إرجاع اسم الفصل كسلسلة):
MyClass.class.getName()
دعونا نستخدم انعكاس Java لإنشاء مثيل للفئة:
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الدراسي، فلن ClassLoaderيقوم المسؤول عن تحميل الفئات في JVM بتحميل الفصل مطلقًا. هذا يعني أنه يتعين عليك فرض ClassLoaderتحميله والحصول على وصف للفئة في شكل متغير Class. ولهذا السبب لدينا forName(String)الطريقة، أين Stringاسم الفئة التي نحتاج إلى وصفها. بعد الحصول على Сlassالكائن، سيؤدي استدعاء الطريقة newInstance()إلى إرجاع Objectكائن تم إنشاؤه باستخدام هذا الوصف. كل ما تبقى هو توفير هذا الكائن لفصلنا MyClass. رائع! لقد كان ذلك صعبًا، ولكنني آمل أن يكون مفهومًا. الآن يمكننا إنشاء مثيل لفئة في سطر واحد حرفيًا! لسوء الحظ، لن يعمل النهج الموضح إلا مع المُنشئ الافتراضي (بدون معلمات). كيف يمكنك استدعاء الأساليب والمنشئات مع المعلمات؟ لقد حان الوقت لإلغاء التعليق على منشئنا. كما هو متوقع، newInstance()لا يمكن العثور على المنشئ الافتراضي، ولم يعد يعمل. دعونا نعيد كتابة مثيل الفصل:
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طرق الاتصال. وهذا يطرح السؤال: متى يكون استدعاء المنشئين من خلال التفكير مفيدًا؟ كما ذكرنا سابقًا، لا يمكن لتقنيات Java الحديثة الاستمرار بدون Java Reflection API. على سبيل المثال، Dependency حقن (DI)، الذي يجمع بين التعليقات التوضيحية وانعكاس الأساليب والمنشئات لتشكيل مكتبة Darer الشهيرة لتطوير Android. بعد قراءة هذه المقالة، يمكنك أن تعتبر نفسك بكل ثقة متعلمًا بطرق Java Reflection API. إنهم لا يسمون الانعكاس بالجانب المظلم لجافا من أجل لا شيء. إنه يكسر نموذج OOP تمامًا. في Java، يقوم التغليف بإخفاء وتقييد وصول الآخرين إلى مكونات برنامج معينة. عندما نستخدم المُعدِّل الخاص، فإننا نعتزم الوصول إلى هذا الحقل فقط من داخل الفصل الذي يوجد فيه. ونحن نبني البنية اللاحقة للبرنامج على أساس هذا المبدأ. في هذه المقالة، رأينا كيف يمكنك استخدام الانعكاس لشق طريقك في أي مكان. يعد نمط التصميم الإبداعي Singleton مثالاً جيدًا على ذلك كحل معماري. الفكرة الأساسية هي أن الفصل الذي ينفذ هذا النمط سيكون له مثيل واحد فقط أثناء تنفيذ البرنامج بأكمله. يتم تحقيق ذلك عن طريق إضافة معدل الوصول الخاص إلى المنشئ الافتراضي. وسيكون الأمر سيئًا للغاية إذا استخدم المبرمج الانعكاس لإنشاء المزيد من مثيلات هذه الفئات. بالمناسبة، سمعت مؤخرًا أحد زملاء العمل يطرح سؤالاً مثيرًا للاهتمام: هل يمكن وراثة الفصل الذي يطبق نمط Singleton؟ فهل يمكن، في هذه الحالة، أن يكون حتى التفكير عاجزًا؟ اترك تعليقاتك حول المقال وإجابتك في التعليقات أدناه، واطرح أسئلتك هناك!

المزيد من القراءة:

تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION