היסודות של מעמיסי מחלקות Java

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

מה מעמיסי כיתה עושים

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

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

מחלקה r = loadClass (מחרוזת className, resolutionIt בוליאני); 

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

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

מעמיס המחלקות הראשוני מיישם את יישום ברירת המחדל של loadClass () . לפיכך, קוד זה מבין כי שם המחלקה java.lang.Object נשמר בקובץ עם הקידומת java / lang / Object.class אי שם בנתיב הכיתה. קוד זה מיישם גם חיפוש נתיבי כיתה וגם בדיקת קבצי zip עבור שיעורים. הדבר המגניב ביותר באופן שבו תוכנן זה שג'אווה יכולה לשנות את דגם האחסון הכיתתי שלה פשוט על ידי שינוי מערך הפונקציות המיישם את מטעין הכיתה.

אם תחפרו במעיים של המכונה הווירטואלית ג'אווה, תגלו כי מעמיס הכיתה הראשוני מיושם בעיקר בפונקציות FindClassFromClass ו- ResolveClass .

אז מתי מעמיסים שיעורים? ישנם שני מקרים בדיוק: כאשר קוד הביצוע החדש מבוצע (למשל, FooClass f = FooClass חדש () ;) וכאשר קודני הביצוע מתייחסים למחלקה סטטית (לדוגמה, System. Out ).

מעמיס כיתות לא ראשוני

"אז מה?" אולי תשאל.

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

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

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

בניית SimpleClassLoader

מטעין כיתה מתחיל להיות תת-מחלקה של java.lang.ClassLoader . השיטה המופשטת היחידה שיש ליישם היא loadClass () . זרימת loadClass () היא כדלקמן:

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

SimpleClassLoader מופיע כדלקמן, עם תיאורים לגבי מה שהוא עושה משובץ בקוד.

Class loadClass מסונכרן בציבור (String className, resolutionIt בוליאני) זורק ClassNotFoundException {Class result; בתים classData []; System.out.println (">>>>>> מחלקת טעינה:" + className); / * בדוק את המטמון המקומי של המחלקות * / result = (Class) classes.get (className); אם (תוצאה! = null) {System.out.println (">>>>>> החזרת תוצאה במטמון."); תוצאת החזרה; }

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

/ * בדוק עם מטעין המחלקה הראשונית * / נסה {result = super.findSystemClass (className); System.out.println (">>>>>> מחלקת מערכת חוזרת (ב- CLASSPATH)."); תוצאת החזרה; } לתפוס (ClassNotFoundException e) {System.out.println (">>>>>> לא מחלקת מערכת."); }

כפי שניתן לראות בקוד לעיל, השלב הבא הוא לבדוק אם מעמיס הכיתה הראשוני יכול לפתור שם מחלקה זה. בדיקה זו חיונית הן לשפיות והן לביטחון המערכת. לדוגמא, אם תחזיר מופע משלך של java.lang.Object למתקשר, אז אובייקט זה לא ישתף אף מעמד-על משותף עם אובייקט אחר! ניתן לסכן את אבטחת המערכת אם מעמיס הכיתות שלך החזיר את הערך שלו java.lang.SecurityManager , שלא היו לו את אותם המחאות כמו של האמיתי.

/ * נסה לטעון אותו מהמאגר שלנו * / classData = getClassImplFromDataBase (className); אם (classData == null) {זרוק ClassNotFoundException חדש (); }

לאחר הבדיקות הראשוניות, אנו מגיעים לקוד שלמעלה שם מטעין הכיתה הפשוט מקבל הזדמנות לטעון יישום של מחלקה זו. ל- SimpleClassLoader יש שיטה getClassImplFromDataBase () שבדוגמה הפשוטה שלנו רק מקדימה את הספרייה "חנות" לשם הכיתה ומוסיפה את התוסף ".impl". בחרתי בטכניקה זו בדוגמה כך שלא תהיה שאלה שמטעין הכיתה הקדמוני ימצא את הכיתה שלנו. שים לב ש- sun.applet.AppletClassLoader מקדים את כתובת ה- URL של בסיס הקוד מדף ה- HTML שבו יישומון חי עד השם ואז HTTP מקבל בקשה להביא את קודי הביצה .

 / * הגדר אותו (מנתח את קובץ המחלקה) * / תוצאה = defineClass (classData, 0, classData.length); 

אם היישום של המחלקה נטען, השלב הלפני אחרון הוא להתקשר לשיטת defineClass () מ- java.lang.ClassLoader , שיכולה להיחשב כשלב הראשון של אימות מחלקה. שיטה זו מיושמת במכונה הווירטואלית של ג'אווה והיא אחראית לוודא שביתי המחלקה הם קובץ כיתת Java חוקי. באופן פנימי, שיטת defineClass ממלאת מבנה נתונים בו ה- JVM משתמש כדי לקיים שיעורים. אם נתוני המחלקה אינם תקינים , קריאה זו תגרום לזריקת ClassFormatError .

אם (resolutionIt) {resolClass (תוצאה); }

הדרישה הספציפית לטעינת המחלקה האחרונה היא להתקשר ל- resolClass () אם הפרמטר הבוליאני resolutionIt היה נכון. שיטה זו עושה שני דברים: ראשית, היא גורמת לטעון מפורשות כל מחלקות שמפנים אליהם מחלקה זו וליצור אובייקט אב-טיפוס למחלקה זו; לאחר מכן, הוא קורא למאמת לבצע אימות דינמי של הלגיטימיות של קודי ה- BYT בכיתה זו. אם האימות נכשל, קריאת שיטה זו תזרוק LinkageError , הנפוצה שבהן היא VerifyError .

שים לב כי עבור כל מחלקה שתטען, המשתנה resolutionIt תמיד יהיה נכון. רק כאשר המערכת קוראת רקורסיבית ל- loadClass () היא עשויה להגדיר את המשתנה הזה כ- false מכיוון שהיא יודעת שהמחלקה שהיא מבקשת כבר נפתרה.

class.put (className, תוצאה); System.out.println (">>>>>> מחזיר מחלקה טעונה לאחרונה."); תוצאת החזרה; }

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

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

שיקולי ביטחון

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

In our simple class loader, if the primordial class loader couldn't find the class, we loaded it from our private repository. What happens when that repository contains the class java.lang.FooBar ? There is no class named java.lang.FooBar, but we could install one by loading it from the class repository. This class, by virtue of the fact that it would have access to any package-protected variable in the java.lang package, can manipulate some sensitive variables so that later classes could subvert security measures. Therefore, one of the jobs of any class loader is to protect the system name space.

In our simple class loader we can add the code:

 if (className.startsWith("java.")) throw newClassNotFoundException(); 

just after the call to findSystemClass above. This technique can be used to protect any package where you are sure that the loaded code will never have a reason to load a new class into some package.

Another area of risk is that the name passed must be a verified valid name. Consider a hostile application that used a class name of "..\..\..\..\netscape\temp\xxx.class" as its class name that it wanted loaded. Clearly, if the class loader simply presented this name to our simplistic file system loader this might load a class that actually wasn't expected by our application. Thus, before searching our own repository of classes, it is a good idea to write a method that verifies the integrity of your class names. Then call that method just before you go to search your repository.

Using an interface to bridge the gap

The second non-intuitive issue with working with class loaders is the inability to cast an object that was created from a loaded class into its original class. You need to cast the object returned because the typical use of a custom class loader is something like:

 CustomClassLoader ccl = new CustomClassLoader(); Object o; Class c; c = ccl.loadClass("someNewClass"); o = c.newInstance(); ((SomeNewClass)o).someClassMethod(); 

However, you cannot cast o to SomeNewClass because only the custom class loader "knows" about the new class it has just loaded.

There are two reasons for this. First, the classes in the Java virtual machine are considered castable if they have at least one common class pointer. However, classes loaded by two different class loaders will have two different class pointers and no classes in common (except java.lang.Object usually). Second, the idea behind having a custom class loader is to load classes after the application is deployed so the application does not know a priory about the classes it will load. This dilemma is solved by giving both the application and the loaded class a class in common.

There are two ways of creating this common class, either the loaded class must be a subclass of a class that the application has loaded from its trusted repository, or the loaded class must implement an interface that was loaded from the trusted repository. This way the loaded class and the class that does not share the complete name space of the custom class loader have a class in common. In the example I use an interface named LocalModule, although you could just as easily make this a class and subclass it.

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

משחק עם הדוגמא

כדי לסכם את הדוגמא יצרתי עוד כמה

.java

קבצים. אלו הם:

ממשק ציבורי LocalModule {/ * התחל את המודול * / החלל ריק (אפשרות מחרוזת); }