简体   繁体   中英

Java - “intercept” a private method

I know this has been asked before, and the answer is usually "you can't" and/or "don't," but I'm trying this anyway.

The context is that I'm trying to set up some "black magic" to aid in testing. My code is running ultimately under JUnit and the nature of the system is such that, while I have access to most any library I could want (ByteBuddy, Javassist, etc), I can't play around with the code prior to it running, I'm stuck with working with classes on the fly.

Here's the setup:

// External Library that I have no control over:
package com.external.stuff;

/** This is the thing I ultimately want to capture a specific instance of. */
public class Target {...}

public interface IFace {
  void someMethod();
}

class IFaceImpl {
  @Override  
  void someMethod() {
     ...
     Target t = getTarget(...);
     doSomethingWithTarget(t);
     ...
  }

  private Target getTarget() {...}
  private void doSomethingWithTarget(Target t) {...}
}

Within my test magic-ness, I have an instance of IFace, which I happen to know is an IFaceImpl. What I'd like to do is be able to steal the instance of Target produced internally. Effectively, this would have the same effect as the following (if private methods were overrideable):

class MyIFaceImpl extends IFaceImpl{
  private Consumer<Target> targetStealer;

  @Override  
  void someMethod() {
     ...
     Target t = getTarget(...);
     doSomethingWithTarget(t);
     ...
  }

  /** "Override" either this method or the next one. */
  private Target getTarget() {
    Target t = super.getTarget();
    targetStealer.accept(t);
    return t;
  }

  private void doSomethingWithTarget(Target t) {
    targetStealer.accept(t);
    super.doSomethingWithTarget(t);
  }
}

But, of course, that doesn't work as private methods cannot be overridden. So the next type of approach would be something like ByteBuddy or Javassist

public static class Interceptor {
  private final Consumer<Target> targetStealer;
  // ctor elided

  public  void doSomethingWithTarget(Target t) {
    targetStealer.accept(t);
  }
}


/** Using ByteBuddy. */
IFace byteBuddyBlackMagic(
    IFace iface /* known IFaceImpl*/,
    Consumer<Target> targetStealer) {
  return (IFace) new ByteBuddy()
      .subClass(iface.getClass())
      .method(ElementMatchers.named("doSomethingWithTarget"))
      .intercept(MethodDelegation.to(new Interceptor(t))
      .make()
      .load(...)
      .getLoaded()
      .newInstance()
}

/** Or, using Javassist */
IFace javassistBlackMagic(
    IFace iface /* known IFaceImpl*/,
    Consumer<Target> targetStealer) {
  ProxyFactory factory = new ProxyFactory();
  factory.setSuperClass(iface.getClass());
  Class subClass = factory.createClass();
  IFace = (IFace) subClass.newInstance();

  MethodHandler handler =
      new MethodHandler() {
        @Override
        public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
          if (thisMethod.getName().equals("doSomethingWithTarget")) {
            consumer.accept((Target) args[0]);
          }
          return proceed.invoke(self, args);
        }
      };
  ((ProxyObject) instance).setHandler(handler);
  return instance;
}

and as I was testing out these pattern, it worked in other cases where the method I wanted to intercept was package-local, but not for private methods (expected for ByteBuddy, per the documentation ).

So, yes, I recognize that this is attempting to invoke dark powers, and that this is normally frowned upon. The question remains, is this doable?

If you can execute some code in like public static void main block, or just before IFaceImpl is loaded, then you can use javassist to edit that class directly before it is loaded - so you can change method to be public, add another one, etc:

public class Main {
    public static void main(String[] args) throws Exception {
        // this would return "original"
//        System.out.println(IFace.getIFace().getName());
        // IFaceImpl class is not yet loaded by jvm
        CtClass ctClass = ClassPool.getDefault().get("lib.IFaceImpl");
        CtMethod getTargetMethod = ctClass.getDeclaredMethod("getTarget");
        getTargetMethod.setBody("{ return app.Main.myTarget(); }");
        ctClass.toClass(); // now we load our modified class

        // yay!
        System.out.println(IFace.getIFace().getName());
    }

    public static Target myTarget() {
        return new Target("modified");
    }
}

where library code is like this:

public interface IFace {
    String getName();
    static IFace getIFace() {
        return new IFaceImpl();
    }
}
class IFaceImpl implements IFace {
    @Override public String getName() {
        return getTarget().getName();
    }
    private Target getTarget() {
        return new Target("original");
    }
}
public class Target {
    private final String name;
    public Target(String name) {this.name = name;}
    public String getName() { return this.name; }
}

If there is no way to execute your code before that class is loaded, then you need to use instrumentalization, I will use byte-buddy-agent library to make this simpler:

public class Main {
    public static void main(String[] args) throws Exception {
        // prints "original"
        System.out.println(IFace.getIFace().getName());

        Instrumentation instrumentation = ByteBuddyAgent.install();
        Class<?> implClass = IFace.getIFace().getClass();
        CtClass ctClass = ClassPool.getDefault().get(implClass.getName());
        CtMethod getTargetMethod = ctClass.getDeclaredMethod("getTarget");
        getTargetMethod.setBody("{ return app.Main.myTarget(); }");
        instrumentation.redefineClasses(new ClassDefinition(implClass, ctClass.toBytecode()));

        // yay!
        System.out.println(IFace.getIFace().getName());
    }

    public static Target myTarget() {
        return new Target("modified");
    }
}

Both versions might be much more problematic to run on java 9 and above due to how modules work, you might need to add additional startup flags.
Note that on java 8 instrumentalization might not be present on client JRE. (but with few more hacks can be added, even at runtime)

using javassist you can instrument the someMethod( ) in the IClassImpl class to send the instance of the TargetClass to someother class and store it there or do other manipulations using the instance created.

this can be achieved using the insertAfter( ) method in javassist .

For example :

method.insertAfter( "TestClass.storeTargetInst(t)" ); // t is the instance of Target class in IClassImpl.someMethod

TestClass{ public static void storeTargetInst(Object o){ ### code to store instance ###} }

The insertAfter() method injects a line of code before the return statement of a method or as the last line of a method in case of void methods.

Refer this link for more information on the methods available for instrumentation. Hope this helps!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM