All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.perfectable.introspection.proxy.MethodInvocation Maven / Gradle / Ivy

There is a newer version: 5.1.0
Show newest version
package org.perfectable.introspection.proxy;

import org.perfectable.introspection.query.ConstructorQuery;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nullable;

import com.google.common.primitives.Primitives;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.concurrent.LazyInit;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
import static org.perfectable.introspection.Introspections.introspect;

/**
 * Capture of a information needed to invoke a method.
 *
 * 

This class collects and mimics arguments received by different proxy frameworks when method is intercepted on * proxy object, represents it in uniform fashion and allows some manipulations. Instances of this class can be also * constructed synthetically, as if the call was made but intercepted before execution. * *

This class handles three elements of a call: Method that was called, a receiver, that is object on which method * was called, and arguments that were passed with the call. Receiver can be omitted, if the method is static. * *

There are two most important methods on this class: {@link #invoke} which will execute the method with specified * arguments and receiver, and return result or throw an exception. The other method is {@link #decompose} which * allows introspection of structure of this invocation. * *

Objects of this class are unmodifiable, that is methods of this class that would change the object actually * produce new changed object. It is not immutable, because both receiver and arguments passed can, and often are, * mutable. * * @param Type of method receiver (i.e. {@code this}) */ public final class MethodInvocation implements Invocation { private static final Object[] EMPTY_ARGUMENTS = new Object[0]; private final Method method; @Nullable private final T receiver; private final Object[] arguments; @LazyInit private transient MethodHandle handle; private static final MethodHandle PRIVATE_LOOKUP_CONSTRUCTOR = findPrivateLookupConstructor(); /** * Create invocation that was intercepted from proxy mechanism. * *

This method assumes that if {@code method} is vararg, and requires X non-vararg parameters, the * {@code arguments} passed here contains exactly X+1 elements, where X one are the non-vararg arguments that are * compatible with non-vararg parameter type, and the last element is an array of elements of the vararg type. * This is how the method call is represented in runtime for varargs. This method will 'unroll' the last array * argument and create invocation that has flat arguments array. * *

For non-varargs {@code method}, this method is identical to {@link #of}, i.e. {@code arguments} must be a * flat array with element count exactly equal to method parameter count, and with compatible types. * * @param method method that was/will be called on invocation * @param receiver receiver of the method call (i.e. {@code this}) * @param arguments arguments in a runtime representation * @param type of receiver * @return method invocation comprised from passed arguments * @throws IllegalArgumentException when method invocation is illegal and will not succeed: ex. method is static * and receiver was provided (or other way around), receiver is of wrong type for the provided method, * or arguments are not matching method parameter types. */ public static MethodInvocation intercepted(Method method, @Nullable T receiver, @Nullable Object... arguments) { Object[] actualArguments = flattenVariableArguments(method, arguments); return of(method, receiver, actualArguments); } /** * Create synthetic invocation from scratch. * *

This method assumes that if {@code method} is vararg, and requires X non-vararg parameters, * {@code arguments} contain at least X elements, where each of these elements is compatible with corresponding * parameter of the method, and any amount of elements that are compatible with the variable parameter of the * method. * *

For non-varargs {@code method}, this method expects an array with element count exactly equal to method * parameter count, and with compatible types. * * @param method method that was/will be called on invocation * @param receiver receiver of the method call (i.e. {@code this}) * @param arguments arguments in a source representation * @param type of receiver * @return method invocation comprised from passed arguments * @throws IllegalArgumentException when method invocation is illegal and will not succeed: ex. method is static * and receiver was provided (or other way around), receiver is of wrong type for the provided method, * or arguments are not matching method parameter types. */ public static MethodInvocation of(Method method, @Nullable T receiver, Object... arguments) { requireNonNull(method); // receiver might be null requireNonNull(arguments); verifyCallability(method, receiver, arguments); Object[] argumentsClone = arguments.clone(); return new MethodInvocation<>(method, receiver, argumentsClone); } @SuppressWarnings("ArrayIsStoredDirectly") private MethodInvocation(Method method, @Nullable T receiver, Object... arguments) { this.method = method; this.receiver = receiver; this.arguments = arguments; } /** * Executes the configured invocation. * * @return result of non-throwing invocation. If the method was {@code void}, the result will be null. * @throws Throwable result of throwing invocation. This will be exactly the exception that method thrown. */ @CanIgnoreReturnValue @Nullable @Override public Object invoke() throws Throwable { if (handle == null) { handle = createHandle(); } return handle.invoke(); } /** * Interface that allows decomposition of the invocation. * * @param type of receiver expected * @param type of result of decomposition. */ @FunctionalInterface public interface Decomposer { /** * Decomposition method. * * @param method method that was called * @param receiver receiver that the method was called on, or null if the method was static * @param arguments arguments passed to the method, in source (flat) representation * @return result of decomposition */ R decompose(Method method, @Nullable T receiver, Object... arguments); } /** * Decomposes the invocation to its parts. * *

This method allows to transform this invocation by its parts into other object. * *

For example, decomposition might produce log message of method called: *

	 *     Decomposer<Object, String> stringifingDecomposer = (method, receiver, arguments) ->
	 *         String.format("Method %s was called on %s with %s", method, receiver, arguments);
	 *     LOGGER.debug(invocation.decompose(stringifingDecomposer))
	 * 
* *

Another example: decomposer might substitute invocation method for another one: *

	 *     Decomposer<Object, MethodInvocation<?>> replacingDecomposer = (method, receiver, arguments) ->
	 *         MethodInvocation.of(anotherMethod, receiver, arguments);
	 *     MethodInvocation<?> replacedMethodInvocation = invocation.decompose(replacingDecomposer))
	 *     return replacedMethodInvocation.invoke();
	 * 
* * @param decomposer decomposer to use for this invocation * @param return type of decomposition * @return whatever decomposer returned on its {@link Decomposer#decompose} call */ @CanIgnoreReturnValue public R decompose(Decomposer decomposer) { return decomposer.decompose(method, receiver, arguments.clone()); } /** * Creates new invocation with replaced method. * *

New method is checked for compatibility with both receiver and arguments. * * @param newMethod another method to be used * @return new invocation with new method, same receiver and same arguments * @throws IllegalArgumentException when new method is incompatible with receiver or arguments in any way */ public MethodInvocation withMethod(Method newMethod) { verifyReceiverCompatibility(newMethod, receiver); verifyArgumentsCompatibility(newMethod, arguments); return new MethodInvocation<>(newMethod, receiver, arguments); } /** * Creates new invocation with replaced receiver. * *

New receiver is checked for compatibility with method. * * @param newReceiver another receiver to be used * @param extension type of the receiver, to allow concretization of result * @return new invocation with same method, new receiver and same arguments * @throws IllegalArgumentException when new receiver is incompatible with method */ public MethodInvocation withReceiver(X newReceiver) { verifyReceiverCompatibility(method, newReceiver); return new MethodInvocation<>(method, newReceiver, arguments); } /** * Creates new invocation with replaced arguments. * *

New arguments is checked for compatibility with method. * * @param newArguments new arguments to be used * @return new invocation with same method, same receiver and new arguments * @throws IllegalArgumentException when new arguments is incompatible with method */ public MethodInvocation withArguments(Object... newArguments) { verifyArgumentsCompatibility(method, newArguments); return new MethodInvocation<>(method, receiver, newArguments); } @Override public int hashCode() { return Objects.hash(this.method, this.receiver, Arrays.hashCode(this.arguments)); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof MethodInvocation)) { return false; } MethodInvocation other = (MethodInvocation) obj; return Objects.equals(this.method, other.method) && Objects.equals(this.receiver, other.receiver) && Arrays.equals(this.arguments, other.arguments); } private static void verifyCallability(Method method, @Nullable Object receiver, Object... arguments) { verifyReceiverCompatibility(method, receiver); verifyArgumentsCompatibility(method, arguments); } private static void verifyReceiverCompatibility(Method method, @Nullable Object receiver) { if ((method.getModifiers() & Modifier.STATIC) == 0) { checkArgument(receiver != null, "Method %s is not static, got null as receiver", method); Class requiredType = method.getDeclaringClass(); checkArgument(requiredType.isInstance(receiver), "Method %s requires %s as receiver, got %s", method, requiredType, receiver); } else { checkArgument(receiver == null, "Method %s is static, got %s as receiver", method, receiver); } } private static void verifyArgumentsCompatibility(Method method, Object... arguments) { Class[] formals = method.getParameterTypes(); boolean isVarArgs = method.isVarArgs(); if (isVarArgs) { checkArgument(arguments.length >= formals.length - 1, "Method %s requires at least %s arguments, got %s", method, formals.length - 1, arguments.length); } else { checkArgument(arguments.length == formals.length, "Method %s requires %s arguments, got %s", method, formals.length, arguments.length); } for (int i = 0; i < arguments.length; i++) { Class parameterType; if (isVarArgs && i >= formals.length - 1) { parameterType = formals[formals.length - 1].getComponentType(); } else { parameterType = formals[i]; } Object argument = arguments[i]; if (argument == null) { checkArgument(!parameterType.isPrimitive(), "Method %s has primitive %s as parameter %s, got null argument", method, parameterType, i + 1); } else { Class argumentType = argument.getClass(); Class wrappedParameterType = Primitives.wrap(parameterType); checkArgument(wrappedParameterType.isAssignableFrom(argumentType), "Method %s takes %s as parameter %s, got %s as argument", method, wrappedParameterType, i + 1, argument); } } } private static Object[] flattenVariableArguments(Method method, @Nullable Object[] actuals) { if (actuals == null) { return EMPTY_ARGUMENTS; } if (!method.isVarArgs()) { return actuals; } Class[] formals = method.getParameterTypes(); Object variableActual = actuals[actuals.length - 1]; int variableLength = Array.getLength(variableActual); int resultSize = (formals.length - 1) + variableLength; Object[] result = new Object[resultSize]; System.arraycopy(actuals, 0, result, 0, formals.length - 1); for (int i = 0; i < variableLength; i++) { result[formals.length - 1 + i] = Array.get(variableActual, i); } return result; } @SuppressWarnings("IllegalCatch") private MethodHandle createHandle() { MethodHandles.Lookup lookup; try { lookup = (MethodHandles.Lookup) PRIVATE_LOOKUP_CONSTRUCTOR.invoke(method.getDeclaringClass()); } catch (Throwable throwable) { throw new AssertionError(throwable); } MethodHandle methodHandle; try { methodHandle = lookup.unreflect(method); } catch (IllegalAccessException e) { throw new AssertionError(e); } if (receiver != null) { methodHandle = methodHandle.bindTo(receiver); } if (method.isVarArgs()) { Class[] parameterTypes = method.getParameterTypes(); Class lastParameterType = parameterTypes[parameterTypes.length - 1]; int overflowArguments = arguments.length - parameterTypes.length + 1; methodHandle = methodHandle.asCollector(lastParameterType, overflowArguments); } return MethodHandles.insertArguments(methodHandle, 0, arguments); } private static MethodHandle findPrivateLookupConstructor() { int allModifiers = MethodHandles.Lookup.PUBLIC | MethodHandles.Lookup.PROTECTED | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PRIVATE; MethodHandles.Lookup lookup = MethodHandles.lookup(); ConstructorQuery generalConstructorQuery = introspect(MethodHandles.Lookup.class) .constructors(); Optional> noPreviousClassConstructorOption = generalConstructorQuery .parameters(Class.class, int.class) .asAccessible() .option(); if (noPreviousClassConstructorOption.isPresent()) { Constructor lookupConstructor = noPreviousClassConstructorOption.get(); MethodHandle methodHandle; try { methodHandle = lookup.unreflectConstructor(lookupConstructor); } catch (IllegalAccessException e) { throw new AssertionError(e); } return MethodHandles.insertArguments(methodHandle, 1, allModifiers); } Optional> withPreviousClassConstructorOption = generalConstructorQuery .parameters(Class.class, Class.class, int.class) .asAccessible() .option(); if (withPreviousClassConstructorOption.isPresent()) { Constructor lookupConstructor = withPreviousClassConstructorOption.get(); MethodHandle methodHandle; try { methodHandle = lookup.unreflectConstructor(lookupConstructor); } catch (IllegalAccessException e) { throw new AssertionError(e); } return MethodHandles.insertArguments(methodHandle, 1, null, allModifiers); } throw new AssertionError("Finding private lookup constructor was unsuccessful"); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy