vendredi 8 mars 2019

Java - "intercept" a private method

I know this has been asked before, and the answer is usually "you can't" and/or "don't," but I'm trying this anyway.

The context is that I'm trying to set up some "black magic" to aid in testing. My code is running ultimately under JUnit and the nature of the system is such that, while I have access to most any library I could want (ByteBuddy, Javassist, etc), I can't play around with the code prior to it running, I'm stuck with working with classes on the fly.

Here's the setup:

// 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) {...}
}

Within my test magic-ness, I have an instance of IFace, which I happen to know is an IFaceImpl. What I'd like to do is be able to steal the instance of Target produced internally. Effectively, this would have the same effect as the following (if private methods were overrideable):

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);
  }
}

But, of course, that doesn't work as private methods cannot be overridden. So the next type of approach would be something like ByteBuddy or 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;
}

and as I was testing out these pattern, it worked in other cases where the method I wanted to intercept was package-local, but not for private methods (expected for ByteBuddy, per the documentation).

So, yes, I recognize that this is attempting to invoke dark powers, and that this is normally frowned upon. The question remains, is this doable?





Aucun commentaire:

Enregistrer un commentaire