Home > java > Java Instrumentation Framework

Java Instrumentation Framework

7 dicembre 2010

Translate in English with Google Translate
Con Java 5 è stata recepita la JSR-163 ( Java Platform Profiling Architecture API ) che include un meccanismo di instrumentation del bytecode tramite l’introduzione della Java Programming Language Instrumentation Services ( JPLIS ) che introduce un nuovo package dal nome java.lang.instrument che consente di eseguire le istruzione di un agente ed eventualmente modificare il contenuto del bytecode inerente ai metodi di una classe in modo tale da modificare a RunTime il suo comportamento.

Da precisare che il suddetto package non fornisce nessuna implementazione utile a modificare il bytecode ma, per fare ciò, occorre utilizzare librerie di terze parti, per esempio in ambito open-source possiamo utilizzare javassist, BCEL oppure ASM .

Il package java.lang.instrument definisce poche classi, in particolare:

  1. 2 interfacce: ClassFileTransformer e Instrumentation
  2. 1 classe concreta: ClassDefinition
  3. 2 classi di eccezioni: IllegalClassFormatException e UnmodifiableClassException

Nel package sun.instrument sono presenti delle classi di implementazione di default di Sun:

  1. la classe InstrumentationImpl che implementa l’interfaccia Instrumentation e fornisce l’implementazione Sun di default
  2. la classe TransformerManager utilizzata da InstrumentationImpl per gestire la lista dei Trasformer cioè delle classi che si occupano di eseguire le modifiche al bytecode.

Vediamo come si presenta il Class Diagram:

Instrument Class Diagram

Instrument Class Diagram

Come utilizzare il framework

Il framework può essere utilizzato in 2 momenti:

  1. in fase di startup della JVM: creando un classe agent che presenti al suo interno il metodo premain con questa firma:
    • public static void premain(String args, Instrumentation inst)
  2. in fase di esecuzione della JVM: creando un classe agent che presenti al suo interno il metodo agentmain con questa firma:
    • public static void agentmain(String args, Instrumentation inst)

Al fine di utilizzare il framework occorre:

  1. creare un file JAR che:
    • includa la classe agent sviluppata
    • preveda nel manifest del jar la proprietà Agent-Class oppure PreMain-Class a seconda che l’instrumentazione debba avvenire in fase di startup o di esecuzione di una JVM valorizzata con il nome della classe agent
  2. utilizzare l’opzione -javaagent in sede di esecuzione della JVM seguito dal nome del jar, nel caso di esecuzione dell’agent in sede di startup della JVM

Esempio: esecuzione dell’agent in fase di startup della JVM

Cominiciamo con un esempio molto semplice per capire, a piccoli passi, come utilizzare l’instrumentazione.
In questo esempio avviamo l’agent allo startup della JVM pertanto implementiamo il metodo premain.
Creiamo la classe main dal nome MyMainClass:


public class MyMainClass {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main!");
    }
}

Creiamo la classe agent dal nome MyAgentClass:


import java.lang.instrument.Instrumentation;
public class MyAgentClass {
    public static void premain(String args, Instrumentation inst) {
        System.out.println("pre-main!");
    }
}

Compiliamo le classi e pacchettizziamo la classe MyAgentClass in un file myagent.jar inserendo nel file manifest la proprietà relativa alla classe agent:


$JAVA_HOME/bin/javac *.java
echo "Premain-Class: MyAgentClass" > metainf.txt
$JAVA_HOME/bin/jar -cmfv metainf.txt myagent.jar MyAgentClass.class

Eseguiamo la classe MyMainClass valorizzando l’opzione della JVM javaagent myagent.jar, ossia il nome del jar dell’agent


$JAVA_HOME/bin/java -javaagent:myagent.jar -cp . MyMainClass
pre-main!
main!

Verifichiamo in questo caso che il metodo premain della classe MyAgentClass sia stato invocato prima del metodo main della classe MyMainClass.

Esempio: esecuzione dell’agent in fase di esecuzione della JVM

In questo secondo esempio vediamo come eseguire l’agent nel caso in cui la JVM sia già startata.
Per agganciari ad una JVM in esecuzione, a partire da Java 1.6, è possibile utilizzare le librerie disponibili nel package com.sun.tools.attach che tramite la classe VirtualMachine consente di utilizzare il metodo attach per agganciarsi alla JVM fornendo il PID del processo Java e successivamente tramite il metodo loadAgent consente di caricare l’agent nella JVM target.
Per l’utilizzo della libreria del package com.sun.tools.attach occorre impostare nel CLASSPATH il percorso del jar $JAVA_HOME/lib/tools.jar che non viene inserito di default nel classpath.
I passaggi da seguire sono questi:

  1. creazione di una classe main che deve rappresentare una JVM in esecuzione a cui agganciarsi
  2. creazione di una classe agent e successiva pacchettizzazione in file jar con opportuna modifica del file manifest
  3. creazione di una classe attach che dovrà agganciarsi alla JVM in esecuzione e caricamento dell’agent jar

1. classe main
Creiamo la classe main dal nome MyMainClass che presenta solo un Thread in sleep  in modo tale da garantire lo start della JVM a cui ci possiamo successivamente agganciare.


public class MyMainClass {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main!");
        Thread.sleep(100*1000);
    }
}

2. classe agent
Creiamo l’agent che dovrà essere inserito nella JVM in esecuzione:


import java.lang.instrument.Instrumentation;
public class MyAgentClass {
    public static void agentmain(String args, Instrumentation inst) {
        System.out.println("agent-main!");
    }
}

Creiamo il jar dell’agent dal nome myagent.jar indicando nel manifest la proprietà Agent-Class seguito dalla classe agent dal nome MyAttachClass:


echo "Agent-Class: MyAgentClass" > metainf.txt
$JAVA_HOME/bin/jar -cmfv metainf.txt myagent.jar MyAgentClass.class

3. classe attach
Creiamo la classe attach dal nome MyAttachClass che si occuperà di agganciarsi alla JVM e caricherà l’agent:


import com.sun.tools.attach.*;
public class MyAttachClass {
    public static void main(String[] args) throws Exception {
        VirtualMachine v = VirtualMachine.attach(args[0]);
        v.loadAgent("/opt/test/myagent.jar");
        System.out.println("attach-main!");
    }
}

A questo punto apriamo 2 shell:

  1. nella prima shell eseguiamo la classe main in modo da startare la prima JVM, in questo modo:
    
    $JAVA_HOME/bin/java  -cp . MyMainClass
    main!
    
  2. nella seconda shell recuperiamo il pid del processo.
    Possiamo utilizzare l’utility $JAVA_HOME/bin/jps che consente di visualizzare la lista delle JVM in esecuzione. Considerando che anche jps è un programma java, anche jps risulterà nella lista dei processi elencati.
    Nel nostro caso abbiamo: 

    
    $JAVA_HOME/bin/jps
    5181 Jps
    5123 MyMainClass
    

    Questo comando si presenta come una alternativa ai soliti comandi quali ps -ef | grep java oppure pgrep java su unix/linux.
    Ok. Abbiamo recuperato il PID della JVM: 5123 MyMainClass ed adesso passiamolo alla classe attach in questo modo:

    
    $JAVA_HOME/bin/java -cp .:$JAVA_HOME/lib/tools.jar MyAttachClass 5123
    attach-main!
    

A questo punto nella prima shell verrà visualizzato questo messaggio:


agent-main!

che indica che l’agent è stato caricato dalla JVM ed è stato eseguito.

Nel prossimo articolo vediamo come utilizzare appieno l’instrumentation per modificare il bytecode di una classe.

Categorie:java Tag:,
%d blogger cliccano Mi Piace per questo: