Java-“拦截”私有方法

时间:2019-03-09 02:12:45

标签: java reflection

我知道这已经被问过了,答案通常是“你不能”和/或“不要”,但是我还是想尝试一下。

上下文是,我正在尝试设置一些“黑魔法”来辅助测试。我的代码最终在JUnit下运行,并且系统的本质是,尽管我可以访问我可能想要的大多数任何库(ByteBuddy,Javassist等),但是在运行该代码之前我无法使用它,我一直在忙于上课。

这是设置:

// 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) {...}
}

在测试不可思议性内,我有一个IFace实例,我碰巧知道它是一个IFaceImpl。我希望要做的是能够窃取内部产生的Target实例。实际上,这将具有以下效果(如果可以重写私有方法):

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

但是,那当然不行,因为私有方法不能被覆盖。 因此,下一类方法是类似ByteBuddyJavassist

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

在我测试这些模式时,它在其他情况下仍然有效,即我想拦截的方法是程序包本地的,但不适用于私有方法(预期为ByteBuddy,根据the documentation)。

所以,是的,我知道这是在试图唤起黑暗的力量,而这通常是令人皱眉的。问题仍然存在,这可行吗?

2 个答案:

答案 0 :(得分:0)

如果您可以在公共静态void主块中执行某些代码,或者在加载IFaceImpl之前执行某些代码,则可以在加载该类之前直接使用javassist编辑该类-这样就可以将方法更改为公开,再添加一个,等等:

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

库代码如下:

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

如果在加载该类之前无法执行代码,则需要使用工具化,我将使用byte-buddy-agent库来简化此操作:

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

由于模块的工作方式,这两个版本在Java 9及更高版本上运行都可能会有更多问题,您可能需要添加其他启动标志。
请注意,在Java 8上,客户端JRE可能没有工具化。 (但即使在运行时也可以添加更多的hack)

答案 1 :(得分:0)

使用javassist,您可以在IClassImpl类中检测someMethod(),以将TargetClass的实例发送到其他类并将其存储在其他类中,或者使用创建的实例进行其他操作。

这可以使用javassist中的insertAfter()方法来实现。

例如:

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 ###} }

insertAfter()方法在方法的return语句之前插入一行代码,在使用void方法的情况下,将其插入到方法的最后一行。

有关可用于检测方法的更多信息,请参见this link。 希望这可以帮助!