我知道这已经被问过了,答案通常是“你不能”和/或“不要”,但是我还是想尝试一下。
上下文是,我正在尝试设置一些“黑魔法”来辅助测试。我的代码最终在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);
}
}
但是,那当然不行,因为私有方法不能被覆盖。
因此,下一类方法是类似ByteBuddy
或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;
}
在我测试这些模式时,它在其他情况下仍然有效,即我想拦截的方法是程序包本地的,但不适用于私有方法(预期为ByteBuddy,根据the documentation)。
所以,是的,我知道这是在试图唤起黑暗的力量,而这通常是令人皱眉的。问题仍然存在,这可行吗?
答案 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。 希望这可以帮助!