[英]Java - “intercept” a private method
我知道這已經被問過了,答案通常是“您不能”和/或“不要”,但是無論如何我都在嘗試。
上下文是我正在嘗試設置一些“黑魔法”來輔助測試。 我的代碼最終在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)。
因此,是的,我認識到這是在試圖喚起黑暗力量,而這通常是令人皺眉的。 問題仍然存在,這可行嗎?
如果您可以在公共靜態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)
使用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語句之前或在空方法的情況下,將方法的最后一行插入代碼行。
有關可用於儀器的方法的更多信息,請參考此鏈接 。 希望這可以幫助!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.