Используя Javassist для регистрации вызовов методов и значений аргументов, как сделать класс регистратора видимым в каждом инструментированном классе?

Инструмент (в этом репозитории) состоит из 3 классов (указанных ниже). Проблема заключается в том, как сделать мой класс ParaTracer.Logger видимым в каждом инструменте класса I (например, java.util.Random, показанном ниже). Оператор cp.importPackage( "ParaTracer.Logger"); не работает, и я получаю эту ошибку:

java.lang.NoClassDefFoundError: ParaTracer/Logger at java.util.Random.nextLong(Random.java)

Я попытался динамически загрузить класс Logger внутри каждого инструментированного класса. Но, похоже, я использовал Class.getMethod() неправильно, или компилятор Javassist слишком примитивен для компиляции кода с динамической загрузкой классов. Я получаю эту ошибку:

javassist.CannotCompileException: [source error] getMethod(java.lang.String,java.lang.Class,java.lang.Class) not found in java.lang.Class

Следующие 3 класса экспортируются в файл JAR с файлом MANIFEST.MF, определяющим Premain-Class, и передаются в JVM при запуске любой инструментальной программы с использованием переключателя:

-javaagent:/Path/To/ParaTracerAgent.jar

Вот 3 класса.

package ParaTracer;

import java.lang.instrument.Instrumentation;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;

public class ParaTracer {

    private static volatile Instrumentation instr;

    public static void premain(String agentArgs, Instrumentation inst) {
        instr = inst;
        SimpleClassTransformer transformer = new SimpleClassTransformer();
        inst.addTransformer( transformer, false );
    }
}

Класс трансформатора:

package ParaTracer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.HashSet;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;


public class SimpleClassTransformer implements ClassFileTransformer {

    public HashMap< String, HashSet< String > > mInstrumentedMethods;

    public SimpleClassTransformer() {
        mInstrumentedMethods = new HashMap< String, HashSet< String > >();

        mInstrumentedMethods.put( "java.util.Random", new HashSet< String >() );
        mInstrumentedMethods.get( "java.util.Random").add( "nextLong" );
    }

    @Override
    public byte[] transform(
        ClassLoader       loader,
        String            className,
        Class<?>          classBeingRedefined,
        ProtectionDomain  protectionDomain,
        byte[]            classfileBuffer) throws IllegalClassFormatException {

        System.err.println( "---- Instrumenting: " + className );

        byte[] byteCode = classfileBuffer;

        String normalizedClassName = className.replaceAll("/", ".");

        if ( mInstrumentedMethods.containsKey( normalizedClassName ) ) {
            try {
                ClassPool cp = ClassPool.getDefault();

                cp.importPackage( "ParaTracer.Logger");

                CtClass cc = cp.get( normalizedClassName );

                for( String method : mInstrumentedMethods.get( normalizedClassName ) ) {
                    CtMethod  m  = cc.getDeclaredMethod( method );

                    StringBuilder sbs = new StringBuilder();
                    sbs.append( "long tid = Thread.currentThread().getId();" );
                    sbs.append( "StringBuilder sbArgs = new StringBuilder();" );
                    sbs.append( "sbArgs.append( System.identityHashCode( $0 ) );" );
                    CtClass[] pTypes = m.getParameterTypes();
                    for( int i=0; i < pTypes.length; ++i ) {
                        CtClass pType = pTypes[i];
                        if ( pType.isPrimitive() ) {
                            sbs.append( "sbArgs.append( \", \" + $args[" + i + "] );" );
                        } else {
                            sbs.append( "sbArgs.append( \", \" + System.identityHashCode( $args[" + i + "] ) );" );
                        }
                    }
                    sbs.append( "ParaTracer.Logger.pushArgs( tid, sbArgs.toString() );" );
                    sbs.append( "StringBuilder sb = new StringBuilder();" );
                    sbs.append( "sb.append( tid + \" : " + m.getLongName() + ".<START>(\" );" );
                    sbs.append( "sb.append( sbArgs.toString() );" );
                    sbs.append( "sb.append( \")\" );" );
                    sbs.append( "ParaTracer.Logger.print( sb.toString() );" );

                    m.insertBefore("{" + sbs.toString() + "}");

                    StringBuilder sbe = new StringBuilder();
                    sbe.append( "long tid = Thread.currentThread().getId();" );
                    sbe.append( "String args = ParaTracer.Logger.popArgs( tid );" );
                    sbe.append( "StringBuilder sb = new StringBuilder();" );
                    sbe.append( "sb.append( tid + \" : " + m.getLongName() + ".<END>(\" );" );
                    sbe.append( "sb.append( args );" );
                    sbe.append( "sb.append( \")\" );" );
                    sbe.append( "ParaTracer.Logger.print( sb.toString() );" );

                    m.insertAfter("{" + sbe.toString() + "}");
                }
                byteCode = cc.toBytecode();
                cc.detach();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return byteCode;
    }
}

Класс поточно-безопасного регистратора задается:

package ParaTracer;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Stack;

public class Logger {

    private static String loggerFilePath = "\"/some/fixed/path\"";
    private static FileWriter   fw;
    private static PrintWriter  out;
    private static HashMap< Long, Stack<String> > callStacks;

    public static synchronized void pushArgs( long tid, String args ) {
        try {
            init();
        } catch (IOException e) {
            e.printStackTrace();
        }
        if ( ! callStacks.containsKey( tid ) ) {
            callStacks.put( tid, new Stack<String>() );
        }
        callStacks.get( tid ).push( args );
    }

    public static synchronized String popArgs( long tid ) {
        assert( callStacks.containsKey( tid ) );
        assert( ! callStacks.get( tid ).empty() );
        return callStacks.get( tid ).pop();
    }

    public static synchronized void shutdown() {
        if ( fw == null ) return;
        try {
            fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static synchronized void print( String str ) {
        try {
            init();
        } catch (IOException e) {
            e.printStackTrace();
        }
        out.print( str );
    }

    private static void init() throws IOException {
        if ( fw != null ) return;
        fw  = new FileWriter( loggerFilePath );
        out = new PrintWriter( fw );
        callStacks = new HashMap< Long, Stack<String> >();
    }
}

person Ahmed Nassar    schedule 04.04.2015    source источник
comment
Поскольку вопросы Javassist, похоже, не вызывают особого интереса в SO, я просто полностью удалил класс ParaTracer.Logger и использовал System.err для регистрации вызовов методов и использовал System.setOut()/.setErr() для перенаправления стандартного вывода/ошибки в файл. Не было необходимости печатать значения аргументов при возврате метода. Теперь это работает.   -  person Ahmed Nassar    schedule 05.04.2015


Ответы (2)


Согласно документации для агентов Java класс агента загружается системным загрузчиком классов. Но если вы хотите инструментировать базовые классы Java и ссылаться на них на собственный пользовательский класс, тогда этот класс должен быть доступен загрузчику классов bootstrap, а не системному.

Переместите свой класс Logger в отдельный файл JAR и укажите этот файл в атрибуте Boot-Class-Path манифеста JAR агента:

Boot-Class-Path: ParaTracerLogger.jar

Теперь класс регистратора отображается в загрузчике начальной загрузки и может быть виден инструментированными java.lang классами.

person Ian Roberts    schedule 05.04.2015
comment
Спасибо за этот ответ; Я провел бессчетное количество часов в погоне за этим. Я также обнаружил, что параметры -Xbootclasspath/a:/‹путь к банке› или -Xbootclasspath/p:/‹путь к банке› в открытом JDK также были полезны, но ваш ответ вдохновил меня изучить их. - person Leo; 05.04.2015
comment
Кроме того, я только что узнал, что вам действительно не нужно перемещать код в другую банку; установка Boot-Class-Path: следующим образом вызывает загрузку самого jar-файла агента (./ относится к пути к файлу jar-файла агента): Booth-Class-Path: ./ParaTracerAgent.jar - person Leo; 05.04.2015
comment
@ Ян Робертс: Большое спасибо. Теперь отдельный класс ParaTracer.Logger работает нормально, и мне не нужно встраивать непотокобезопасный код ведения журнала в каждое инструментальное место, и мне не нужно открывать/закрывать файл журнала каждый раз, когда нужно напечатать сообщение. - person Ahmed Nassar; 06.04.2015

В итоге у меня был метод ClassFileTransformer.tranform следующим образом:

По сути, код, вставленный Javassist, повторно открывает один и тот же файл журнала каждый раз, когда появляется сообщение для записи, и добавляет это сообщение в выходной файл. Наличие отдельного файла журнала было важно, потому что перенаправление стандартного вывода/ошибки приведет к загрязнению файлов журнала, если эти потоки уже используются инструментированным приложением (как это обычно бывает).

@Override
public byte[] transform(
        ClassLoader       loader,
        String            className,
        Class<?>          classBeingRedefined,
        ProtectionDomain  protectionDomain,
        byte[]            classfileBuffer)
                throws IllegalClassFormatException {

    byte[] byteCode = classfileBuffer;

    String normalizedClassName = className.replaceAll("/", ".");
    System.out.println( "\tNormalized: " + normalizedClassName );

    ClassMonitorSet classMonitorSet = monitorClass( normalizedClassName );
    if ( classMonitorSet != null ) {
        System.out.println( "\tMonitoring: " + normalizedClassName );
        try {
            ClassPool cp = ClassPool.getDefault();
            CtClass cc = cp.get( normalizedClassName );

            for( String methodName : classMonitorSet.monitorSet ) {
                CtMethod[]  methods  = cc.getDeclaredMethods( methodName );
                for( CtMethod method : methods ) {

                    StringBuilder sbs = new StringBuilder();
                    sbs.append( "long tid = Thread.currentThread().getId();" );
                    sbs.append( "StringBuilder sbArgs = new StringBuilder();" );
                    sbs.append( "sbArgs.append( System.identityHashCode( $0 ) );" );
                    CtClass[] pTypes = method.getParameterTypes();
                    for( int i=0; i < pTypes.length; ++i ) {
                        CtClass pType = pTypes[i];
                        if ( pType.isPrimitive() ) {
                            sbs.append( "sbArgs.append( \", \" + $args[" + i + "] );" );
                        } else {
                            sbs.append( "sbArgs.append( \", \" + System.identityHashCode( $args[" + i + "] ) );" );
                        }
                    }
                    sbs.append( "StringBuilder sb = new StringBuilder();" );
                    sbs.append( "sb.append( tid + \" : " + method.getLongName() + ".<START>(\" );" );
                    sbs.append( "sb.append( sbArgs.toString() );" );
                    sbs.append( "sb.append( \")\" );" );
                    sbs.append( "String fPath = \"/path/to/log.out\";" );
                    sbs.append( "try {" );
                    sbs.append( "   java.io.FileWriter  fw  = new java.io.FileWriter( fPath, true );" );
                    sbs.append( "   java.io.PrintWriter out = new java.io.PrintWriter( fw, true );" );
                    sbs.append( "   out.println( sb.toString() );" );
                    sbs.append( "   fw.close();" );
                    sbs.append( "} catch (java.io.IOException e) {" );
                    sbs.append( "   e.printStackTrace();" );
                    sbs.append( "}" );

                    method.insertBefore("{" + sbs.toString() + "}");

                    StringBuilder sbe = new StringBuilder();
                    sbe.append( "long tid = Thread.currentThread().getId();" );
                    sbe.append( "StringBuilder sb = new StringBuilder();" );
                    sbe.append( "sb.append( tid + \" : " + method.getLongName() + ".<END>(*)\" );" );
                    sbe.append( "String fPath = \"/path/to/log.out\";" );
                    sbe.append( "try {" );
                    sbe.append( "   java.io.FileWriter  fw  = new java.io.FileWriter( fPath, true );" );
                    sbe.append( "   java.io.PrintWriter out = new java.io.PrintWriter( fw, true );" );
                    sbe.append( "   out.println( sb.toString() );" );
                    sbe.append( "   fw.close();" );
                    sbe.append( "} catch (java.io.IOException e) {" );
                    sbe.append( "   e.printStackTrace();" );
                    sbe.append( "}" );

                    method.insertAfter("{" + sbe.toString() + "}");
                }
            }
            byteCode = cc.toBytecode();
            cc.detach();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    return byteCode;
}
person Ahmed Nassar    schedule 05.04.2015
comment
Исходный код вашего проекта где-то поблизости? - person joe-jeff; 05.10.2017
comment
@joe-jeff Раньше это было в частном репо (как часть более крупного исследовательского проекта). Я сделал это общедоступным здесь: github.com/anassar/ParaTracer. - person Ahmed Nassar; 12.10.2017