7

I'm stuck with javassist. I've added a new method to my object class on runtime.

My object class:

package tmp3;

public class Car {
    public Car(){}
}

My test class:

package tmp3;

import java.lang.reflect.Method;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class TestMain {
    public static void main(String[] args) {
        try {

            CtClass ctclass = ClassPool.getDefault().get("tmp3.Car");
            CtMethod newmethod = CtNewMethod.make("public void testPrint() { System.out.println(\"test ok\"); }",ctclass);
            ctclass.addMethod(newmethod);
            ctclass.writeFile();

            for(Method me: ctclass.toClass().getDeclaredMethods()){ //test print, ok
                System.out.println(me.getName());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

But after that point, I don't know how to call(invoke) it. I've read that javassist has not the capability to invoke methods. Then how can I invoke the method which I've just added with javassist?

I've tried lots of things in two days but had no success. Could you please help me with this?

Korki Korkig
  • 2,736
  • 9
  • 34
  • 51

2 Answers2

9

Java classes have static interfaces which means, as you probably already know, Java is not designed by default to add methods to a class at runtime so it's a bit tricky, but not that hard to achieve what you want.

You've used Javassist, a bytecode modifier framework, to engineer your compiled class to add more bytecode that represents a new method. You can have one of the two scenarios:

Scenario 1: Code which is compiled along with your Car class before injection

In this case, when your code is being compiled the Java Compiler only knows the Car interface without any injections. So you can't just invoke the injected method directly, like this:

 Car car = new Car();
 car.testPrint();

You have to do it by reflection like @Scorpion correctly commented:

 Car car = new Car();
 Method method = car.getClass().getMethod("testPrint", new Class[]{});
 method.invoke(car,new Object[]{});

But this is not the only way...

Scenario 2: Code which USES your compiled and injected Class

If you compile your Car class, inject it and afterwards write code against the compiled class (for example having the Car class in a jar file) you'll be able to call your injected method as if it were any other regular method.

Do the following exercise:

  1. Compile your Car class
  2. Run your TestMain which will do the injection
  3. Create another project in your IDE and add to that project's classpath the directory with the injected class OR create a jar with only the injected class and add that jar to the classpath
  4. Create a class in the new project that creates a new Car instance, notice that you're now able to invoke testPrint method without any hassle.

A few things you should keep attention:

  • If you're overwriting your original class with the injected class, you might end up with an invalid class resulting in a java.lang.ClassFormatError with an error message saying that you have a Truncated Class file. This happens if Javassist hasn't loaded all the bytecode to memory and tried to write and read to and from the same class file, which results in a total mess. To avoid this, you can either write to a different path or make sure you load all the bytecode to memory before writing the file (use the toByteCode() from CtClass) .
  • If you have two class files, one with the injected code and one with the original code, remember to have only one in your classpath.
pabrantes
  • 2,161
  • 1
  • 28
  • 34
  • Thank you for your detailed quick answer. I'll try your second solution but I prefer the reflection as you and @Scorpion suggested. I'll be more comfortable with it. I've added your code to the bottom from the first solution but it gives "NoSuchMethodException". I think it gets the compiled class instead of injected? – Korki Korkig Dec 10 '12 at 13:09
  • 1
    @KorkiKorkig: yes, most probably you're getting the original class instead of the injected one. You can check which class you are loading by accessing it's protection domain like this `object.getClass().getProtectionDomain().getCodeSource().getLocation()` which will print you the URL from which class file the class was loaded. Even in scenario 1, you have to make sure only one of the `Car` classes is in your project's classpath and it should be the injected. Also to check the bytecode (in order to see if the class was sucessfully injected) you can use **javap** – pabrantes Dec 10 '12 at 13:24
  • @ pabrantes I could get the method from the right class by modifiying your code as `Class refClass = ctclass.toClass(); Method method = refClass.getDeclaredMethod("testPrint"); Car car = new Car(); method.invoke(car,new Object[]{});` But if I declare a Car object at top, just before all the modifications, it gives "attempted duplicate class definition for name: "tmp3/Car"" exception at the `Class computerClass = ctclass.toClass();` line. Is there a way to get ride of this? – Korki Korkig Dec 10 '12 at 15:30
  • @KorkiKorkig: You're mixing things, for your test you need 2 different classes with main, one is your TestMain that does the injection, the other is one that you don't have yet that does the instrumentation test present in scenario1. These two main classes **should be separated projects** to be easier for you. The 1st one has the original car class in it's classpath, the 2nd one only the injected. Use writeFile(PATH) to write your injected class to somewhere else and make sure that other path is in your 2nd project classpath. That way you'll be able to use the classes independently. – pabrantes Dec 10 '12 at 15:41
  • @KorkiKorkig: if you want to do the injection and instrumentation all in the same, you can't load the Car class before (which is what happens when you don't reference the `Car car = new Car()` before injection) or you have to play tricks with customer classloaders to allow class reloading. – pabrantes Dec 10 '12 at 15:42
0

Use java reflection to invoke it, javassist for the class modification and loading ... then java reflection to run it (invoke ...).