mael
mael

Reputation: 2254

ClassFileTransformer + Javassist: no such field

Ok, what I am trying to do is doing a java agent that would monitor an application. So, I am trying to inject code in PreparedStatements to measure SQL queries execution times. To do that, I have developped a class implementing ClassFileTransformer. It looks like that:

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();");
    }

}

So, what I want to do is injecting code in each class (in the "executeQuery()" method) that implements java.sql.PreparedStatement. To test it, I am just running a basic "select * from a_table" on a MySQL database with the agent attached to the JVM. But I am getting the following exception:

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

It happens on the line executeQuery.insertBefore("this.$$probe.start();");. What is weird is that is does not happen each time the "probeStatement" method is executed: the method is first called for the class com.mysql.jdbc.PreparedStatement, and no exception is thrown. Then, the method is called for the class com.mysql.jdbc.ServerPreparedStatement. The exception is thrown. The method is also called for the class com.mysql.jdbc.JDBC4PreparedStatement and the exception is thrown too. Both ServerPreparedStatement and JDBC4PreparedStatementextend com.mysql.jdbc.PreparedStatement so maybe it is somehow related...

I am new to javassist and java agent things, so maybe the answer is obvious, but I can't get why this exception is thrown.

Upvotes: 3

Views: 2140

Answers (1)

Luca Basso Ricci
Luca Basso Ricci

Reputation: 18413

I think problem is this:
You are adding field $$probe in currentClass (your public class com.mysql.jdbc.ServerPreparedStatement extends com.mysql.jdbc.PreparedStatement, not directly implements java.sql.PreparedStatement) but your are override method com.mysql.jdbc.PreparedStatement.executeQuery() (from javadoc: CtClass.getMethod() - The returned method may be declared in a super class.) and you haven't inject field into com.mysql.jdbc.PreparedStatement and this can cause the compilation error exception (when you instrument com.mysql.jdbc.PreparedStatement no error is thrown because you are inject field into java.sql.PreparedStatement.executeQuery() implementor class).
You run into this situation (just an idea):

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();
  }
}

Extract from currentClass the first superclass that implements (or @Override) executeQuery() and inject $$probe into or @Override executeQuery() in your current class

Foe example, overriding executeQuery() in currentClass you will have

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();
  }
}

I hope to be clear, English is not my native language.

Upvotes: 3

Related Questions