ClassFileTransformer + Javassist: такого поля нет

Хорошо, я пытаюсь сделать java-агент, который будет контролировать приложение. Итак, я пытаюсь внедрить код в PreparedStatements для измерения времени выполнения SQL-запросов. Для этого я разработал класс, реализующий ClassFileTransformer. Это выглядит так:

public class JDBCClassTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        ClassPool pool = ClassPool.getDefault();
        CtClass currentClass = null;
        CtClass statement = null;
        try {
            currentClass = pool.makeClass(new java.io.ByteArrayInputStream(classfileBuffer));
            statement = pool.get("java.sql.PreparedStatement");
            if (currentClass.subtypeOf(statement) && !currentClass.isInterface()) {
                probeStatement(currentClass);
            }
            classfileBuffer = currentClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();

        } finally {
            if (currentClass != null) {
                currentClass.detach();
            }
        }
        return classfileBuffer;

    }

    private void probeStatement(CtClass currentClass) throws NotFoundException, CannotCompileException {
        CtField field = CtField.make("private fr.mael.package.SQLProbe $$probe;", currentClass);
        currentClass.addField(field);
        CtMethod setter = CtNewMethod.make("public void set$$probe(fr.mael.package.SQLProbe probe){ this.$$probe = probe;}", currentClass);
        currentClass.addMethod(setter);

        CtMethod executeQuery = currentClass.getMethod("executeQuery", "()Ljava/sql/ResultSet;");

        executeQuery.insertBefore("this.$$probe.start();");
        executeQuery.insertAfter("this.$$probe.stop();");
    }

}

Итак, что я хочу сделать, так это внедрить код в каждый класс (в методе «executeQuery()»), который реализует java.sql.PreparedStatement. Чтобы проверить это, я просто запускаю базовый «выбор * из a_table» в базе данных MySQL с агентом, подключенным к JVM. Но я получаю следующее исключение:

javassist.CannotCompileException: [source error] no such field: $$probe
    at javassist.CtBehavior.insertBefore(CtBehavior.java:725)
    at javassist.CtBehavior.insertBefore(CtBehavior.java:685)

Это происходит на линии executeQuery.insertBefore("this.$$probe.start();");. Странно то, что это не происходит каждый раз при выполнении метода «probeStatement»: метод сначала вызывается для класса com.mysql.jdbc.PreparedStatement, и никаких исключений не возникает. Затем вызывается метод для класса com.mysql.jdbc.ServerPreparedStatement. Выбрасывается исключение. Метод также вызывается для класса com.mysql.jdbc.JDBC4PreparedStatement, и также генерируется исключение. И ServerPreparedStatement, и JDBC4PreparedStatementпродлевают com.mysql.jdbc.PreparedStatement, так что, возможно, это как-то связано...

Я новичок в вещах javassist и java-агента, поэтому, возможно, ответ очевиден, но я не могу понять, почему возникает это исключение.


person mael    schedule 10.08.2013    source источник


Ответы (1)


Я думаю, проблема в следующем:
вы добавляете поле $$probe в currentClass (ваш public class com.mysql.jdbc.ServerPreparedStatement extends com.mysql.jdbc.PreparedStatement, а не напрямую реализует java.sql.PreparedStatement), но вы переопределяете метод com.mysql.jdbc.PreparedStatement.executeQuery() (из javadoc: CtClass.getMethod() - Возвращенный метод может быть объявлен в суперклассе.), и вы не вводите поле в com.mysql.jdbc.PreparedStatement, и это может вызвать исключение ошибки компиляции (когда вы используете инструмент com.mysql.jdbc.PreparedStatement, ошибка не выдается, потому что вы вводите поле в java.sql.PreparedStatement.executeQuery() класс реализации).
Вы сталкиваетесь с такой ситуацией (просто идея):

class com.mysql.jdbc.ServerPreparedStatement$$Probed {
  private fr.mael.package.SQLProbe $$probe;

  public void set$$probe(fr.mael.package.SQLProbe probe){ this.$$probe = probe;}
}
class com.mysql.jdbc.PreparedStatement$$Probed {
  public java.sql.ResultSet executeQuery() {
    // this.$$probe in this class doesn't exists! 
    this.$$probe.start();
    ...
    this.$$probe.end();
  }
}

Извлеките из currentClass первый суперкласс, который реализует (или @Override) executeQuery(), и внедрите $$probe в или @Override executeQuery() в ваш текущий класс

Например, переопределение executeQuery() в currentClass у вас будет

class com.mysql.jdbc.ServerPreparedStatement$$Probed {
  private fr.mael.package.SQLProbe $$probe;

  public void set$$probe(fr.mael.package.SQLProbe probe){ this.$$probe = probe;}

  @Override
  public java.sql.ResultSet executeQuery() {
    this.$$probe.start();
    super.executeQuery();
    this.$$probe.end();
  }
}

Я надеюсь быть ясным, английский не мой родной язык.

person Luca Basso Ricci    schedule 22.08.2013
comment
Спасибо, вы наставили меня на правильный путь! Я фактически исправил это, используя getDeclaredMethod. getDeclaredMethod генерирует исключение NotFoundException, если метод не объявлен в текущем классе. Использование блока try/catch кажется неправильным, но я не смог найти способ проверить, объявлен ли метод в классе. - person mael; 26.08.2013
comment
Как только проблема найдена, решение находится под углом. Надеюсь, вы скоро найдете хорошее решение! - person Luca Basso Ricci; 26.08.2013