מדריך למניפולציה של Java Bytecode עם ASM

1. הקדמה

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

2. תלות

עלינו להוסיף את התלות ASM שלנו pom.xml:

 org.ow2.asm asm 6.0 org.ow2.asm asm-util 6.0 

אנו יכולים להשיג את הגרסאות העדכניות ביותר של asm ו- asm-util מ- Maven Central.

3. יסודות ה- API של ASM

ה- ASM API מספק שני סגנונות של אינטראקציה עם שיעורי Java לשינוי וייצור: מבוסס אירועים ומבוסס על עצים.

3.1. ממשק API מבוסס אירועים

ה- API הזה הוא כבד מבוסס על ה אורח תבנית והוא דומה בתחושה לדגם הניתוח SAX של עיבוד מסמכי XML. הוא מורכב, בבסיסו, מהרכיבים הבאים:

  • ClassReader - מסייע בקריאת קבצי כיתות והוא תחילתו של שינוי כיתה
  • ClassVisitor - מספק את השיטות המשמשות לשינוי הכיתה לאחר קריאת קבצי המחלקה הגולמיים
  • ClassWriter - משמש להפקת התוצר הסופי של שינוי הכיתה

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

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

ה ClassVisitor שיטות ב- API מבוסס אירועים נקראות בסדר הבא:

בקר ב- visitSource? visitOuterClass? (visitAnnotation | visitAttribute) * (visitInnerClass | visitField | visitMethod) * visitEnd

3.2. ממשק API מבוסס עצים

ממשק API זה הוא יותר מונחה עצמים ממשק API והוא מקביל לדגם JAXB של עיבוד מסמכי XML.

זה עדיין מבוסס על ה- API מבוסס האירוע, אך הוא מציג את ה- ClassNode מעמד שורש. כיתה זו משמשת כנקודת הכניסה למבנה הכיתה.

4. עבודה עם ה- ASM API המבוסס על אירועים

נשנה את java.lang. שלם שיעור עם ASM. ועלינו להבין מושג בסיסי בנקודה זו: ה ClassVisitor הכיתה מכילה את כל שיטות המבקר הדרושות ליצירה או שינוי של כל חלקי הכיתה.

עלינו לעקוף רק את שיטת המבקרים הדרושה ליישום השינויים שלנו. נתחיל בהגדרת הרכיבים המוקדמים:

מחלקה ציבורית CustomClassWriter {static String className = "java.lang.Integer"; סטטי מחרוזת cloneableInterface = "java / lang / Cloneable"; קורא ClassReader; כותב ClassWriter; ציבורי CustomClassWriter () {reader = ClassReader new (className); סופר = ClassWriter חדש (קורא, 0); }}

אנו משתמשים בזה כבסיס להוספת ה- ניתנת לשיבוט ממשק למניה מספר שלם בכיתה, ונוסיף גם שדה ושיטה.

4.1. עבודה עם שדות

בואו ניצור את שלנו ClassVisitor בה נשתמש להוסיף שדה ל- מספר שלם מעמד:

מחלקה ציבורית AddFieldAdapter מרחיב את ClassVisitor {private String fieldName; שדה מחרוזת פרטי Default; גישת פרטי פרטית = org.objectweb.asm.Opcodes.ACC_PUBLIC; בוליאני פרטי isFieldPresent; ציבורי AddFieldAdapter (מחרוזת fieldName, int fieldAccess, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; this.fieldName = fieldName; this.access = fieldAccess; }} 

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

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

שיטה זו גם מאפשרת לנו לשנות את הנראות או את סוג השדות הקיימים:

@Override FieldVisitor ציבורי visitField (גישה int, שם מחרוזת, מחרוזת מחרוזת, חתימת מחרוזת, ערך אובייקט) {if (name.equals (fieldName)) {isFieldPresent = true; } להחזיר cv.visitField (גישה, שם, תיאור, חתימה, ערך); } 

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

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

ואז עלינו להתקשר ל visitEnd שיטה על אובייקט זה ל לאות שסיימנו לבקר בשדה זה:

@ ביטול ציבורי בטל ריק visitEnd () {if (! IsFieldPresent) {FieldVisitor fv = cv.visitField (access, fieldName, fieldType, null, null); אם (fv! = null) {fv.visitEnd (); }} cv.visitEnd (); } 

חשוב להיות בטוחים כי כל רכיבי ASM המשמשים מקורם ב- org.objectweb.asm חֲבִילָה - הרבה ספריות משתמשות בספריית ASM באופן פנימי ו- IDE יכולים להכניס אוטומטית את ספריות ה- ASM הכלולות.

כעת אנו משתמשים במתאם שלנו ב- הוסף שדה שיטה, השגת גרסה משוננת של java.lang. שלםעם השדה הנוסף שלנו:

מחלקה ציבורית CustomClassWriter {AddFieldAdapter addFieldAdapter; // ... בתים ציבוריים [] addField () {addFieldAdapter = AddFieldAdapter חדש ("aNewBooleanField", org.objectweb.asm.Opcodes.ACC_PUBLIC, כותב); reader.accept (addFieldAdapter, 0); מחזיר כותב. toByteArray (); }}

ביטלנו את visitField ו visitEnd שיטות.

כל מה שיש לעשות בנוגע לשדות קורה עם visitField שיטה. משמעות הדבר היא שנוכל לשנות גם שדות קיימים (למשל, להפוך שדה פרטי לציבור) על ידי שינוי הערכים הרצויים המועברים ל- visitField שיטה.

4.2. עבודה עם שיטות

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

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

בואו להפוך את שיטת toUnsignedString לציבורית:

public class PublicizeMethodAdapter מרחיב את ClassVisitor {publicizeMethodAdapter (int api, ClassVisitor cv) {super (ASM4, cv); this.cv = cv; } MethodVisitor visitMethod ציבורי (גישה int, שם מחרוזת, מחרוזת מחרוזת, חתימת מחרוזת, מחרוזת [] חריגים) {if (name.equals ("toUnsignedString0")) {החזר cv.visitMethod (ACC_PUBLIC + ACC_STATIC, שם, תיאור, חתימה, חריגים); } להחזיר cv.visitMethod (גישה, שם, תיאור, חתימה, חריגים); }} 

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

במקרה זה אנו משתמשים במכיני הגישה ב- org.objectweb.asm.Occodes חבילה ל לשנות את נראות השיטה. לאחר מכן אנו מחברים את שלנו ClassVisitor:

בית ציבורי [] publicizeMethod () {pubMethAdapter = חדש PublicizeMethodAdapter (כותב); reader.accept (pubMethAdapter, 0); מחזיר כותב. toByteArray (); } 

4.3. עבודה עם שיעורים

באותו קו כמו שיטות שינוי, אנו לשנות שיעורים על ידי יירוט של שיטת האורחים המתאימה. במקרה זה, אנו מיירטים לְבַקֵר, שהיא השיטה הראשונה בהיררכיית המבקרים:

מחלקה ציבורית AddInterfaceAdapter מרחיב את ClassVisitor {public AddInterfaceAdapter (ClassVisitor cv) {super (ASM4, cv); } ביקורת חלל ציבורית @ @ עקירה (גרסת int, גישה int, שם מחרוזת, חתימת מחרוזת, מחרוזת superName, ממשקי מחרוזת []) {מחרוזת [] החזקה = מחרוזת חדשה [interfaces.length + 1]; החזקת [holding.length - 1] = ממשק cloneable; System.arraycopy (ממשקים, 0, החזקה, 0, ממשקים.אורך); cv.visit (V1_8, גישה, שם, חתימה, supername, החזקה); }} 

אנו עוקפים את לְבַקֵר שיטה להוסיף את ניתנת לשיבוט ממשק למערך הממשקים לתמיכה על ידי מספר שלם מעמד. אנו מחברים את זה בדיוק כמו כל השימושים האחרים במתאמים שלנו.

5. שימוש בכיתה המתוקנת

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

בנוסף פשוט לכתוב את הפלט של author.toByteArray לדיסק כקובץ מחלקה, ישנן עוד כמה דרכים לתקשר עם המותאמים אישית שלנו מספר שלם מעמד.

5.1. משתמש ב TraceClassVisitor

ספריית ASM מספקת את TraceClassVisitor כיתת שירות בה נשתמש התבוננות פנימית בכיתה המתוקנת. כך אנו יכולים לאשר שהשינויים שלנו קרו.

בגלל ה TraceClassVisitor הוא ClassVisitor, נוכל להשתמש בו כתחליף טיפה לתקן ClassVisitor:

PrintWriter pw = PrintWriter חדש (System.out); publicizeMethodAdapter (ClassVisitor cv) {super (ASM4, cv); this.cv = cv; tracer = TraceClassVisitor חדש (cv, pw); } MethodVisitor visitMethod ציבורי (גישה int, שם מחרוזת, מחרוזת מחרוזת, חתימת מחרוזת, מחרוזת [] חריגים) {if (name.equals ("toUnsignedString0")) {System.out.println ("ביקור בשיטה לא חתומה"); החזר tracer.visitMethod (ACC_PUBLIC + ACC_STATIC, שם, תיאור, חתימה, חריגים); } להחזיר tracer.visitMethod (גישה, שם, תיאור, חתימה, חריגים); } visited End public () {tracer.visitEnd (); System.out.println (tracer.p.getText ()); } 

מה שעשינו כאן זה להתאים את ClassVisitor שעברנו למוקדם יותר שלנו PublicizeMethodAdapter עם ה TraceClassVisitor.

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

בעוד שתיעוד ה- ASM קובע כי ה- TraceClassVisitor יכול להדפיס אל PrintWriter המסופק לבנאי, נראה שזה לא עובד כמו שצריך בגרסה האחרונה של ASM.

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

5.2. שימוש במכשור Java

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

לכשיר את java.lang. שלם כיתה, אנחנו כתוב סוכן שיוגדר כפרמטר של שורת פקודה עם ה- JVM. הסוכן דורש שני מרכיבים:

  • כיתה המיישמת שיטה בשם מראש
  • יישום של ClassFileTransformer בה אנו נספק בתנאי את הגרסה שהשתנתה של הכיתה שלנו
Premain בכיתה ציבורית {premain public public space ריק (String agentArgs, Instrumentation inst) {inst.addTransformer (ClassFileTransformer חדש () {@Override בייט ציבורי [] transform (ClassLoader l, שם מחרוזת, Class c, ProtectionDomain d, byte [] b) זורק את IllegalClassFormatException {if (name.equals ("java / lang / Integer")) {CustomClassWriter cr = חדש CustomClassWriter (b); להחזיר cr.addField ();} להחזיר b;}}); }}

כעת אנו מגדירים את שלנו מראש מחלקת יישום בקובץ מניפסט של JAR באמצעות התוסף Maven jar:

 org.apache.maven.plugins maven-jar-plugin 2.4 com.baeldung.examples.asm.instrumentation. להישאר נכון 

בנייה ואריזה של הקוד שלנו עד כה מייצרת את הצנצנת שנוכל להעמיס כסוכן. כדי להשתמש בהתאמה אישית שלנו מספר שלם בכיתה בהיפותטית "YourClass.class“:

java YourClass -javaagent: "/ path / to / theAgentJar.jar"

6. מסקנה

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

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

ראינו חלק מהעוצמה של ספריית ASM - זה מסיר הרבה מגבלות שאנחנו עשויים להיתקל בהם עם ספריות צד שלישי ואפילו שיעורי JDK סטנדרטיים.

ASM נמצא בשימוש נרחב תחת מכסה המנוע של כמה מהספריות הפופולריות ביותר (Spring, AspectJ, JDK וכו ') כדי לבצע הרבה "קסמים" בזמן.

אתה יכול למצוא את קוד המקור של מאמר זה בפרויקט GitHub.


$config[zx-auto] not found$config[zx-overlay] not found