#java #reflection
#java #отражение
Вопрос:
Я знаю, что об этом спрашивали раньше, и ответ обычно «вы не можете» и / или «не надо», но я все равно пытаюсь это сделать.
Контекст заключается в том, что я пытаюсь настроить некоторую «черную магию», чтобы помочь в тестировании. Мой код в конечном счете выполняется под управлением 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, согласно документации).
Итак, да, я признаю, что это попытка вызвать темные силы, и что это обычно не одобряется. Остается вопрос, выполнимо ли это?
Комментарии:
1. Просто не тестируйте частные методы. Протестируйте общедоступные методы; их внутренняя организация не должна быть важной.
2. И наоборот, если вам действительно нужно протестировать (или смоделировать) частный метод, подумайте о том, чтобы сделать его вместо этого закрытым пакетом. Обычно это достаточно хороший флаг, чтобы другие знали, что им не следует возиться с ним, в то же время позволяя большинству тестовых макетных библиотек легко получить к нему доступ.
3. Скопируйте исходный код
IFaceImpl
, измените его ‘ видимость, скомпилируйте его, тщательно измените путь к классу, чтобы ваша версияIFaceImpl
загружалась раньше внешней библиотеки. jvm внутренне используетURLClassPath
для поиска файлов классов, который сканирует каждое местоположение в classpath одно за другим, слева направо. Это должно работать, по крайней мере, на openjdk4. Итак, что касается ответов на данный момент … 1. Я не пытаюсь протестировать эти методы, я просто хочу получить доступ к внутреннему экземпляру Target, который они используют. 2. У меня нет доступа для изменения видимости метода. 3. Это не было описано в оригинале, но у меня также нет средств, чтобы гарантировать, какая версия будет выбрана. Очевидно, что я мог бы просто скопировать класс, отредактировать его и использовать копию, но есть также причины не делать этого здесь, которые, по крайней мере, так же сильны, как не использовать черную магию.
5. @BenLeitner можете ли вы выполнить эту настройку при запуске приложения? Поскольку есть изящный трюк, который вы можете сделать, но только если вы сможете выполнить некоторый код ДО загрузки IFaceImpl.
Ответ №1:
используя javassist, вы можете использовать someMethod() в классе IClassImpl для отправки экземпляра TargetClass в какой-либо другой класс и сохранить его там или выполнить другие манипуляции с использованием созданного экземпляра.
этого можно достичь с помощью метода insertAfter( ) в javassist .
Например :
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.
Обратитесь к этой ссылке для получения дополнительной информации о методах, доступных для инструментирования. Надеюсь, это поможет!
Комментарии:
1. Похоже, это было бы именно то, что я искал, спасибо!
Ответ №2:
Если вы можете выполнить некоторый код, например, в основном блоке public static 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. (но можно добавить еще несколько взломов даже во время выполнения)
Комментарии:
1. При запуске нет, но я могу запустить код до создания рассматриваемого экземпляра.
2. @BenLeitner тогда просто попробуйте оба решения, но сначала одно из них следует поместить в место, где ваш код выполняется первым, чтобы дать ему наибольшие шансы на работу.