Home > java > Java Instrumentation Framework con Javassist

Java Instrumentation Framework con Javassist

15 dicembre 2010

Translate in English with Google Translate

Nell’articolo precedente ho introdotto il Java Instrumentation Framework recepito in Java 5 e che consente di instrumentare le classi in modo da poter apportare delle modifiche a Runtime al bytecode per esigenze di logging, debugging, profiling o per altro.

Precedentemente avevo introdotto solo le basi della instrumentazione mentre in questo articolo approfondiamo l’argomento e vediamo come l’agent riesca a modificare il bytecode utilizzando la libreria di manipolazione del bytecode javassist.

L’obiettivo del nostro esercizio è quello di inserire in tutti i metodi della nostra applicazione una istruzione di logging, tale da tracciare il nome della classe e del metodo in esecuzione. Per fare questo dobbiamo fare 3 cose:

  1. creare una applicazione target di esempio
  2. creare l’agent di instrumentazione
  3. creare le classi di manipolazione del bytecode utilizzando la libreria javassist

Dividiamo la struttura delle classi in 2 parti:

  1. la prima parte si riferisce alla applicazione che vogliamo instrumentare.
    Creiamo una applicazione molto semplice composta da poche classi tale da poterla instrumentare introducendo nei metodi di tutte le classi una istruzione di logging per poter tenere traccia dell’ordine delle operazioni svolte.
  2. la seconda parte si riferisce all’agent, in questo caso intendiamo sia l’implementazione del framework di instrumentazione e sia all’utilizzo delle librerie di manipolazione del bytecode.

Prima parte

La prima parte viene mostrata nel Class Diagram seguente che mostra la nostra semplice applicazione. Si tratta di una classe main dal nome MyMainClass che invoca un metodo dal nome metodo() della classe TestClass, come segue:

Class Diagram: Main Application

Class Diagram: Main Application

La classe MyMainClass è la seguente:

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

La classe TestClass è la seguente:

public class TestClass{
    public void metodo(){
        System.out.println("metodo");
    }
}

Seconda parte

La seconda parte è costituita dalle classi di instrumentazione e dalle classi di javaassit (colorate in modo diverso).

Le classi di Instrumentation create sono:

  1. MyAgentClass che prevede il metodo premain() che verrà eseguito allo start della JVM che utilizza come classe Transformer la classe AgentTransformer
  2. AgentTransformer implementa l’interfaccia Sun ClassFileTransformer. AgentTransformer si occupa di invocare il pool di classi di javassit ClassPool per poi creare un oggetto CtClass che incapsula le clasi della nostra applicazione ed inietta il bytecode all’inizio di tutti i suoi metodi.
Class Diagram: Java Instrumentation + Javassist

Class Diagram: Java Instrumentation + Javassist

La classe MyAgentClass è la seguente:

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

La classe AgentTransformer è la seguente:

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;

public class AgentTransformer implements ClassFileTransformer {

    String[] otherPackages = new String[]{"sun.", "java.", "javax."};
    ClassPool classPool = ClassPool.getDefault();

    @Override
    public byte[] transform(ClassLoader loader, String className,
            Class classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] x = null;
        CtClass ctClass = null;
        try {
            ctClass = classPool.makeClass(new ByteArrayInputStream(
                    classfileBuffer));
            if (isClassToInstrument(ctClass)) {
                String classNameInstanced = ctClass.getClassFile().getName();
                for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
                    ctMethod.insertBefore(getText(classNameInstanced, ctMethod));
                }
            }
            x = ctClass.toBytecode();
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
        } finally {
            ctClass.detach();
        }
        return x;
    }

    private boolean isClassToInstrument(CtClass ctClass) throws
            NotFoundException {
        for (String otherPackage : otherPackages) {
            if (ctClass.getClassFile().getName().startsWith(otherPackage)) {
                return false;
            }
        }
        return true;
    }

    private String getText(String classNameInstanced, CtMethod ctMethod) {
        StringBuffer sb = new StringBuffer();
        sb.append("System.out.println(");
        sb.append("\"");
        sb.append("Instrumentation of method ");
        sb.append(classNameInstanced);
        sb.append(".");
        sb.append(ctMethod.getName());
        sb.append("()\");");
        return sb.toString();
    }
}

Compiliamo le classi della nostra applicazione:

$JAVA_HOME/bin/javac MyMainClass.java TestClass.java

Poi compiliamo le classi di instrumentation MyAgentClass.java e AgentTransformer.java e creiamo il jar dell’agent dal nome myagent.jar:

$JAVA_HOME/bin/javac -cp javassist.jar MyAgentClass.java AgentTransformer.java
echo "PreMain-Class: MyAgentClass" > metainf.txt
echo "Boot-Class-Path: javassist.jar" >> metainf.txt
$JAVA_HOME/bin/jar -cmfv metainf.txt myagent.jar MyAgentClass.class AgentTransformer.class

Eseguiamo l’applicazione SENZA USARE il jar dell’agent, in modo da avviare l’applicazione e capire cosa avviene nel caso di assenza di instrumentazione.

<strong>$JAVA_HOME/bin/java -cp javassist.jar MyMainClass</strong>
main!
metodo

Eseguiamo l’applicazione USANDO il jar dell’agent, in modo da notare le differenze dovute alla instrumentazione:

$JAVA_HOME/bin/java  -javaagent:myagent.jar -cp javassist.jar MyMainClass
agent-main!
Instrumentation of method MyMainClass.main()
main!
Instrumentation of method TestClass.metodo()
metodo

Vediamo che viene mostrato a console il logging delle classi invocate: MyMainClass.main() e TestClass.metodo(). Il bytecode delle classi MyMainClass e TestClass è stato modificato a runtime ma i files classe non sono stati alterati.

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