מדריך למכשור ג'אווה

1. הקדמה

במדריך זה נדבר על Java Instrumentation API. הוא מספק את היכולת להוסיף קוד בתים לשיעורי Java קיימים.

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

2. התקנה

לאורך המאמר נבנה אפליקציה באמצעות מכשור.

היישום שלנו יורכב משני מודולים:

  1. אפליקציית כספומט המאפשרת לנו למשוך כסף
  2. וסוכן ג'אווה שיאפשר לנו למדוד את ביצועי הכספומט שלנו על ידי מדידת זמן ההוצאות המושקע

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

הפרויקט שלנו יהיה בעל המבנה הבא:

com.baeldung.instrumentation יישום סוכן pom 1.0.0 

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

3. מהו סוכן Java

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

כדי שסוכן יעבוד, עלינו להגדיר שתי שיטות:

  • מראש - יטען באופן סטטי את הסוכן באמצעות הפרמטר -javaagent בהפעלה של JVM
  • סוכן הראשי - יטען באופן דינמי את הסוכן ל- JVM באמצעות Java Attach API

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

ראשית, בוא נראה כיצד נשתמש בסוכן Java קיים.

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

4. טעינת סוכן Java

כדי שנוכל להשתמש בסוכן Java, עלינו לטעון אותו תחילה.

יש לנו שני סוגים של עומס:

  • סטטי - עושה שימוש ב- מראש לטעינת הסוכן באמצעות אפשרות -javaagent
  • דינמי - עושה שימוש ב- סוכן הראשי כדי לטעון את הסוכן ל- JVM באמצעות Java Attach API

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

4.1. עומס סטטי

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

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

java -javaagent: agent.jar -jar application.jar

חשוב לציין שאנחנו צריכים תמיד לשים את -javaagent פרמטר לפני -קַנקַן פָּרָמֶטֶר.

להלן יומני הפקודה שלנו:

22: 24: 39.296 [main] INFO - [Agent] בשיטה המוקדמת 22: 24: 39.300 [main] INFO - [Agent] המחלקה המרה MyAtm 22: 24: 39.407 [main] INFO - [יישום] התחלת יישום כספומט 22: 24: 41.409 [ראשי] INFO - [יישום] משיכה מוצלחת של [7] יחידות! 22: 24: 41.410 [ראשי] מידע - [יישום] פעולת המשיכה הושלמה תוך: 2 שניות! 22: 24: 53.411 [ראשי] מידע - [יישום] משיכה מוצלחת של [8] יחידות! 22: 24: 53.411 [ראשי] מידע - [יישום] פעולת המשיכה הושלמה תוך: 2 שניות!

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

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

4.2. עומס דינמי

ההליך של טעינת סוכן Java ל- JVM שכבר פועל נקרא עומס דינמי. הסוכן מצורף באמצעות ממשק ה- API של Java Attach.

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

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

VirtualMachine jvm = VirtualMachine.attach (jvmPid); jvm.loadAgent (agentFile.getAbsolutePath ()); jvm.detach ();

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

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

אנחנו נקרא לשיעור הזה מַשׁגֵר וזה יהיה מחלקת הקבצים העיקרית שלנו:

מעמד ציבורי משגר {ציבורי ריק ריק סטטי (מחרוזת [] טענות) זורק חריג {אם (טיעונים [0]. שווה ("StartMyAtmApplication")) {חדש MyAtmApplication (). רץ (args); } אחר אם (args [0] .equals ("LoadAgent")) {AgentLoader חדש (). run (args); }}}

הפעלת היישום

java -jar application.jar StartMyAtmApplication 22: 44: 21.154 [main] INFO - [Application] התחלת יישום כספומט 22: 44: 23.157 [main] INFO - [Application] משיכה מוצלחת של [7] יחידות!

צירוף Java Agent

לאחר הפעולה הראשונה, אנו מצרפים את סוכן Java ל- JVM שלנו:

java -jar application.jar LoadAgent 22: 44: 27.022 [main] INFO - מצורף למיקוד JVM עם PID: 6575 22: 44: 27.306 [main] INFO - מצורף למטרה JVM וטען בהצלחה סוכן Java 

בדוק יומני יישומים

עכשיו כשצירפנו את הסוכן שלנו ל- JVM נראה שנמצא זמן ההשלמה הכולל של פעולת משיכת הכספומט השנייה.

פירוש הדבר שהוספנו את הפונקציונליות שלנו בזמן שאנחנו פועלים:

22: 44: 27.229 [צרף מאזין] INFO - [סוכן] בשיטת agentmain 22: 44: 27.230 [צרף מאזין] INFO - [סוכן] מחלקה משתנה MyAtm 22: 44: 33.157 [ראשי] INFO - [יישום] משיכה מוצלחת [8] יחידות! 22: 44: 33.157 [main] מידע - [יישום] פעולת המשיכה הושלמה תוך: 2 שניות!

5. יצירת סוכן Java

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

מכיוון שסוכן java עושה שימוש ב- Java Instrumentation API, לפני שנעמיק יותר מדי ביצירת הסוכן שלנו, בואו נראה כמה מהשיטות הנפוצות ביותר ב- API זה ותיאור קצר של מה שהם עושים:

  • addTransformer - מוסיף שנאי למנוע המכשור
  • getAllLoadedClasses - מחזיר מערך של כל הכיתות שטעונות כרגע על ידי ה- JVM
  • לשנות טרנספורמציה - מקל על המכשור של שיעורים שכבר טעונים על ידי הוספת קוד בתים
  • removeTransformer - מבטל רישום של השנאי שסופק
  • הגדר מחדש משקפיים - הגדר מחדש את קבוצת המחלקות שסופקה באמצעות קבצי המחלקה שסופקו, כלומר המחלקה תוחלף במלואה, לא תשתנה כמו עם לשנות טרנספורמציה

5.1. צור את Premain ו Agentmain שיטות

אנו יודעים כי כל סוכן Java זקוק לפחות לאחד מ- מראש אוֹ סוכן הראשי שיטות. האחרון משמש לעומס דינמי, ואילו הראשון משמש לטעינה סטטית של סוכן Java ל- JVM.

בואו נגדיר את שניהם בסוכן שלנו כך שנוכל לטעון את הסוכן הזה באופן סטטי ודינמי:

Premain ריק ריק מראש (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] בשיטת premain"); מחרוזת className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); } סוכן חלל סטטי ציבורי (String agentArgs, Instrumentation inst) {LOGGER.info ("[Agent] בשיטת agentmain"); מחרוזת className = "com.baeldung.instrumentation.application.MyAtm"; transformClass (className, inst); }

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

להלן הקוד של ה- transformClass שיטה שהגדרנו כדי לעזור לנו להפוך MyAtm מעמד.

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

חלל סטטי פרטי transformClass (מחרוזת className, מכשור מכשור) {Class targetCls = null; ClassLoader targetClassLoader = null; // בדוק אם נוכל להשיג את הכיתה באמצעות forName נסה {targetCls = Class.forName (className); targetClassLoader = targetCls.getClassLoader (); טרנספורמציה (targetCls, targetClassLoader, מכשור); לַחֲזוֹר; } לתפוס (Exception ex) {LOGGER.error ("מחלקה [{}] לא נמצא עם Class.forName"); } // אחרת חזרו על כל המחלקות הטעונות ומצאו את מה שאנחנו רוצים (Class clazz: instrumentation.getAllLoadedClasses ()) {if (clazz.getName (). שווה (className)) {targetCls = clazz; targetClassLoader = targetCls.getClassLoader (); טרנספורמציה (targetCls, targetClassLoader, מכשור); לַחֲזוֹר; }} זרק RuntimeException חדש ("נכשל מציאת הכיתה [" + className + "]"); } שינוי ריק סטטי פרטי (Class clazz, ClassLoader classLoader, Instrumentation instrumentation) {AtmTransformer dt = new AtmTransformer (clazz.getName (), classLoader); instrumentation.addTransformer (dt, נכון); נסה את {instrumentation.retransformClasses (clazz); } לתפוס (Exception ex) {לזרוק RuntimeException חדש ("טרנספורמציה נכשלה עבור: [" + clazz.getName () + "]", לשעבר); }}

עם זה מהדרך, בואו נגדיר את השנאי עבור MyAtm מעמד.

5.2. הגדרת שלנו שַׁנַאי

שנאי כיתה חייב ליישם ClassFileTransformer וליישם את שיטת הטרנספורמציה.

נשתמש ב- Javassist כדי להוסיף קוד בתים אליו MyAtm מחלקה והוסף יומן עם זמן העסקה הכולל של משיכת ATW:

מחלקה ציבורית AtmTransformer מיישמת ClassFileTransformer {@Override byte public [] transform (ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte [] classfileBuffer) {byte [] byteCode = classfileBuffer; מחרוזת finalTargetClassName = this.targetClassName .replaceAll ("\.", "/"); אם (! className.equals (finalTargetClassName)) {return byteCode; } אם (className.equals (finalTargetClassName) && loader.equals (targetClassLoader)) {LOGGER.info ("[Agent] Transforming class MyAtm"); נסה את {ClassPool cp = ClassPool.getDefault (); CtClass cc = cp.get (targetClassName); CtMethod m = cc.getDeclaredMethod (WITHDRAW_MONEY_METHOD); m.addLocalVariable ("startTime", CtClass.longType); m.insertBefore ("startTime = System.currentTimeMillis ();"); StringBuilder endBlock = StringBuilder חדש (); m.addLocalVariable ("endTime", CtClass.longType); m.addLocalVariable ("opTime", CtClass.longType); endBlock.append ("endTime = System.currentTimeMillis ();"); endBlock.append ("opTime = (endTime-startTime) / 1000;"); endBlock.append ("LOGGER.info (\" [יישום] פעולת המשיכה הושלמה תוך: "+" \ "+ opTime + \" שניות! \ ");"); m.insertAfter (endBlock.toString ()); byteCode = cc.toBytecode (); cc.detach (); } לתפוס (NotFoundException | CannotCompileException | IOException e) {LOGGER.error ("Exception", e); }} להחזיר byteCode; }}

5.3. יצירת קובץ מניפסט של סוכן

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

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

בקובץ הצנצנת הסופי של Java, נוסיף את השורות הבאות לקובץ המניפסט:

סוכן מחלקה: com.baeldung.instrumentation.agent.MyInstrumentationAgent Can-Define-Classes: true Can-Transransform-Classes: true Premain-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent

סוכן המכשור של Java שלנו הושלם כעת. להפעלתו, עיין בסעיף טעינת סוכן Java במאמר זה.

6. מסקנה

במאמר זה דיברנו על ממשק ה- API של Java Instrumentation. בדקנו כיצד לטעון סוכן Java ל- JVM באופן סטטי ודינמי.

בדקנו גם כיצד נקים סוכן Java משלנו מאפס.

כמו תמיד, ניתן למצוא את היישום המלא של הדוגמה ב- Github.


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