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 Objectmethods, such as equalshashCode 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 equalshashCode 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.

posted @ 2016-03-16 15:10  princessd8251  阅读(142)  评论(0编辑  收藏  举报