I am trying to replace a method with a different method that has a different return type but it seems like the only way I can successfully do it is to have the source code of all methods that have a call site to the replaced method. I have even tried instrumenting the calling methods to call the new replacement method but I get a VerifyError (Bad type on operand stack). Is there a way to only use the bytecode and not depend on the source code when rebuilding dependent methods?
Functioning Example (with source code dependency)
public class OverrideTest {
final ClassPool POOL = ClassPool.getDefault();
class Foo {
public Integer getFoo() { return new Integer(1); }
public String doA() { return getFoo().toString(); }
}
class FooReplacement {
public String getFoo() { return "A"; }
}
@Test
public void d() throws Throwable {
CtClass srcClass = POOL.getCtClass(Foo.class.getName());
CtClass extClass = POOL.getCtClass(FooReplacement.class.getName());
CtClass d = POOL.makeClass("Derp");
CtClass STRING = POOL.get("java.lang.String");
CtClass INT = POOL.get("java.lang.Integer");
{
CtMethod doA1 = srcClass.getMethod("doA", Descriptor.ofMethod(STRING, new CtClass[0]));
CtMethod getFoo1 = srcClass.getMethod("getFoo", Descriptor.ofMethod(INT, new CtClass[0]));
CtMethod getFoo = new CtMethod(INT, "getFoo", new CtClass[0], d);
CtMethod doA = new CtMethod(STRING, "doA", new CtClass[0], d);
d.addMethod(doA);
d.addMethod(getFoo);
doA.setBody(doA1, null);
getFoo.setBody(getFoo1, null);
d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);
d.removeMethod(getFoo);
CtMethod getFooReplaced = new CtMethod(STRING, "getFoo", new CtClass[0], d);
d.addMethod(getFooReplaced);
CtMethod getFooReplaced1 = extClass.getMethod("getFoo", Descriptor.ofMethod(STRING, new CtClass[0]));
getFooReplaced.setBody(getFooReplaced1, null);
doA.setBody("{ return getFoo().toString(); }");
d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);
}
{
Class<?> c = d.toClass();
Constructor<?> ctor = c.getConstructor();
Object derp = ctor.newInstance();
Method getFoo = derp.getClass().getMethod("getFoo");
Method doA = derp.getClass().getMethod("doA");
Object doResult = doA.invoke(derp);
Object getResult = getFoo.invoke(derp);
assertEquals("A", getResult);
assertEquals("A", doResult);
}
}
}
Non-Functioning Example (VerifyError)
public class OverrideTest {
final ClassPool POOL = ClassPool.getDefault();
class Foo {
public Integer getFoo() { return new Integer(1); }
public String doA() { return getFoo().toString(); }
}
class FooReplacement {
public String getFoo() { return "A"; }
}
@Test
public void d() throws Throwable {
CtClass srcClass = POOL.getCtClass(Foo.class.getName());
CtClass extClass = POOL.getCtClass(FooReplacement.class.getName());
CtClass d = POOL.makeClass("Derp");
CtClass STRING = POOL.get("java.lang.String");
CtClass INT = POOL.get("java.lang.Integer");
{
CtMethod doA1 = srcClass.getMethod("doA", Descriptor.ofMethod(STRING, new CtClass[0]));
CtMethod getFoo1 = srcClass.getMethod("getFoo", Descriptor.ofMethod(INT, new CtClass[0]));
CtMethod getFoo = new CtMethod(INT, "getFoo", new CtClass[0], d);
CtMethod doA = new CtMethod(STRING, "doA", new CtClass[0], d);
d.addMethod(doA);
d.addMethod(getFoo);
doA.setBody(doA1, null);
getFoo.setBody(getFoo1, null);
d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);
CtMethod tempMethod = new CtMethod(getFoo.getReturnType(), "tempFoo", new CtClass[0], d);
d.addMethod(tempMethod);
doA.instrument(new MethodReplacer(getFoo, tempMethod));
d.removeMethod(getFoo);
CtMethod getFooReplaced = new CtMethod(STRING, "getFoo", new CtClass[0], d);
d.addMethod(getFooReplaced);
CtMethod getFooReplaced1 = extClass.getMethod("getFoo", Descriptor.ofMethod(STRING, new CtClass[0]));
getFooReplaced.setBody(getFooReplaced1, null);
doA.instrument(new MethodReplacer(tempMethod, getFooReplaced));
d.removeMethod(tempMethod);
d.removeMethod(doA);
CtMethod doA2 = new CtMethod(STRING, "doA", new CtClass[0], d);
d.addMethod(doA2);
doA2.setBody(doA, null);
d.setModifiers(d.getModifiers() & ~Modifier.ABSTRACT);
}
{
Class<?> c = d.toClass();
Constructor<?> ctor = c.getConstructor();
Object derp = ctor.newInstance();
Method getFoo = derp.getClass().getMethod("getFoo");
Method doA = derp.getClass().getMethod("doA");
Object doResult = doA.invoke(derp);
Object getResult = getFoo.invoke(derp);
assertEquals("A", getResult);
assertEquals("A", doResult);
}
}
class MethodReplacer extends ExprEditor {
private CtMethod replacedMethod;
private CtMethod replacement;
MethodReplacer(CtMethod replacedMethod, CtMethod replacement) {
this.replacedMethod = replacedMethod;
this.replacement = replacement;
}
@Override
public void edit(MethodCall mcall) throws CannotCompileException {
CtClass declaringClass = replacedMethod.getDeclaringClass();
try {
CtMethod m = mcall.getMethod();
if (declaringClass.equals(m.getDeclaringClass()) && m.equals(replacedMethod))
mcall.replace("$_ = " + replacement.getName()+"($$);");
} catch (NotFoundException e) {
throw new RuntimeException("Unable to instrument a dependent method call to " + replacedMethod.getName(), e);
}
}
}
}
When you compile a class with a single method:
class Foo {
Object bar() { }
}
this method's descriptor which is part of the method's signature will look something like this after compilation:
()Ljava/lang/Object;
Note that the return type is part of the method signature! Thus, any call to the method is made explicitly to a method that returns an instance of type Object
. If you changes the return type to for example an Integer
, the descriptor would instead be ()Ljava/lang/Integer;
.
If you had compiled a class Qux
to call the method of Foo
with the ()Ljava/lang/Object;
descriptor but then changed the method in Foo
to return an Integer
instead, the method call could not be dispatched by the JVM without recompiling Qux
: For the JVM, the method Qux
is calling does no longer exist. The same issue applied for another method defined in Foo
when you use a tool like javassist to only "recompile" a single method without recompiling methods that call the first method. This is the problem the verifier complains about in your example.
However, the Java compiler knows the concept of bridge methods for this purpose where Foo
would look like in a pseudo Java code that represents the generated Java byte code.
class Foo {
Object bar() { this.[Integer]bar(); }
Integer bar() { }
}
after its compilation where this.[Integer]bar();
represents a call to the bar
method that returns an Integer
. The [Object]bar()
method represents a bridge method in this context. You could emulate the creation of such a bridge method yourself by first redefining the method signature to return a different type and additionally adding a bridge method with the original return type.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With