New Tricks with Dynamic Proxies in Java 8 (part 2)
from https://opencredo.com/dynamic-proxies-java-part-2/
Consider an instance of java.reflection.InvocationHandler
that simply passes every method call through to an underlying instance:
public class PassthroughInvocationHandler implements InvocationHandler { private final Object target; public PassthroughInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(target, args); } }
To create a proxy using this invocation handler, we use the newProxyInstance
static utility method on thejava.reflection.Proxy
class:
@SuppressWarnings("unchecked") public static <T> T proxying(T target, Class<T> iface) { return (T) Proxy.newProxyInstance( iface.getClassLoader(), new Class<?>[] { iface }, new PassthroughInvocationHandler(target)); }
The method newProxyInstance
takes three parameters: the classloader to use, an array of interfaces that the created proxy must implement, and the InvocationHandler
to handle method calls with. It will create a new proxy class at runtime if none already exists, and return an instance of that class which dispatches method calls to the supplied InvocationHandler
. Once a proxy class has been created for a particular classloader and set of interfaces, that class is cached and re-used – it is as efficient as a hand-rolled implementation of the bridge between the desired interface and InvocationHandler
would have been.
For convenience, here’s a utility method for creating new proxies which derives the classloader from the target interface, and allows additional interfaces to be specified using a varargs parameter:
public static <T> T simpleProxy(Class<? extends T> iface, InvocationHandler handler, Class<?>...otherIfaces) { Class<?>[] allInterfaces = Stream.concat( Stream.of(iface), Stream.of(otherIfaces)) .distinct() .toArray(Class<?>[]::new); return (T) Proxy.newProxyInstance( iface.getClassLoader(), allInterfaces, handler); }
We can use it to create a new pass-through proxy like so:
public static <T> T passthroughProxy(Class<? extends T> iface, T target) { return simpleProxy(iface, new PassthroughInvocationHandler(target)); }
Performance Implications
What is the performance impact of using dynamic proxies? I ran the following tests using Contiperf:
public class PassthroughInvocationHandlerPerformanceTest { @Rule public final ContiPerfRule rule = new ContiPerfRule(); public interface RandomNumberGenerator { long getNumber(); } private static final Random random = new Random(); private static final RandomNumberGenerator concreteInstance = random::nextLong; private static final RandomNumberGenerator proxiedInstance = passthroughProxy( RandomNumberGenerator.class, concreteInstance); @Test @PerfTest(invocations = 1000, warmUp = 200) public void invokeConcrete() { getAMillionRandomLongs(concreteInstance); } @Test @PerfTest(invocations = 1000, warmUp = 200) public void invokeProxied() { getAMillionRandomLongs(proxiedInstance); } private void getAMillionRandomLongs(RandomNumberGenerator generator) { for (int i = 0; i < 1000000; i++) { generator.getNumber(); } } }
Two implementations of RandomNumberGenerator
, one concrete and the other proxied, were asked for a million random numbers each. Each test was repeated a thousand times, with a 200ms warm-up. The results (to two significant places) were as follows:
Concrete instance | Proxied instance | |
Max ms | 31 | 34 |
Average ms | 22.4 | 25 |
Median ms | 22 | 24 |
With an average difference of 2.6ms per million invocations, these results suggest an overhead (on my laptop) for simple pass-through proxying of around 2.6 nanoseconds per call, once the JVM has had a chance to optimize everything.
Obviously a more complicated process of method dispatch will introduce a greater overhead, but this shows that the performance impact of merely introducing proxying is negligible for most purposes.
What’s different in Java 8
Java 8 introduces three new language features which are relevant for our purposes here. The first is static methods on interfaces, which can be used to supply a proxied implementation of the interface to which they belong, e.g.
public interface PersonMatcher extends Matcher<Person> { static PersonMatcher aPerson() { return MagicMatcher.proxying(PersonMatcher.class); } PersonMatcher withName(String expected); PersonMatcher withName(Matcher<String> matcher); PersonMatcher withAge(int expected); PersonMatcher withAge(Matcher<Integer> ageMatcher); }
Previously, we would have had to add the aPerson
method to some other class; now we can conveniently bundle it together with the interface it instantiates. (Note that static
methods aren’t implemented by a proxy, as they’re attached to the interface rather than the instance).
The second relevant new language feature is default
methods on interfaces. These can be very useful, but require some special handling. Suppose our Person
class has a dateOfBirth
field of type LocalDate
, and we would like to be able to use a String
(e.g. “2000-05-05”) to specify the expected date. We can provide this option with a default
method on the matcher interface that performs the necessary type conversion, as follows:
PersonMatcher withDateOfBirth(LocalDate expected); default PersonMatcher withDateOfBirth(String expected) { return withDateOfBirth(LocalDate.parse(expected, DateTimeFormatter.ISO_DATE)); }
In order for this to work, the proxy’s InvocationHandler
must be able to recognize default
method invocations and dispatch them appropriately (how to do this is covered below).
The final new language feature is lambda expressions. These don’t have much impact on the kinds of interfaces we generate proxies for, but they do facilitate a particular approach to building InvocationHandlers
, which is the subject of the next section.
A functional method interpretation stack
The basic task of an InvocationHandler
is to decide what to do with a method call, perform the necessary action, and return a result. We can split this task into two parts: one which decides what to do for a given method, and another which executes that decision. First, we define a @FunctionalInterface
for the method call handler, which defines the executable behaviour for a given method.
1 @FunctionalInterface 2 public interface MethodCallHandler { 3 Object invoke(Object proxy, Object[] args) throws Throwable; 4 }
Then we define a MethodInterpreter
interface which finds the correct MethodCallHandler
for each method.
@FunctionalInterface public interface MethodInterpreter extends InvocationHandler { @Override default Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodCallHandler handler = interpret(method); return handler.invoke(proxy, args); } MethodCallHandler interpret(Method method); }
Note that MethodInterpreter
extends, and provides a default implementation for, InvocationHandler
. This means that any lambda expression which can be assigned to MethodInterpreter
can also be automatically “promoted” into an InvocationHandler
.
Once we have made this separation, we can wrap any MethodInterpreter
in a chain of interceptors to modify its behaviour. Each interceptor in the chain will be responsible either for handling some particular kind of case, or for modifying the behaviour of interceptors further down the chain.
For example, assuming that the same method will always be interpreted in the same way, we can wrap ourMethodInterpreter
so that its interpretation is cached, replacing the cost of “interpreting” a method with the cost of looking up a MethodCallHandler
in a Map
:
public static MethodInterpreter caching(MethodInterpreter interpreter) { ConcurrentMap<Method, MethodCallHandler> cache = new ConcurrentHashMap<>(); return method -> cache.computeIfAbsent(method, interpreter::interpret); }
For long-lived proxy instances on which the same methods will be called many times, such as service classes, caching the method interpretation can provide a small performance boost.
There are two kinds of “special case” that may be worth handling separately. The first is calls to generic Object
methods, such as equals
, hashCode
and toString
. These we will typically want to pass through to some underlying “instance” object that represents the identity (and holds the state) of the proxy. The second is calls todefault
methods, which as mentioned above require some special handling. We’ll deal with default
methods first.
Here’s the function which wraps a MethodInterpreter
so that it can handle calls to default
methods:
public static MethodInterpreter handlingDefaultMethods(MethodInterpreter nonDefaultInterpreter) { return method -> method.isDefault() ? DefaultMethodCallHandler.forMethod(method) : nonDefaultInterpreter.interpret(method); }
Either the method is a default method, in which case we return a MethodCallHandler
which dispatches the call directly to the default method, or we use the supplied nonDefaultInterpreter
to work out what to do with it.
The DefaultMethodCallHandler
class uses some reflection trickery to get a suitable MethodHandle
for thedefault
method, and dispatches the call to that:
final class DefaultMethodCallHandler { private DefaultMethodCallHandler() { } private static final ConcurrentMap<Method, MethodCallHandler> cache = new ConcurrentHashMap<>(); public static MethodCallHandler forMethod(Method method) { return cache.computeIfAbsent(method, m -> { MethodHandle handle = getMethodHandle(m); return (proxy, args) -> handle.bindTo(proxy).invokeWithArguments(args); }); } private static MethodHandle getMethodHandle(Method method) { Class<?> declaringClass = method.getDeclaringClass(); try { Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class .getDeclaredConstructor(Class.class, int.class); constructor.setAccessible(true); return constructor.newInstance(declaringClass, MethodHandles.Lookup.PRIVATE) .unreflectSpecial(method, declaringClass); } catch (IllegalAccessException | NoSuchMethodException | InstantiationException | InvocationTargetException e) { throw new RuntimeException(e); } } }
This is fairly ugly code, but fortunately we only have to write it once.
For binding calls to equals
, hashCode
and toString
, or to any other methods defined on an interface which some “target” object implements, we implement a wrapper which checks to see whether the called method can be handled by the target object, and fails over to an unboundHandler
for any methods that aren’t implemented by the target:
public static MethodInterpreter binding(Object target, MethodInterpreter unboundInterpreter) { return method -> { if (method.getDeclaringClass().isAssignableFrom(target.getClass())) { return (proxy, args) -> method.invoke(target, args); } return unboundInterpreter.interpret(method); }; }
We might even decide that this “target” object is the only handler available to field method calls at this point in the chain, and that calls to methods not supported by the object should fail with an exception:
public static MethodInterpreter binding(Object target) { return binding(target, method -> { throw new IllegalStateException(String.format( "Target class %s does not support method %s", target.getClass(), method)); }); }
Finally, we can wire in interceptors that can observe and modify method calls and decide whether or not to pass them down the chain of MethodCallHandlers
. We do this by defining another @FunctionalInterface
,MethodCallInterceptor
:
@FunctionalInterface public interface MethodCallInterceptor { Object intercept(Object proxy, Method method, Object[] args, MethodCallHandler handler) throws Throwable; default MethodCallHandler intercepting(Method method, MethodCallHandler handler) { return (proxy, args) -> intercept(proxy, method, args, handler); } }
and then applying the interception to an InterpretingMethodHandler
like this:
public static MethodInterpreter intercepting(MethodInterpreter interpreter, MethodCallInterceptor interceptor) { return method -> interceptor.intercepting(method, interpreter.interpret(method)); }
At this point, we have the ability to construct a stack of wrapped MethodInterpreters
that will progressively build up a method call handler for each method handled by an InvocationHandler
. We can now use these to help generate proxies of various kinds. I’ll conclude this post by showing how to build a proxy that provides method intercepting behaviour similar to that of the Spring AOP framework.
Proxy example: Intercepting proxy
The interceptingProxy
method below creates an intercepting proxy that wraps an underlying implementation of some interface, sending every call against the interface to the underlying object but providing the suppliedMethodCallInterceptor
with the opportunity to record or modify the call:
public static <T> T interceptingProxy(T target, Class<T> iface, MethodCallInterceptor interceptor) { return simpleProxy(iface, caching(intercepting( handlingDefaultMethods(binding(target)), interceptor))); } }
Note the ordering of wrappers, in particular that intercepting
comes before handlingDefaultMethods
, so thatdefault
method invocations are also intercepted, and that caching
wraps everything. Here it is in use, recording method calls against a Person
instance into a List<String>
of callDetails
.
public interface Person { String getName(); void setName(String name); int getAge(); void setAge(int age); default String display() { return String.format("%s (%s)", getName(), getAge()); } } public static final class PersonImpl implements Person { // Standard bean implementation } @Test public void interceptsCalls() { Person instance = new PersonImpl(); List<String> callDetails = new ArrayList<>(); MethodCallInterceptor interceptor = (proxy, method, args, handler) -> { Object result = handler.invoke(proxy, args); callDetails.add(String.format("%s: %s -> %s", method.getName(), Arrays.toString(args), result)); return result; }; Person proxy = Proxies.interceptingProxy(instance, Person.class, interceptor); proxy.setName("Arthur Putey"); proxy.setAge(42); assertThat(proxy.display(), equalTo("Arthur Putey (42)")); assertThat(callDetails, contains( "setName: [Arthur Putey] -> null", "setAge: [42] -> null", "getName: null -> Arthur Putey", "getAge: null -> 42", "display: null -> Arthur Putey (42)")); }
In the final post in this series, I’ll explore some more sophisticated and useful examples. As before, code implementing the above can be found in the proxology github repository.