Invocation replacement

And now, something completely different:) Proxetta provides proxy mechanism that may replace almost any method invocation, including object creation (i.e. constructors) with custom method call. For example, it is possible to replace invocation of some interface methods with some custom implementation. Or, ahead to more loose-coupled code, it is possible to replace object creation with a factory method that returns wired and populated objects.

Usage

Usage is identical to how Proxetta works with dynamic proxies - we just need to provide different types of aspects: InvokeAspect. InvokeAspect defines the pointcut, i.e. the method invocation that should be replaced and the advice, i.e. destination method that should be invoked instead.

Here is an example of proxy creation:

Proxetta proxetta = Proxy.invokeProxetta().withAspect(
    invokeInfo -> {
        if (invokeInfo.getMethodName().equals("foo")) {
            return InvokeReplacer.with(Replacer.class, "bar");
        }
        return null;
    }
};

Invoke aspect is set here on all invocations of method named foo(). Invocations will be replaced with the static method Replacer.bar(). Now, let's apply the proxy:

One one = proxetta.proxy().setTarget(One.class).newInstance();

The instance of One is now proxified. If class One looks like this:

public class One {
    public void example1() {
        Two two = new Two();
        int i = two.foo("one");
        System.out.print(i);
    }
}

it will be modified: foo method call will be replaced and generated bytecode will look like this:

public void example1() {
    Two two = new Two();
    int i = Replacer.bar(two);
    System.out.print(i);
}

Nice!

Another example

Let's replace all calls to System.currentTimeMillis() with custom method. For the purpose of this example, let's say that we have a class like this:

public class TimeClass {

    public long time() {
        return System.currentTimeMillis();
    }
}

Ok, here is how to build a proxy class:

TimeClass timeClass =
(TimeClass) InvokeProxetta.withAspects(new InvokeAspect() {
    @Override
    public boolean apply(MethodInfo methodInfo) {
        return methodInfo.isTopLevelMethod();
    }

    @Override
    public InvokeReplacer pointcut(InvokeInfo invokeInfo) {
        if (
                invokeInfo.getClassName().equals("java.lang.System") &&
                invokeInfo.getMethodName().equals("currentTimeMillis")
            ) {
            return InvokeReplacer.with(MySystem.class, "currentTimeMillis");
        }
        return null;
    }
}).builder(TimeClass.class).newInstance();

What we have here? We created InvokeAspect that will be applied on all top-level methods of the target class (TimeClass in this example); and in the pointcut we defined what invocation to replace and the replacement call. Now, the code will call MySystem.currentTimeMillis instead of System's' method in all top-level methods of TimeClass.

Remember:

Methods apply and builder define where to apply the proxy (the target), pointcut defines what invocation to replace in target and the replacement call. {: .attn}

Simple, right :)

InvokeInfo and InvokeReplacer

InvokeInfo contains a lot of information about the methods being invoked. It is used to determine if some method call should be replaced or not. Using InvokeInfo we can get class name, method signatures, arguments etc. of the invoked methods; so we can decide if some invocation is a target one, one that should be replaced.

InvokeReplacer holds information about the replacement method - the one that will be invoked instead of the target method. Replacement method is defined as a class name and a method name.

Replacement methods must be static! {: .attn}

Moreover, using InvokeReplacer we can instruct to pass some additional arguments to replaced method, depending of the target method's signature.

Dynamic replacements

Because the replacement method is defined as a string, we can build them dynamically, as in the following example:

invokeInfo -> {
    return InvokeReplacer.with(Replacer.class,
        invokeInfo.getMethodName() + invokeInfo.getArgumentsCount());
}

Here all method invocation are replaced with Replacer methods which names contains original method name and number of arguments. For example, call to foo("xxx") is replaced with Replacer.foo1("xxx").

Replacements

There are several different types of invocation in Java, so there are as many different replacement points in Proxetta. Each replacement method must take the same number of arguments and must return the same type of result. When replaced method is an instance method, there will be an additional argument holding the reference to the instance.

Virtual invocation

This is a simple method call on an instance, as in above example. Replacement method receives following arguments:

  • reference to instance of method owner (in the above example its two)

  • all arguments of the replaced method

Static methods

Static methods are replaced without any additional arguments.

Interface methods

Similar to virtual methods, interface method calls are replaced with method call that receives an additional argument, that is interface implementation.

Constructors

Constructors' replacement methods doesn't receive any additional argument and must return the created instance. Important: due to VM bytecode, when replacing constructors, there must be a replacement method for each present constructor!

Replacing constructors creation with method invocation might be a powerful feature. Let's take an example:

public class One {
    public void example() {
        Two two = new Two();
        two.hello();
    }
}
public class Two() {
    public String value;
    public void hello() {
        System.out.println(value);
    }
}

If we call method One#example() it would, obviously, print "null". Now, let's replace the constructor call with Proxetta:

InvokeProxetta proxetta = Proxy.invokeProxetta.proxy().withAspect(
        invokeInfo -> {
            if (invokeInfo.getMethodName().equals("<init>")) {
                return InvokeReplacer.with(Replacer.class,
                    "new" + invokeInfo.getClassShortName());
            } else {
                return null;
            }
        }
    );

Now we are replacing all constructors with Replacer#new${ClassName} methods. For example:

public class Replacer {
    public static Two newTwo() {
        Two two = new Two();
        two.value = "hello";
        return two;
    }
}

If we run proxified class One, this time we will have the result hello. And we didn't touch the source of One!

Additional arguments

The replacement method receives all arguments as a replaced method, plus the reference to target instance if available. However, we can instruct InvokeReplacer to provide more arguments in replacement methods. Here are the available additional arguments:

  • owner name

  • method name

  • method signature

  • reference to this

  • target class

All additional arguments are placed at the end, after existing method arguments.

Under the hood

How does the Proxetta do all this? It creates an identical subclass as a target, but with replaced method invocations.

Because of the nature of subclass, there are some VM limitations that we have to be aware of.

Don't call super()

Because super.super is not provided by VM.

Constructors are executed twice

Since subclass copies constructors too, they will be executed as well as the target class constructors. So all initialization will be executed twice, once for proxified class, then for the target class. Therefore, do not put heavy-duty initialization in the constructor.

Last updated