New Tricks With Dynamic Proxies In Java 8 (part 3)

from https://opencredo.com/new-tricks-with-dynamic-proxies-in-java-8-part-3/

In this post, the last in the New Tricks With Dynamic Proxies series (see part 1 and part 2), I’m going to look at using dynamic proxies to create bean-like value objects to represent records.

The basic idea here is to have some untyped storage for a collection of property values, such as an array of Objects, and a typed wrapper around that storage which provides a convenient and type-safe access mechanism. A dynamic proxy is used to convert calls on getter and setter methods in the wrapper interface into calls which read and write values in the store. We want to be able to define an entire value type purely through its access methods, like so:

public interface Person {

    static Person create() {
        return BeanProxy.proxying(Person.class);
    }

    String getName();
    void setName(String name);
    int getAge();
    void setAge(int age);
}

and have equalshashCode and toString functionality generated along with access to each instance’s storage.

A naive implementation of this approach might look like this:

public class BeanProxy implements MethodInterpreter {

    public static <T> T proxying(Class iface) {
        return Proxies.simpleProxy(iface, new BeanProxy());
    }

    private Map<String, Object> store = new HashMap();

    @Override
    public MethodCallHandler interpret(Method method) {
    if (isGetter(method)) {
            return (proxy, args) -> store.get(getPropertyName(method));
        }
        if (isSetter(method)) {
            return (proxy, args) -> store.put(getPropertyName(method), args[0]);
        }
        throw new IllegalArgumentException("Method " + method + " is neither a getter nor a setter.");
    }

    // Implementations of isGetter, isSetter and getPropertyName go here
}

 

The principal shortcoming of this approach is that we have to do a lot of work every time a method is called. We could cache the method interpretation, but then each proxy object would be carrying around its ownMethodCallHandler cache which it would have to populate independently. What we really want to do, since we are given the whole interface at the point the proxy is created, is analyse that interface and set up the method dispatch for all of its methods at once – and then re-use this analysis across all future proxy instances targetting the same interface.

Bean property analysis

The methods we’re interested in here are the bean property methods which follow the Java Bean conventions:getFoo or isFoo for a getter, setFoo for a setter. We can use the java.beans.Introspector to get a collection of PropertyDescriptors for all of the properties exposed by an interface, and then extract the property name and getter/setter method pair from each PropertyDescriptor. Here’s a class which packages up that analysis, exposing an array of property names and indices into that array for the corresponding getter and setter methods:

public final class BeanPropertyAnalysis {

    public static BeanPropertyAnalysis forClass(Class<?> iface) {
        BeanInfo beanInfo = Nonchalantly.invoke(() -> Introspector.getBeanInfo(iface)); // ignores checked exception
        PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
        return new BeanPropertyAnalysis(descriptors);
    }

    private final PropertyDescriptor[] descriptors;

    public BeanPropertyAnalysis(PropertyDescriptor[] descriptors) {
        this.descriptors = descriptors;
    }

    public String[] getPropertyNames() {
        return Stream.of(descriptors)
                .map(PropertyDescriptor::getName)
                .toArray(String[]::new);
    }

    public Map<Method, Integer> getGetterIndices() {
        return indicesForMethods(PropertyDescriptor::getReadMethod);
    }

    public Map<Method, Integer> getSetterIndices() {
        return indicesForMethods(PropertyDescriptor::getWriteMethod);
    }

    private Map<Method, Integer> indicesForMethods(Function<PropertyDescriptor, Method> methodSelector) {
        return IntStream.range(0, descriptors.length)
                .collect(HashMap::new,
                        (map, index) -> map.put(methodSelector.apply(descriptors[index]), index),
                        Map::putAll);
    }
}

 

Once we have this information for a class, we would like to re-use it across proxy instances. We can do this by separating out the proxy implementation into two classes: BeanProxySchema, which represents the re-usable part, and BeanProxyStorage, which represents the state of each individual instance.

The Bean Proxy Schema

Let’s look at BeanProxySchema first:

public class BeanProxySchema {

    private static final Function<Class<?>, BeanProxySchema> CACHED = Memoizer.memoize(BeanProxySchema::forClassUncached);

    public static BeanProxySchema forClass(Class<?> iface) {
        return CACHED.apply(iface);
    }

    private static BeanProxySchema forClassUncached(Class<?> iface) {
        BeanPropertyAnalysis ifacePropertyAnalysis = BeanPropertyAnalysis.forClass(iface);

        return new BeanProxySchema(
                ifacePropertyAnalysis.getPropertyNames(),
                ClassInterpreter.mappingWith(
                        getInterpreter(
                                ifacePropertyAnalysis.getGetterIndices(),
                                ifacePropertyAnalysis.getSetterIndices()))
                        .interpret(iface));
    }

    private static UnboundMethodInterpreter<BeanProxyStorage> getInterpreter(Map<Method, Integer> getterIndices, Map<Method, Integer> setterIndices) {
        return method -> {
            if (getterIndices.containsKey(method)) {
                int slotIndex = getterIndices.get(method);
                return storage -> (proxy, args) -> storage.get(slotIndex);
            }

            if (setterIndices.containsKey(method)) {
                int slotIndex = setterIndices.get(method);
                return storage -> (proxy, args) -> storage.set(slotIndex, args[0]);
            }

            throw new IllegalArgumentException(String.format("Method %s is neither a getter nor a setter", method));
        };
    }

    private final String[] propertyNames;
    private final UnboundMethodInterpreter<BeanProxyStorage> unboundMethodInterpreter;

    public BeanProxySchema(String[] propertyNames, UnboundMethodInterpreter<BeanProxyStorage> unboundMethodInterpreter) {
        this.propertyNames = propertyNames;
        this.unboundMethodInterpreter = unboundMethodInterpreter;
    }

    public String formatValues(Object[] data) {
        return IntStream.range(0, propertyNames.length)
                .mapToObj(i -> String.format("%s: %s", propertyNames[i], data[i]))
                .collect(Collectors.joining(",", "{", "}"));
    }

    public BeanProxyStorage createStorage() {
        return new BeanProxyStorage(this, new Object[propertyNames.length]);
    }

    public MethodInterpreter getMethodInterpreter(BeanProxyStorage storage) {
        return unboundMethodInterpreter.bind(storage);
    }
}

 

There is quite a lot going on here. The schema keeps hold of two pieces of information: firstly, an array of property names, which it uses to create a meaningful toString representation for a corresponding array of property values in the formatValues method; secondly, an UnboundMethodInterpreter which has to be bound to an object representing a proxy instance’s state before it can be used as a MethodInterpreter.

The idea here is that the UnboundMethodInterpreter represents everything that can be known about how to dispatch methods for a particular class in the absence of any instance state – it is consequently re-usable across different proxy instances. It can be bound to a BeanProxyStorage object representing a particular instance’s state to get a MethodInterpreter that knows how to interpret method calls for that instance.

We can see how this works in the getInterpreter method, which returns a function that takes a Method and returns a function that takes any BeanProxyStorage instance and returns a MethodCallHandler for that method called against that instance. What’s really happening here is a sort of currying, where instead of writing thislambda:

(method, state, proxy, args) -> result

we write these nested lambdas:

method -> state -> (proxy, args) -> result

This style of higher-order functional programming, in which we build functions that build functions that build functions, may seem unfamiliar for Java, but it’s an effective way of separating out the “interpretation” work that can be done early and re-usably from the “last-minute” work that must be done when a proxy method is actually called on some particular instance.

Bean proxy storage

Here is the BeanProxyStorage class, which wraps the Object[] array containing a proxy instance’s property values:

public final class BeanProxyStorage implements EqualisableByState {

    private final BeanProxySchema schema;
    private final Object[] values;

    BeanProxyStorage(BeanProxySchema schema, Object[] values) {
        this.schema = schema;
        this.values = values;
    }

    public Object get(int index) {
        return values[index];
    }

    public Object set(int index, Object value) {
        values[index] = value;
        return null;
    }

    @Override
    public String toString() {
        return schema.formatValues(values);
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof BeanProxyStorage)) {
            return false;
        }

        BeanProxyStorage other = (BeanProxyStorage) o;
        return Objects.equals(schema, other.schema)
            && Arrays.deepEquals(values, other.values);
    }

    @Override
    public int hashCode() {
        return Arrays.deepHashCode(values);
    }

    public MethodInterpreter getMethodInterpreter() {
        return MethodInterpreters.binding(this, schema.getMethodInterpreter(this));
    }

    @Override
    public Object getState() {
        return this;
    }
}

 

As you can see, the BeanProxyStorage maintains a reference to the corresponding BeanProxySchema, which it uses to generate its toString representation and when checking for equality. It also implements theEqualisableByState interface, which provides a single method, getState, which returns the BeanProxyStorageinstance itself. What is this for? Well, we have to have a way of comparing two proxy instances, and neither can directly access the state of another (since the underlying InvocationHandler of each proxy is just a lambda expression wrapping a chain of method interpretation and execution). Accordingly, we need to intercept calls toequals, and ensure that both proxies implement EqualisableByState so that their state can be retrieved and compared.

Equality with EqualisableByState

Here’s what actually happens in MethodInterpreters.binding:

  public static MethodInterpreter binding(Object target, MethodInterpreter unboundInterpreter) {
        MethodCallHandler equaliser = getEqualiserFor(target);

        return method -> {
            if (method.equals(EQUALS_METHOD)) {
                return equaliser;
            }

            if (method.getDeclaringClass().isAssignableFrom(target.getClass())) {
                return (proxy, args) -> method.invoke(args);
            }

            return unboundInterpreter.interpret(method);
        };
    }

    private static MethodCallHandler getEqualiserFor(Object target) {
        if (target instanceof EqualisableByState) {
            Object targetState = ((EqualisableByState) target).getState();
            return (proxy, args) -> hasEqualState(targetState, args[0]);
        }

        return (proxy, args) -> target.equals(args[0]);
    }

    private static boolean hasEqualState(Object state, Object other) {
        return other instanceof EqualisableByState
                && state.equals(((EqualisableByState) other).getState());
    }

 

The MethodInterpreter returned from this interprets an incoming method in one of three ways:

  1. equals is handled by a special equaliser method, which uses EqualisableByState.getState to get the underlying state of two proxy objects and compare them for equality.
  2. hashCodetoString and getState are handled by the bound “target” object, in this case an instance ofBeanProxyState.
  3. All other methods – i.e. getters and setters defined on the proxied interface – are handled by the suppliedunboundInterpreter, which in this case is the one provided by the BeanProxySchema.

Proxy instantiation

It remains only to provide a method for instantiating bean proxy instances:

public final class BeanProxy {

    private BeanProxy() {
    }

    public static <T> T proxying(Class<> proxyClass) {
        BeanProxySchema schema = BeanProxySchema.forClass(proxyClass);
        BeanProxyStorage storage = schema.createStorage();

        return Proxies.simpleProxy(proxyClass, storage.getMethodInterpreter(), EqualisableByState.class);
    }

}

This method first obtains the schema for the requested proxy class (using a cached method, so that it is generated only once and then lookup up thereafter), then gets a storage object matching that schema. Finally, it creates a proxy which implements both the requested proxy class and, additionally, EqualisableByState, supplying theMethodInterpreter generated by the storage instance as its InvocationHandler.

Closing remarks

This is a somewhat “deconstructed” style of Java: we have separated out parts of an object that ordinarily would belong together, and used functional programming techniques to assemble a chain of interpreters so that decisions about what to do with a given method call are handled in different places by functions specialising in different cases. The result is a fairly efficient implementation, in which nearly everything is pre-calculated and cached that can be, so that the work needed to dispatch each individual method call on a proxy instance is kept to a minimum.

The complete implementation of BeanProxy may be found in the proxology source code repository, along with the code from the previous two installments in this series.

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