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 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!
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
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.
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"
).
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.
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 are replaced without any additional arguments.
Similar to virtual methods, interface method calls are replaced with method call that receives an additional argument, that is interface implementation.
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
!
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.
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.
Because super.super
is not provided by VM.
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.