התבונן לעומק בממשק ה- API של Reflection Java

בחודש שעבר, "Java In-Depth", דיברתי על התבוננות פנימית ודרכים בהן שיעור ג'אווה עם גישה לנתוני מחלקה גולמיים יכול להסתכל "בתוך" כיתה ולהבין כיצד בנוי הכיתה. יתר על כן, הראיתי כי בתוספת מטעין כיתות, ניתן להעלות את הכיתות הללו לסביבת הריצה ולהוציא אותן לפועל. דוגמה זו היא סוג של התבוננות פנימית סטטית . החודש אסתכל על ה- API של רפלקציה של Java, שמעניק לשיעורי Java את היכולת לבצע התבוננות פנימית דינמית : היכולת להסתכל בתוך שיעורים שכבר נטענים.

התועלת של התבוננות פנימית

אחת החוזקות של ג'אווה היא שהיא תוכננה מתוך הנחה שהסביבה בה היא פועלת תשתנה באופן דינמי. הכיתות נטענות באופן דינמי, הכריכה נעשית באופן דינמי, ומופעי אובייקט נוצרים באופן דינמי בזמן הצורך. מה שלא היה דינמי מאוד מבחינה היסטורית הוא היכולת לתפעל שיעורים "אנונימיים". בהקשר זה, מחלקה אנונימית היא קבוצה הנטענת או מוצגת למחלקה Java בזמן הריצה וסוגיה לא היה ידוע בעבר לתוכנית Java.

שיעורים אנונימיים

קשה להסביר תמיכה בשיעורים אנונימיים וקשה עוד יותר לעצב אותם בתוכנית. האתגר בתמיכה בכיתה אנונימית יכול להיאמר כך: "כתוב תוכנית שכאשר ניתן לה אובייקט ג'אווה, היא יכולה לשלב את האובייקט במבצעו המתמשך." הפיתרון הכללי הוא קשה למדי, אך על ידי הגבלת הבעיה ניתן ליצור כמה פתרונות מיוחדים. ישנן שתי דוגמאות לפתרונות מיוחדים למחלקת בעיה זו בגירסת 1.0 של Java: יישומוני Java וגרסת שורת הפקודה של מתורגמן Java.

יישומוני Java הם מחלקות Java שנטענות על ידי מכונה וירטואלית פועלת בהקשר של דפדפן אינטרנט ומופעלת. שיעורי ג'אווה אלה הם אנונימיים מכיוון שזמן הריצה אינו יודע מבעוד מועד את המידע הדרוש להפעלת כל כיתה בודדת. עם זאת, הבעיה של הפעלת מחלקה מסוימת נפתרת באמצעות מחלקת Java java.applet.Applet.

מחלקות על נפוצות, כמו Appletממשקי Java, כמו AppletContext, מטפלות בבעיה של שיעורים אנונימיים על ידי יצירת חוזה עליו סוכם בעבר. באופן ספציפי, ספק סביבת זמן ריצה מפרסם שהיא יכולה להשתמש בכל אובייקט התואם לממשק מוגדר, וצרכנית סביבת זמן הריצה משתמשת בממשק שצוין בכל אובייקט שהוא מתכוון לספק לזמן הריצה. במקרה של יישומונים, ממשק מוגדר היטב קיים בצורה של מעמד-על נפוץ.

החיסרון של פתרון מעמד-על נפוץ, במיוחד בהעדר ירושה מרובה, הוא שלא ניתן להשתמש באובייקטים שנבנו להפעלה בסביבה גם במערכת אחרת, אלא אם כן המערכת הזו מיישמת את כל החוזה. במקרה של Appletממשקים, סביבת האירוח חייבת ליישם AppletContext. המשמעות של פתרון היישומון היא שהפתרון פועל רק כאשר אתה טוען יישומונים. אם תשים מופע של Hashtableאובייקט בדף האינטרנט שלך ותפנה אליו את הדפדפן שלך, הוא לא יטען כי מערכת היישומון לא יכולה לפעול מחוץ לטווח המוגבל שלה.

בנוסף לדוגמת היישומון, התבוננות פנימית עוזרת לפתור בעיה שהזכרתי בחודש שעבר: להבין כיצד להתחיל לבצע בכיתה שגרסת שורת הפקודה של המכונה הווירטואלית ג'אווה נטענת זה עתה. בדוגמה זו, על המכונה הווירטואלית להפעיל שיטה סטטית כלשהי במחלקה הטעונה. לפי האמנה, שיטה זו נקראת mainולוקחת טיעון יחיד - מערך Stringאובייקטים.

המוטיבציה לפיתרון דינמי יותר

האתגר בארכיטקטורת Java 1.0 הקיימת הוא שקיימות בעיות שיכולות להיפתר על ידי סביבת התבוננות פנימית דינמית יותר - כגון רכיבי ממשק משתמש הניתנים לטעינה, מנהלי התקנים הניתנים לטעינה במערכת הפעלה מבוססת Java וסביבות עריכה הניתנות להגדרה דינמית. "אפליקציית הרוצח", או הבעיה שגרמה ליצירת ה- API של השתקפות Java, הייתה פיתוח של מודל רכיב אובייקט עבור Java. מודל זה מכונה כיום JavaBeans.

רכיבי ממשק משתמש הם נקודת תכנון אידיאלית עבור מערכת התבוננות פנימית מכיוון שיש להם שני צרכנים שונים מאוד. מצד אחד, אובייקטים הרכיב מקושרים יחד כדי ליצור ממשק משתמש כחלק מיישום כלשהו. לחלופין, צריך להיות ממשק לכלים שמטפלים ברכיבי משתמשים מבלי לדעת מה הם הרכיבים, או, חשוב מכך, ללא גישה לקוד המקור של הרכיבים.

ממשק ה- API של Java Reflection צמח מהצרכים של ה- API של רכיב ממשק המשתמש של JavaBeans.

מהי השתקפות?

באופן בסיסי, ה- Reflection API מורכב משני רכיבים: אובייקטים המייצגים את החלקים השונים של קובץ מחלקה, ואמצעי לחילוץ אותם אובייקטים בצורה בטוחה ומאובטחת. זה האחרון חשוב מאוד, מכיוון שג'אווה מספקת אמצעי אבטחה רבים, ולא יהיה זה הגיוני לספק קבוצה של מחלקות שמבטלות את האמצעים הללו.

המרכיב הראשון של ה- Reflection API הוא המנגנון המשמש לאיסוף מידע על מחלקה. מנגנון זה מובנה בכיתה ששמה Class. המחלקה המיוחדת Classהיא הסוג האוניברסלי למידע המטא המתאר אובייקטים במערכת Java. מעמיסי מחלקות במערכת Java מחזירים אובייקטים מהסוג Class. עד כה שלוש השיטות המעניינות ביותר בשיעור זה היו:

  • forName, אשר יטען מחלקה עם שם נתון, באמצעות מטעין המחלקה הנוכחי

  • getName, שיחזיר את שם המחלקה Stringכאובייקט, שהיה שימושי לזיהוי הפניות לאובייקטים לפי שם המחלקה שלהם

  • newInstance, אשר יפעיל את קונסטרוקטור האפס במחלקה (אם הוא קיים) ויחזיר לך מופע אובייקט של מחלקת אובייקט זו

לשלוש שיטות שימושיות אלה ה- Reflection API מוסיף כמה שיטות נוספות למחלקה Class. אלה הם כדלקמן:

  • getConstructor, getConstructors,getDeclaredConstructor
  • getMethod, getMethods,getDeclaredMethods
  • getField, getFields,getDeclaredFields
  • getSuperclass
  • getInterfaces
  • getDeclaredClasses

בנוסף לשיטות אלה, נוספו מחלקות חדשות רבות לייצוג האובייקטים שהשיטות הללו יחזירו. השיעורים החדשים בעיקר הם חלק java.lang.reflectמהחבילה, אבל כמה שיעורים הבסיסיים הסוג החדש ( Void, Byteוכן הלאה) נמצאים java.langבחבילה. ההחלטה התקבלה להציב את הכיתות החדשות במקום בו הם נמצאים על ידי הצבת כיתות שייצגו מטא-נתונים בחבילת ההשתקפות ושיעורים שייצגו סוגים בחבילת השפה.

לפיכך, ה- Reflection API מייצג מספר שינויים בכיתה Classהמאפשרים לך לשאול שאלות על פנימיות הכיתה, וחבורה של שיעורים המייצגים את התשובות שהשיטות החדשות הללו נותנות לך.

כיצד אוכל להשתמש ב- Reflection API?

השאלה "כיצד אוכל להשתמש בממשק ה- API?" היא אולי השאלה המעניינת יותר מ"מהי השתקפות? "

ממשק ה- API של השתקפות הוא סימטרי , כלומר אם אתה מחזיק Classאובייקט, אתה יכול לשאול על הפנימיות שלו, ואם יש לך אחד הפנימי, אתה יכול לשאול אותו באיזו מחלקה הכריז עליו. כך תוכלו לעבור הלוך ושוב ממעמד לשיטה לפרמטר למחלקה לשיטה וכן הלאה. שימוש מעניין אחד בטכנולוגיה זו הוא לגלות את רוב התלות ההדדית בין כיתה נתונה לשאר המערכת.

דוגמא עובדת

ברמה המעשית יותר, עם זאת, תוכלו להשתמש בממשק ה- API של השתקפות בכדי להוריד כיתה, בדיוק כפי dumpclassשעשתה הכיתה שלי בעמודה בחודש שעבר.

כדי להדגים את ה- Reflection API, כתבתי מחלקה בשם ReflectClassשתיקח מחלקה הידועה בזמן הריצה של Java (כלומר זה נמצא בדרך שלך בכיתה איפשהו), ובאמצעות ה- Reflection API, תשליך את המבנה שלה לחלון המסוף. כדי להתנסות בכיתה זו תצטרך שתהיה לך גרסת 1.1 של ה- JDK.

הערה: אל לא מנסה להשתמש זמן 1.0 ריצה, תוך שהיא מקבלת את כול מבולבלת, בדרך כלל וכתוצאה מכך חריג שינוי המעמד עולה בקנה אחד.

השיעור ReflectClassמתחיל באופן הבא:

ייבא java.lang.reflect. *; ייבא java.util. *; מחלקה ציבורית ReflectClass {

כפי שניתן לראות לעיל, הדבר הראשון שהקוד עושה הוא לייבא את מחלקות ה- Reflection API. לאחר מכן, הוא קופץ ישר לשיטה העיקרית, שמתחילה כפי שמוצג להלן.

סטטי ציבורי ריק ריק (מחרוזת טוענת []) {קונסטרוקטור cn []; מחלקה סמ"ק []; שיטה מ"מ []; שדה ff []; מחלקה c = null; Class supClass; מחרוזת x, y, s1, s2, s3; Hashtable classRef = חדש Hashtable (); if (args.length == 0) {System.out.println ("אנא ציין שם מחלקה בשורת הפקודה."); System.exit (1); } נסה {c = Class.forName (args [0]); } לתפוס (ClassNotFoundException ee) {System.out.println ("לא ניתן היה למצוא מחלקה '" + טענות [0] + "'"); System.exit (1); }

השיטה mainמצהירה על מערכים של בונים, שדות ושיטות. אם אתה זוכר, אלה שלושה מתוך ארבעת החלקים הבסיסיים של תיק הכיתה. החלק הרביעי הוא התכונות, שלרוע המזל ממשק ה- Reflection אינו נותן לך גישה אליהן. לאחר המערכים ביצעתי עיבוד שורת פקודה. אם המשתמש הקליד שם מחלקה, הקוד מנסה לטעון אותו forNameבשיטת המחלקה Class. forNameהשיטה לוקחת שמות בכיתה Java, לא שמות קבצים, לכן כדי לחפש בתוך java.math.BigIntegerהכיתה, אתה פשוט להקליד "Java ReflectClass java.math.BigInteger," ולא נקודה איפה את הקובץ בכיתה למעשה מאוחסן.

זיהוי החבילה של הכיתה

בהנחה שקובץ הכיתה נמצא, הקוד ימשיך לשלב 0, שמוצג להלן.

 /* * Step 0: If our name contains dots we're in a package so put * that out first. */ x = c.getName(); y = x.substring(0, x.lastIndexOf(".")); if (y.length() > 0) { System.out.println("package "+y+";\n\r"); } 

In this step, the name of the class is retrieved using the getName method in class Class. This method returns the fully qualified name, and if the name contains dots, we can presume that the class was defined as part of a package. So Step 0 is to separate the package name part from the class name part, and print out the package name part on a line that starts with "package...."

Collecting class references from declarations and parameters

With the package statement taken care of, we proceed to Step 1, which is to collect all of the other class names that are referenced by this class. This collection process is shown in the code below. Remember that the three most common places where class names are referenced are as types for fields (instance variables), return types for methods, and as the types of the parameters passed to methods and constructors.

 ff = c.getDeclaredFields(); for (int i = 0; i < ff.length; i++) { x = tName(ff[i].getType().getName(), classRef); } 

In the above code, the array ff is initialized to be an array of Field objects. The loop collects the type name from each field and process it through the tName method. The tName method is a simple helper that returns the shorthand name for a type. So java.lang.String becomes String. And it notes in a hashtable which objects have been seen. At this stage, the code is more interested in collecting class references than in printing.

The next source of class references are the parameters supplied to constructors. The next piece of code, shown below, processes each declared constructor and collects the references from the parameter lists.

 cn = c.getDeclaredConstructors(); for (int i = 0; i  0) { for (int j = 0; j < cx.length; j++) { x = tName(cx[j].getName(), classRef); } } } 

As you can see, I've used the getParameterTypes method in the Constructor class to feed me all of the parameters that a particular constructor takes. These are then processed through the tName method.

An interesting thing to note here is the difference between the method getDeclaredConstructors and the method getConstructors. Both methods return an array of constructors, but the getConstructors method only returns those constructors that are accessible to your class. This is useful if you want to know if you actually can invoke the constructor you've found, but it isn't useful for this application because I want to print out all of the constructors in the class, public or not. The field and method reflectors also have similar versions, one for all members and one only for public members.

השלב האחרון, המוצג להלן, הוא איסוף ההפניות מכל השיטות. קוד זה צריך לקבל הפניות הן מסוג השיטה (בדומה לשדות לעיל) והן מהפרמטרים (בדומה לבונים לעיל).

mm = c.getDeclaredMethods (); עבור (int i = 0; i 0) {for (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}}

בקוד לעיל ישנן שתי קריאות tName- אחת לאיסוף סוג ההחזרה ואחת לאיסוף כל סוג פרמטר.