Java — «перехватить» частный метод

#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 одно за другим, слева направо. Это должно работать, по крайней мере, на openjdk

4. Итак, что касается ответов на данный момент … 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 тогда просто попробуйте оба решения, но сначала одно из них следует поместить в место, где ваш код выполняется первым, чтобы дать ему наибольшие шансы на работу.