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 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 type 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 = InvokeProxetta.withAspects( new InvokeAspect() { @Override public InvokeReplacer pointcut(InvokeInfo invokeInfo) { if (invokeInfo.getMethodName().equals("foo")) { return InvokeReplacer.with(Replacer.class, "bar"); } return null; } } };
Invoke aspect from the example is set on all invocations of method
called foo()
. Invocations will be replaced with the static method
Replacer.bar()
. Now, lets apply the proxy:
One one = proxetta.builder(One.class).newInstance();
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 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.
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 the target method. Replacement method
is defined as a class name and a method name.
Replacement methods must be static!
Moreover, using InvokeReplacer
we can instruct to pass some additional
arguments to replaced method, depending of the target method's signature.
Dynamic replacements
Because replacement method is defined as a string, we can build them dynamically, as in following example:
new InvokeAspect() { @Override public InvokeReplacer pointcut(InvokeInfo 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 as 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 powerful feature. Lets 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, lets replace constructor call with Proxetta:
InvokeProxetta proxetta = InvokeProxetta.withAspects( new InvokeAspect() { @Override public InvokeReplacer pointcut(InvokeInfo 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
Replacement method receives all arguments as 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 limitation 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 constructor.