net.freeutils.util.DelegatingProxy Maven / Gradle / Ivy
Show all versions of jelementary Show documentation
/*
* Copyright © 2003-2024 Amichai Rothman
*
* This file is part of JElementary - the Java Elementary Utilities package.
*
* JElementary is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* JElementary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JElementary. If not, see .
*
* For additional info see https://www.freeutils.net/source/jelementary/
*/
package net.freeutils.util;
import java.lang.reflect.*;
import java.util.*;
/**
* The {@code DelegatingProxy} class wraps a delegation target object using a proxy class.
*
* The default behavior of this class is to invoke any called method
* of the proxy interfaces on the target object.
*
* However, if the self-invoke mechanism is specified during initialization,
* then any method that is implemented in this class (or its subclass) with
* the exact same signature as an interface method will be invoked directly
* on this instance instead of invoking it on the target class, effectively
* allowing target methods to be overridden by this class.
*
* Such overriding methods can internally invoke the original methods directly
* on the {@link #getTarget target} object if necessary.
*
* Alternatively, a subclass can override the {@link #invoke} method itself
* to customize the invocation mechanism.
*
* @param the type of the proxy class
*/
public class DelegatingProxy implements InvocationHandler {
/**
* Holds the data for an overridden method.
*/
protected static class MethodOverride {
protected final int hash;
protected final Method method;
protected final Method override;
/**
* Constructs a new MethodOverride instance.
*
* @param method the overridden method
* @param overload the overriding method
* @param hash the hash code used to look up the method
*/
public MethodOverride(Method method, Method overload, int hash) {
this.method = method;
this.override = overload;
this.hash = hash;
}
}
protected final T target;
protected final T proxy;
protected final MethodOverride[] overrides;
/**
* Constructs a {@link DelegatingProxy} for the given target.
*
* @param selfInvoke if true, any invocation of a proxy method will invoke a
* method of this object with an identical signature if one exists;
* otherwise, it is invoked directly on the target
* @param target the delegated target object
* @param interfaces the list of interfaces for the proxy class to implement
*/
public DelegatingProxy(boolean selfInvoke, T target, Class>... interfaces) {
int methodCount = 0;
for (Class> i : interfaces)
methodCount += i.getMethods().length;
this.target = target;
this.proxy = Reflect.newProxyInstance(this, interfaces);
this.overrides = selfInvoke
? createLookup(canonizeProxyMethods(getOverridingMethods(interfaces)), methodCount)
: null;
}
/**
* Returns the proxy object.
*
* @return the proxy object
*/
public T getProxy() {
return proxy;
}
/**
* Returns the original target object.
*
* This can be used to invoke methods directly on the target.
*
* @return the target object
*/
public T getTarget() {
return target;
}
/**
* Creates a lookup hashtable for efficient method override lookups.
*
* Unfortunately, Method.hashCode/equals are not too efficient and
* HashMap isn't flexible enough to override them without also
* generating a lot of garbage for custom key objects.
*
* Instead, we make our own simple but efficient read-only hashtable
* using a spacious array, cached method name hash and linear probing,
* resulting in a fairly efficient lookup (reference comparison on
* canonized methods, cached name-only hashcode comparison to quickly
* skip slots during linear probing, power-of-two masking for modulo, etc.)
*
* @param overridesMap the map of overridden methods for which the lookup
* hashtable is created
* @param totalMethodCount the total number of methods that may be looked
* up in this hashtable (including ones that are not overridden)
* @return the lookup hashtable
*/
protected MethodOverride[] createLookup(Map overridesMap, int totalMethodCount) {
// table size is always larger than map (loops always end on empty slot),
// power of two for quick mask instead of modulo, and spacious (compared
// to total lookup method count, not only overridden method count),
// to make collisions less likely and linear probes short
totalMethodCount = Math.max(totalMethodCount, overridesMap.size());
int size = Integer.highestOneBit(totalMethodCount) << 3;
int mask = size - 1;
MethodOverride[] overrides = new MethodOverride[size];
for (Map.Entry e : overridesMap.entrySet()) {
int hash = e.getKey().getName().hashCode(); // quick hash
int i = hash & mask; // index
while (overrides[i] != null) // linear probing to find empty slot
i = (i + 1) & mask; // wrap around
overrides[i] = new MethodOverride(e.getKey(), e.getValue(), hash); // store override data
}
return overrides;
}
/**
* Looks up an overridden method in the previously created
* {@link #createLookup lookup hashtable}.
*
* @param method the method to look up
* @return the overriding method, or null if the given method has no override
*/
public Method lookup(Method method) {
MethodOverride[] overrides = this.overrides;
if (overrides != null) {
int mask = overrides.length - 1;
int hash = method.getName().hashCode();
int i = hash & mask;
while (true) {
MethodOverride override = overrides[i];
if (override == null)
return null;
// compare methods as efficiently as we can -
// reference equality for canonized methods,
// cached hashcode comparison for quick fail,
// and full equals only as last resort
if (method == override.method
|| hash == override.hash && method.equals(override.method))
return override.override;
i = (i + 1) & mask;
}
}
return null;
}
/**
* Creates a map from methods of the given interfaces
* to methods of this object that have an identical signature.
*
* @param interfaces classes of methods to lookup in this object
* @return a map from methods of the given interfaces to methods of this object
* @throws RuntimeException if two methods have the same signature
* but declare different thrown exceptions or different return types
*/
protected Map getOverridingMethods(Class>... interfaces) {
// iterate over all interfaces methods and look for same signature in this class
Map map = new HashMap<>();
for (Class> i : interfaces) {
for (Method m : i.getMethods()) {
Method override = Reflect.getMethod(
this.getClass(), m.getName(), m.getParameterTypes());
if (override != null) {
if (!Containers.equalsIgnoreOrder(
override.getExceptionTypes(), m.getExceptionTypes()))
throw new RuntimeException(
"delegated methods must declare identical exceptions: " + m);
if (!override.getReturnType().equals(m.getReturnType()))
throw new RuntimeException(
"delegated methods must have identical return type");
override.setAccessible(true);
map.put(m, override);
}
}
}
return map;
}
/**
* Creates a method map equivalent to the given one, but with keys replaced
* with equivalent methods that exist as fields of the proxy class.
*
* These fields are the ones actually passed in invoke method calls,
* which allows for very quick reference equality checks rather than
* the slower {@link Method#equals} invocations.
*
* This is an implementation-specific optimization dependent on the current
* proxy implementation of the OpenJDK/Oracle JVM, however it should be
* harmless on other implementations (if the canonical keys cannot be found,
* the original key is retained).
*
* @param methods the methods map whose keys are canonized
* @return a new map equivalent to the given map, but with canonized keys
*/
protected Map canonizeProxyMethods(Map methods) {
// first create a lookup map with the replacements to use
Map proxyMethods = new HashMap<>();
for (Field field : proxy.getClass().getDeclaredFields()) {
if (field.getType() == Method.class) {
try {
field.setAccessible(true); // may be inaccessible in newer JDKs
Method method = (Method)field.get(proxy);
proxyMethods.put(method, method);
} catch (Exception ignore) {}
}
}
// then replace the keys where possible
Map canonized = new HashMap<>(methods.size());
for (Map.Entry e : methods.entrySet()) {
Method proxyMethod = proxyMethods.get(e.getKey());
canonized.put(proxyMethod != null ? proxyMethod : e.getKey(), e.getValue());
}
return canonized;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// invoke either the overriding method in this class,
// or the original method on target
Method override = lookup(method);
return override != null ? override.invoke(this, args) : method.invoke(target, args);
} catch (InvocationTargetException ite) {
throw ite.getCause();
}
}
}