Java - "перехватить" приватный метод - PullRequest
0 голосов
/ 09 марта 2019

Я знаю, что об этом уже спрашивали, и ответ обычно - "ты не можешь" и / или "нет", но я все равно пытаюсь это сделать.

Ситуация такова, что яЯ пытаюсь настроить «черную магию» для помощи в тестировании.Мой код работает в конечном счете под 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, согласно документации ).

Итак, да, я признаю, что это пытается вызвать темные силы, и что это обычно хмуритсяна.Остается вопрос, это выполнимо?

Ответы [ 2 ]

0 голосов
/ 19 марта 2019

используя 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 () внедряет строку кода перед оператором возврата метода или как последнюю строку метода в случае использования методов void.

См. эту ссылку для получения дополнительной информации о методах, доступных для измерительных приборов. Надеюсь это поможет!

0 голосов
/ 15 марта 2019

Если вы можете выполнить некоторый код, например, в общедоступном статическом основном блоке 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. (но с добавлением нескольких хаков, даже во время выполнения)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...