net.bytebuddy.utility.dispatcher.JavaDispatcher Maven / Gradle / Ivy
/*
* Copyright 2014 - Present Rafael Winterhalter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.bytebuddy.utility.dispatcher;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.build.AccessControllerPlugin;
import net.bytebuddy.build.HashCodeAndEqualsPlugin;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.utility.Invoker;
import net.bytebuddy.utility.nullability.MaybeNull;
import net.bytebuddy.utility.privilege.GetSystemPropertyAction;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.security.Permission;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.Map;
/**
*
* A dispatcher for creating a proxy that invokes methods of a type that is possibly unknown on the current VM. Dispatchers do not
* use any of Byte Buddy's regular infrastructure, to avoid bootstrapping issues as these dispatchers are used by Byte Buddy itself.
*
*
* By default, this dispatcher uses the Java {@link Proxy} for creating dispatchers. By setting {@code net.bytebuddy.generate} to
* {@code true}, Byte Buddy can generate proxies manually as byte code to mostly avoid reflection and boxing of arguments as arrays.
*
*
* If a security manager is active, the net.bytebuddy.createJavaDispatcher runtime permission is required. Any dispatching
* will be executed from a separate class loader and an unnamed module but with the {@link java.security.ProtectionDomain} of
* the {@link JavaDispatcher} class. It is not permitted to invoke methods of the {@code java.security.AccessController} class or
* to resolve a {@code java.lang.invoke.MethodHandle$Lookup}.
*
*
* @param The resolved type.
*/
@HashCodeAndEqualsPlugin.Enhance
public class JavaDispatcher implements PrivilegedAction {
/**
* A property to determine, that if {@code true}, dispatcher classes will be generated natively and not by using a {@link Proxy}.
*/
public static final String GENERATE_PROPERTY = "net.bytebuddy.generate";
/**
* If {@code true}, dispatcher classes will be generated natively and not by using a {@link Proxy}.
*/
private static final boolean GENERATE = Boolean.parseBoolean(doPrivileged(new GetSystemPropertyAction(GENERATE_PROPERTY)));
/**
* A resolver to assure that a type's package and module are exported to the created class loader.
* This should normally always be the case, but if another library is shading Byte Buddy or otherwise
* manipulates the module graph, this might become necessary.
*/
private static final DynamicClassLoader.Resolver RESOLVER = doPrivileged(DynamicClassLoader.Resolver.CreationAction.INSTANCE);
/**
* Contains an invoker that makes sure that reflective dispatchers make invocations from an isolated {@link ClassLoader} and
* not from within Byte Buddy's context. This way, no privilege context can be leaked by accident.
*/
private static final Invoker INVOKER = doPrivileged(new InvokerCreationAction());
/**
* The proxy type.
*/
private final Class proxy;
/**
* The class loader to resolve the proxied type from or {@code null} if the bootstrap loader should be used.
*/
@MaybeNull
@HashCodeAndEqualsPlugin.ValueHandling(HashCodeAndEqualsPlugin.ValueHandling.Sort.REVERSE_NULLABILITY)
private final ClassLoader classLoader;
/**
* {@code true} if a proxy class should be manually generated.
*/
private final boolean generate;
/**
* Creates a new dispatcher.
*
* @param proxy The proxy type.
* @param classLoader The class loader to resolve the proxied type from or {@code null} if the bootstrap loader should be used.
* @param generate {@code true} if a proxy class should be manually generated.
*/
protected JavaDispatcher(Class proxy, @MaybeNull ClassLoader classLoader, boolean generate) {
this.proxy = proxy;
this.classLoader = classLoader;
this.generate = generate;
}
/**
* A proxy for {@code java.security.AccessController#doPrivileged} that is activated if available.
*
* @param action The action to execute from a privileged context.
* @param The type of the action's resolved value.
* @return The action's resolved value.
*/
@AccessControllerPlugin.Enhance
private static T doPrivileged(PrivilegedAction action) {
return action.run();
}
/**
* Resolves an action for creating a dispatcher for the provided type where the proxied type is resolved from the bootstrap loader.
*
* @param type The type for which a dispatcher should be resolved.
* @param The resolved type.
* @return An action for creating an appropriate dispatcher.
*/
public static PrivilegedAction of(Class type) {
return of(type, null);
}
/**
* Resolves an action for creating a dispatcher for the provided type.
*
* @param type The type for which a dispatcher should be resolved.
* @param classLoader The class loader to resolve the proxied type from.
* @param The resolved type.
* @return An action for creating an appropriate dispatcher.
*/
protected static PrivilegedAction of(Class type, @MaybeNull ClassLoader classLoader) {
return of(type, classLoader, GENERATE);
}
/**
* Resolves an action for creating a dispatcher for the provided type.
*
* @param type The type for which a dispatcher should be resolved.
* @param classLoader The class loader to resolve the proxied type from.
* @param generate {@code true} if a proxy class should be manually generated.
* @param The resolved type.
* @return An action for creating an appropriate dispatcher.
*/
protected static PrivilegedAction of(Class type, @MaybeNull ClassLoader classLoader, boolean generate) {
if (!type.isInterface()) {
throw new IllegalArgumentException("Expected an interface instead of " + type);
} else if (!type.isAnnotationPresent(Proxied.class)) {
throw new IllegalArgumentException("Expected " + type.getName() + " to be annotated with " + Proxied.class.getName());
} else if (type.getAnnotation(Proxied.class).value().startsWith("java.security.")) {
throw new IllegalArgumentException("Classes related to Java security cannot be proxied: " + type.getName());
} else {
return new JavaDispatcher(type, classLoader, generate);
}
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
public T run() {
try {
Object securityManager = System.class.getMethod("getSecurityManager").invoke(null);
if (securityManager != null) {
Class.forName("java.lang.SecurityManager")
.getMethod("checkPermission", Permission.class)
.invoke(securityManager, new RuntimePermission("net.bytebuddy.createJavaDispatcher"));
}
} catch (NoSuchMethodException ignored) {
/* security manager not available on current VM */
} catch (ClassNotFoundException ignored) {
/* security manager not available on current VM */
} catch (InvocationTargetException exception) {
Throwable cause = exception.getTargetException();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
throw new IllegalStateException("Failed to assert access rights using security manager", cause);
}
} catch (IllegalAccessException exception) {
throw new IllegalStateException("Failed to access security manager", exception);
}
Map dispatchers = new HashMap();
boolean defaults = proxy.isAnnotationPresent(Defaults.class);
String name = proxy.getAnnotation(Proxied.class).value();
Class> target;
try {
target = Class.forName(name, false, classLoader);
} catch (ClassNotFoundException exception) {
for (Method method : proxy.getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
}
if (method.isAnnotationPresent(Instance.class)) {
if (method.getParameterTypes().length != 1 || method.getParameterTypes()[0].isPrimitive() || method.getParameterTypes()[0].isArray()) {
throw new IllegalStateException("Instance check requires a single regular-typed argument: " + method);
} else if (method.getReturnType() != boolean.class) {
throw new IllegalStateException("Instance check requires a boolean return type: " + method);
} else {
dispatchers.put(method, Dispatcher.ForDefaultValue.BOOLEAN);
}
} else {
dispatchers.put(method, defaults || method.isAnnotationPresent(Defaults.class)
? Dispatcher.ForDefaultValue.of(method.getReturnType())
: new Dispatcher.ForUnresolvedMethod("Type not available on current VM: " + exception.getMessage()));
}
}
if (generate) {
return (T) DynamicClassLoader.proxy(proxy, dispatchers);
} else {
return (T) Proxy.newProxyInstance(proxy.getClassLoader(),
new Class>[]{proxy},
new ProxiedInvocationHandler(name, dispatchers));
}
}
boolean generate = this.generate;
for (Method method : proxy.getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
}
if (method.isAnnotationPresent(Instance.class)) {
if (method.getParameterTypes().length != 1 || !method.getParameterTypes()[0].isAssignableFrom(target)) {
throw new IllegalStateException("Instance check requires a single regular-typed argument: " + method);
} else if (method.getReturnType() != boolean.class) {
throw new IllegalStateException("Instance check requires a boolean return type: " + method);
} else {
dispatchers.put(method, new Dispatcher.ForInstanceCheck(target));
}
} else if (method.isAnnotationPresent(Container.class)) {
if (method.getParameterTypes().length != 1 || method.getParameterTypes()[0] != int.class) {
throw new IllegalStateException("Container creation requires a single int-typed argument: " + method);
} else if (!method.getReturnType().isArray() || !method.getReturnType().getComponentType().isAssignableFrom(target)) {
throw new IllegalStateException("Container creation requires an assignable array as return value: " + method);
} else {
dispatchers.put(method, new Dispatcher.ForContainerCreation(target));
}
} else if (target.getName().equals("java.lang.invoke.MethodHandles") && method.getName().equals("lookup")) {
throw new UnsupportedOperationException("Cannot resolve Byte Buddy lookup via dispatcher");
} else {
try {
Class>[] parameterType = method.getParameterTypes();
int offset;
if (method.isAnnotationPresent(IsStatic.class) || method.isAnnotationPresent(IsConstructor.class)) {
offset = 0;
} else {
offset = 1;
if (parameterType.length == 0) {
throw new IllegalStateException("Expected self type: " + method);
} else if (!parameterType[0].isAssignableFrom(target)) {
throw new IllegalStateException("Cannot assign self type: " + target + " on " + method);
}
Class>[] adjusted = new Class>[parameterType.length - 1];
System.arraycopy(parameterType, 1, adjusted, 0, adjusted.length);
parameterType = adjusted;
}
Annotation[][] parameterAnnotation = method.getParameterAnnotations();
for (int index = 0; index < parameterType.length; index++) {
for (Annotation annotation : parameterAnnotation[index + offset]) {
if (annotation instanceof Proxied) {
int arity = 0;
while (parameterType[index].isArray()) {
arity += 1;
parameterType[index] = parameterType[index].getComponentType();
}
if (arity > 0) {
if (parameterType[index].isPrimitive()) {
throw new IllegalStateException("Primitive values are not supposed to be proxied: " + index + " of " + method);
} else if (!parameterType[index].isAssignableFrom(Class.forName(((Proxied) annotation).value(), false, classLoader))) {
throw new IllegalStateException("Cannot resolve to component type: " + ((Proxied) annotation).value() + " at " + index + " of " + method);
}
StringBuilder stringBuilder = new StringBuilder();
while (arity-- > 0) {
stringBuilder.append('[');
}
parameterType[index] = Class.forName(stringBuilder.append('L')
.append(((Proxied) annotation).value())
.append(';')
.toString(), false, classLoader);
} else {
Class> resolved = Class.forName(((Proxied) annotation).value(), false, classLoader);
if (!parameterType[index].isAssignableFrom(resolved)) {
throw new IllegalStateException("Cannot resolve to type: " + resolved.getName() + " at " + index + " of " + method);
}
parameterType[index] = resolved;
}
break;
}
}
}
if (method.isAnnotationPresent(IsConstructor.class)) {
Constructor> resolved = target.getConstructor(parameterType);
if (!method.getReturnType().isAssignableFrom(target)) {
throw new IllegalStateException("Cannot assign " + resolved.getDeclaringClass().getName() + " to " + method);
}
if ((resolved.getModifiers() & Opcodes.ACC_PUBLIC) == 0 || (target.getModifiers() & Opcodes.ACC_PUBLIC) == 0) {
resolved.setAccessible(true);
generate = false;
}
dispatchers.put(method, new Dispatcher.ForConstructor(resolved));
} else {
Proxied proxied = method.getAnnotation(Proxied.class);
Method resolved = target.getMethod(proxied == null ? method.getName() : proxied.value(), parameterType);
if (!method.getReturnType().isAssignableFrom(resolved.getReturnType())) {
throw new IllegalStateException("Cannot assign " + resolved.getReturnType().getName() + " to " + method);
}
exceptions:
for (Class> type : resolved.getExceptionTypes()) {
if (RuntimeException.class.isAssignableFrom(type) || Error.class.isAssignableFrom(type)) {
continue;
}
for (Class> exception : method.getExceptionTypes()) {
if (exception.isAssignableFrom(type)) {
continue exceptions;
}
}
throw new IllegalStateException("Resolved method for " + method + " throws undeclared checked exception " + type.getName());
}
if ((resolved.getModifiers() & Opcodes.ACC_PUBLIC) == 0 || (resolved.getDeclaringClass().getModifiers() & Opcodes.ACC_PUBLIC) == 0) {
resolved.setAccessible(true);
generate = false;
}
if (Modifier.isStatic(resolved.getModifiers())) {
if (!method.isAnnotationPresent(IsStatic.class)) {
throw new IllegalStateException("Resolved method for " + method + " was expected to be static: " + resolved);
}
dispatchers.put(method, new Dispatcher.ForStaticMethod(resolved));
} else {
if (method.isAnnotationPresent(IsStatic.class)) {
throw new IllegalStateException("Resolved method for " + method + " was expected to be virtual: " + resolved);
}
dispatchers.put(method, new Dispatcher.ForNonStaticMethod(resolved));
}
}
} catch (ClassNotFoundException exception) {
dispatchers.put(method, defaults || method.isAnnotationPresent(Defaults.class)
? Dispatcher.ForDefaultValue.of(method.getReturnType())
: new Dispatcher.ForUnresolvedMethod("Class not available on current VM: " + exception.getMessage()));
} catch (NoSuchMethodException exception) {
dispatchers.put(method, defaults || method.isAnnotationPresent(Defaults.class)
? Dispatcher.ForDefaultValue.of(method.getReturnType())
: new Dispatcher.ForUnresolvedMethod("Method not available on current VM: " + exception.getMessage()));
} catch (Throwable throwable) {
dispatchers.put(method, new Dispatcher.ForUnresolvedMethod("Unexpected error: " + throwable.getMessage()));
}
}
}
if (generate) {
return (T) DynamicClassLoader.proxy(proxy, dispatchers);
} else {
return (T) Proxy.newProxyInstance(proxy.getClassLoader(),
new Class>[]{proxy},
new ProxiedInvocationHandler(target.getName(), dispatchers));
}
}
/**
* Indicates a proxied type's name. This annotation is mandatory for proxied types but can also be used on method's
* to describe the actual name of the proxied method or on parameters to indicate the parameter's (component) type.
*/
@Documented
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Proxied {
/**
* Returns the binary name of the proxied type.
*
* @return The binary name of the proxied type.
*/
String value();
}
/**
* Indicates that a proxied method is static.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IsStatic {
/* empty */
}
/**
* Indicates that a proxied method is a constructor.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IsConstructor {
/* empty */
}
/**
* Indicates that a method is supposed to perform an instance check. The annotated method must declare a single argument
* with a type that is assignable from the proxied type.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Instance {
/* empty */
}
/**
* Indicates that the method is supposed to return an array of the proxied type. The annotated method must declare a single,
* {@code int}-typed argument that represents the array's dimension.
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Container {
/* empty */
}
/**
* Indicates that a method is supposed to return a default value if a method or type could not be resolved.
*/
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Defaults {
/* empty */
}
/**
* A privileged action for creating an {@link Invoker}.
*/
@HashCodeAndEqualsPlugin.Enhance
private static class InvokerCreationAction implements PrivilegedAction {
/**
* {@inheritDoc}
*/
public Invoker run() {
return DynamicClassLoader.invoker();
}
}
/**
* An {@link Invoker} that uses Byte Buddy's invocation context to use if dynamic class loading is not supported, for example on Android,
* and that do not use secured contexts, where this security measure is obsolete to begin with.
*/
@HashCodeAndEqualsPlugin.Enhance
private static class DirectInvoker implements Invoker {
/**
* {@inheritDoc}
*/
public Object newInstance(Constructor> constructor, Object[] argument) throws InstantiationException, IllegalAccessException, InvocationTargetException {
return constructor.newInstance(argument);
}
/**
* {@inheritDoc}
*/
public Object invoke(Method method, @MaybeNull Object instance, @MaybeNull Object[] argument) throws IllegalAccessException, InvocationTargetException {
return method.invoke(instance, argument);
}
}
/**
* A dispatcher for handling a proxied method.
*/
protected interface Dispatcher {
/**
* Invokes the proxied action.
*
* @param argument The arguments provided.
* @return The return value.
* @throws Throwable If any error occurs.
*/
@MaybeNull
Object invoke(Object[] argument) throws Throwable;
/**
* Implements this dispatcher in a generated proxy.
*
* @param methodVisitor The method visitor to implement the method with.
* @param method The method being implemented.
* @return The maximal size of the operand stack.
*/
int apply(MethodVisitor methodVisitor, Method method);
/**
* A dispatcher that performs an instance check.
*/
@HashCodeAndEqualsPlugin.Enhance
class ForInstanceCheck implements Dispatcher {
/**
* The checked type.
*/
private final Class> target;
/**
* Creates a dispatcher for an instance check.
*
* @param target The checked type.
*/
protected ForInstanceCheck(Class> target) {
this.target = target;
}
/**
* {@inheritDoc}
*/
public Object invoke(Object[] argument) {
return target.isInstance(argument[0]);
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
methodVisitor.visitTypeInsn(Opcodes.INSTANCEOF, Type.getInternalName(target));
methodVisitor.visitInsn(Opcodes.IRETURN);
return 1;
}
}
/**
* A dispatcher that creates an array.
*/
@HashCodeAndEqualsPlugin.Enhance
class ForContainerCreation implements Dispatcher {
/**
* The component type.
*/
private final Class> target;
/**
* Creates a dispatcher for an array creation.
*
* @param target The component type.
*/
protected ForContainerCreation(Class> target) {
this.target = target;
}
/**
* {@inheritDoc}
*/
public Object invoke(Object[] argument) {
return Array.newInstance(target, (Integer) argument[0]);
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
methodVisitor.visitVarInsn(Opcodes.ILOAD, 1);
methodVisitor.visitTypeInsn(Opcodes.ANEWARRAY, Type.getInternalName(target));
methodVisitor.visitInsn(Opcodes.ARETURN);
return 1;
}
}
/**
* A dispatcher that returns a fixed value.
*/
enum ForDefaultValue implements Dispatcher {
/**
* A dispatcher for a {@code void} type.
*/
VOID(null, Opcodes.NOP, Opcodes.RETURN, 0),
/**
* A dispatcher for a {@code boolean} type.
*/
BOOLEAN(false, Opcodes.ICONST_0, Opcodes.IRETURN, 1),
/**
* A dispatcher for a {@code boolean} type that returns {@code true}.
*/
BOOLEAN_REVERSE(true, Opcodes.ICONST_1, Opcodes.IRETURN, 1),
/**
* A dispatcher for a {@code byte} type.
*/
BYTE((byte) 0, Opcodes.ICONST_0, Opcodes.IRETURN, 1),
/**
* A dispatcher for a {@code short} type.
*/
SHORT((short) 0, Opcodes.ICONST_0, Opcodes.IRETURN, 1),
/**
* A dispatcher for a {@code char} type.
*/
CHARACTER((char) 0, Opcodes.ICONST_0, Opcodes.IRETURN, 1),
/**
* A dispatcher for an {@code int} type.
*/
INTEGER(0, Opcodes.ICONST_0, Opcodes.IRETURN, 1),
/**
* A dispatcher for a {@code long} type.
*/
LONG(0L, Opcodes.LCONST_0, Opcodes.LRETURN, 2),
/**
* A dispatcher for a {@code float} type.
*/
FLOAT(0f, Opcodes.FCONST_0, Opcodes.FRETURN, 1),
/**
* A dispatcher for a {@code double} type.
*/
DOUBLE(0d, Opcodes.DCONST_0, Opcodes.DRETURN, 2),
/**
* A dispatcher for a reference type.
*/
REFERENCE(null, Opcodes.ACONST_NULL, Opcodes.ARETURN, 1);
/**
* The default value.
*/
@MaybeNull
private final Object value;
/**
* The opcode to load the default value.
*/
private final int load;
/**
* The opcode to return the default value.
*/
private final int returned;
/**
* The operand stack size of default value.
*/
private final int size;
/**
* Creates a new default value dispatcher.
*
* @param value The default value.
* @param load The opcode to load the default value.
* @param returned The opcode to return the default value.
* @param size The operand stack size of default value.
*/
ForDefaultValue(@MaybeNull Object value, int load, int returned, int size) {
this.value = value;
this.load = load;
this.returned = returned;
this.size = size;
}
/**
* Resolves a fixed value for a given type.
*
* @param type The type to resolve.
* @return An appropriate dispatcher.
*/
protected static Dispatcher of(Class> type) {
if (type == void.class) {
return VOID;
} else if (type == boolean.class) {
return BOOLEAN;
} else if (type == byte.class) {
return BYTE;
} else if (type == short.class) {
return SHORT;
} else if (type == char.class) {
return CHARACTER;
} else if (type == int.class) {
return INTEGER;
} else if (type == long.class) {
return LONG;
} else if (type == float.class) {
return FLOAT;
} else if (type == double.class) {
return DOUBLE;
} else if (type.isArray()) {
if (type.getComponentType() == boolean.class) {
return OfPrimitiveArray.BOOLEAN;
} else if (type.getComponentType() == byte.class) {
return OfPrimitiveArray.BYTE;
} else if (type.getComponentType() == short.class) {
return OfPrimitiveArray.SHORT;
} else if (type.getComponentType() == char.class) {
return OfPrimitiveArray.CHARACTER;
} else if (type.getComponentType() == int.class) {
return OfPrimitiveArray.INTEGER;
} else if (type.getComponentType() == long.class) {
return OfPrimitiveArray.LONG;
} else if (type.getComponentType() == float.class) {
return OfPrimitiveArray.FLOAT;
} else if (type.getComponentType() == double.class) {
return OfPrimitiveArray.DOUBLE;
} else {
return OfNonPrimitiveArray.of(type.getComponentType());
}
} else {
return REFERENCE;
}
}
/**
* {@inheritDoc}
*/
@MaybeNull
public Object invoke(Object[] argument) {
return value;
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
if (load != Opcodes.NOP) {
methodVisitor.visitInsn(load);
}
methodVisitor.visitInsn(returned);
return size;
}
/**
* A dispatcher for returning a default value for a primitive array.
*/
protected enum OfPrimitiveArray implements Dispatcher {
/**
* A dispatcher for a {@code boolean} array.
*/
BOOLEAN(new boolean[0], Opcodes.T_BOOLEAN),
/**
* A dispatcher for a {@code byte} array.
*/
BYTE(new byte[0], Opcodes.T_BYTE),
/**
* A dispatcher for a {@code short} array.
*/
SHORT(new short[0], Opcodes.T_SHORT),
/**
* A dispatcher for a {@code char} array.
*/
CHARACTER(new char[0], Opcodes.T_CHAR),
/**
* A dispatcher for a {@code int} array.
*/
INTEGER(new int[0], Opcodes.T_INT),
/**
* A dispatcher for a {@code long} array.
*/
LONG(new long[0], Opcodes.T_LONG),
/**
* A dispatcher for a {@code float} array.
*/
FLOAT(new float[0], Opcodes.T_FLOAT),
/**
* A dispatcher for a {@code double} array.
*/
DOUBLE(new double[0], Opcodes.T_DOUBLE);
/**
* The default value.
*/
private final Object value;
/**
* The operand for creating an array of the represented type.
*/
private final int operand;
/**
* Creates a new dispatcher for a primitive array.
*
* @param value The default value.
* @param operand The operand for creating an array of the represented type.
*/
OfPrimitiveArray(Object value, int operand) {
this.value = value;
this.operand = operand;
}
/**
* {@inheritDoc}
*/
public Object invoke(Object[] argument) {
return value;
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
methodVisitor.visitInsn(Opcodes.ICONST_0);
methodVisitor.visitIntInsn(Opcodes.NEWARRAY, operand);
methodVisitor.visitInsn(Opcodes.ARETURN);
return 1;
}
}
/**
* A dispatcher for a non-primitive array type.
*/
@HashCodeAndEqualsPlugin.Enhance
protected static class OfNonPrimitiveArray implements Dispatcher {
/**
* The default value.
*/
@HashCodeAndEqualsPlugin.ValueHandling(HashCodeAndEqualsPlugin.ValueHandling.Sort.IGNORE)
private final Object value;
/**
* The represented component type.
*/
private final Class> componentType;
/**
* Creates a new dispatcher for the default value of a non-primitive array.
*
* @param value The default value.
* @param componentType The represented component type.
*/
protected OfNonPrimitiveArray(Object value, Class> componentType) {
this.value = value;
this.componentType = componentType;
}
/**
* Creates a new dispatcher.
*
* @param componentType The represented component type.
* @return A dispatcher for the supplied component type.
*/
protected static Dispatcher of(Class> componentType) {
return new OfNonPrimitiveArray(Array.newInstance(componentType, 0), componentType);
}
/**
* {@inheritDoc}
*/
public Object invoke(Object[] argument) {
return value;
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
methodVisitor.visitInsn(Opcodes.ICONST_0);
methodVisitor.visitTypeInsn(Opcodes.ANEWARRAY, Type.getInternalName(componentType));
methodVisitor.visitInsn(Opcodes.ARETURN);
return 1;
}
}
}
/**
* A dispatcher for invoking a constructor.
*/
@HashCodeAndEqualsPlugin.Enhance
class ForConstructor implements Dispatcher {
/**
* The proxied constructor.
*/
private final Constructor> constructor;
/**
* Creates a dispatcher for invoking a constructor.
*
* @param constructor The proxied constructor.
*/
protected ForConstructor(Constructor> constructor) {
this.constructor = constructor;
}
/**
* {@inheritDoc}
*/
public Object invoke(Object[] argument) throws Throwable {
return INVOKER.newInstance(constructor, argument);
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
Class>[] source = method.getParameterTypes(), target = constructor.getParameterTypes();
methodVisitor.visitTypeInsn(Opcodes.NEW, Type.getInternalName(constructor.getDeclaringClass()));
methodVisitor.visitInsn(Opcodes.DUP);
int offset = 1;
for (int index = 0; index < source.length; index++) {
Type type = Type.getType(source[index]);
methodVisitor.visitVarInsn(type.getOpcode(Opcodes.ILOAD), offset);
if (source[index] != target[index]) {
methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(target[index]));
}
offset += type.getSize();
}
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
Type.getInternalName(constructor.getDeclaringClass()),
MethodDescription.CONSTRUCTOR_INTERNAL_NAME,
Type.getConstructorDescriptor(constructor),
false);
methodVisitor.visitInsn(Opcodes.ARETURN);
return offset + 1;
}
}
/**
* A dispatcher for invoking a static proxied method.
*/
@HashCodeAndEqualsPlugin.Enhance
class ForStaticMethod implements Dispatcher {
/**
* The proxied method.
*/
private final Method method;
/**
* Creates a dispatcher for invoking a static method.
*
* @param method The proxied method.
*/
protected ForStaticMethod(Method method) {
this.method = method;
}
/**
* {@inheritDoc}
*/
@MaybeNull
public Object invoke(Object[] argument) throws Throwable {
return INVOKER.invoke(method, null, argument);
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
Class>[] source = method.getParameterTypes(), target = this.method.getParameterTypes();
int offset = 1;
for (int index = 0; index < source.length; index++) {
Type type = Type.getType(source[index]);
methodVisitor.visitVarInsn(type.getOpcode(Opcodes.ILOAD), offset);
if (source[index] != target[index]) {
methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(target[index]));
}
offset += type.getSize();
}
methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
Type.getInternalName(this.method.getDeclaringClass()),
this.method.getName(),
Type.getMethodDescriptor(this.method),
this.method.getDeclaringClass().isInterface());
methodVisitor.visitInsn(Type.getReturnType(this.method).getOpcode(Opcodes.IRETURN));
return Math.max(offset - 1, Type.getReturnType(this.method).getSize());
}
}
/**
* A dispatcher for invoking a non-static proxied method.
*/
@HashCodeAndEqualsPlugin.Enhance
class ForNonStaticMethod implements Dispatcher {
/**
* Indicates a call without arguments.
*/
private static final Object[] NO_ARGUMENTS = new Object[0];
/**
* The proxied method.
*/
private final Method method;
/**
* Creates a dispatcher for invoking a non-static method.
*
* @param method The proxied method.
*/
protected ForNonStaticMethod(Method method) {
this.method = method;
}
/**
* {@inheritDoc}
*/
public Object invoke(Object[] argument) throws Throwable {
Object[] reduced;
if (argument.length == 1) {
reduced = NO_ARGUMENTS;
} else {
reduced = new Object[argument.length - 1];
System.arraycopy(argument, 1, reduced, 0, reduced.length);
}
return INVOKER.invoke(method, argument[0], reduced);
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
Class>[] source = method.getParameterTypes(), target = this.method.getParameterTypes();
int offset = 1;
for (int index = 0; index < source.length; index++) {
Type type = Type.getType(source[index]);
methodVisitor.visitVarInsn(type.getOpcode(Opcodes.ILOAD), offset);
if (source[index] != (index == 0 ? this.method.getDeclaringClass() : target[index - 1])) {
methodVisitor.visitTypeInsn(Opcodes.CHECKCAST, Type.getInternalName(index == 0
? this.method.getDeclaringClass()
: target[index - 1]));
}
offset += type.getSize();
}
methodVisitor.visitMethodInsn(this.method.getDeclaringClass().isInterface() ? Opcodes.INVOKEINTERFACE : Opcodes.INVOKEVIRTUAL,
Type.getInternalName(this.method.getDeclaringClass()),
this.method.getName(),
Type.getMethodDescriptor(this.method),
this.method.getDeclaringClass().isInterface());
methodVisitor.visitInsn(Type.getReturnType(this.method).getOpcode(Opcodes.IRETURN));
return Math.max(offset - 1, Type.getReturnType(this.method).getSize());
}
}
/**
* A dispatcher for an unresolved method.
*/
@HashCodeAndEqualsPlugin.Enhance
class ForUnresolvedMethod implements Dispatcher {
/**
* The message for describing the reason why the method could not be resolved.
*/
private final String message;
/**
* Creates a dispatcher for an unresolved method.
*
* @param message The message for describing the reason why the method could not be resolved.
*/
protected ForUnresolvedMethod(String message) {
this.message = message;
}
/**
* {@inheritDoc}
*/
public Object invoke(Object[] argument) throws Throwable {
throw new IllegalStateException("Could not invoke proxy: " + message);
}
/**
* {@inheritDoc}
*/
public int apply(MethodVisitor methodVisitor, Method method) {
methodVisitor.visitTypeInsn(Opcodes.NEW, Type.getInternalName(IllegalStateException.class));
methodVisitor.visitInsn(Opcodes.DUP);
methodVisitor.visitLdcInsn(message);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
Type.getInternalName(IllegalStateException.class),
MethodDescription.CONSTRUCTOR_INTERNAL_NAME,
Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(String.class)),
false);
methodVisitor.visitInsn(Opcodes.ATHROW);
return 3;
}
}
}
/**
* An invocation handler that invokes given dispatchers.
*/
@HashCodeAndEqualsPlugin.Enhance
protected static class ProxiedInvocationHandler implements InvocationHandler {
/**
* Indicates that an invocation handler does not provide any arguments.
*/
private static final Object[] NO_ARGUMENTS = new Object[0];
/**
* The proxied type's name.
*/
private final String name;
/**
* A mapping of proxy type methods to their proxied dispatchers.
*/
private final Map targets;
/**
* Creates a new invocation handler for proxying a type.
*
* @param name The proxied type's name.
* @param targets A mapping of proxy type methods to their proxied dispatchers.
*/
protected ProxiedInvocationHandler(String name, Map targets) {
this.name = name;
this.targets = targets;
}
/**
* {@inheritDoc}
*/
@MaybeNull
public Object invoke(Object proxy, Method method, @MaybeNull Object[] argument) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
if (method.getName().equals("hashCode")) {
return hashCode();
} else if (method.getName().equals("equals")) {
return argument[0] != null
&& Proxy.isProxyClass(argument[0].getClass())
&& Proxy.getInvocationHandler(argument[0]).equals(this);
} else if (method.getName().equals("toString")) {
return "Call proxy for " + name;
} else {
throw new IllegalStateException("Unexpected object method: " + method);
}
}
Dispatcher dispatcher = targets.get(method);
try {
try {
if (dispatcher == null) {
throw new IllegalStateException("No proxy target found for " + method);
} else {
return dispatcher.invoke(argument == null
? NO_ARGUMENTS
: argument);
}
} catch (InvocationTargetException exception) {
throw exception.getTargetException();
}
} catch (RuntimeException exception) {
throw exception;
} catch (Throwable throwable) {
for (Class> type : method.getExceptionTypes()) {
if (type.isInstance(throwable)) {
throw throwable;
}
}
throw new IllegalStateException("Failed to invoke proxy for " + method, throwable);
}
}
}
/**
* A class loader for loading synthetic classes for implementing a {@link JavaDispatcher}.
*/
protected static class DynamicClassLoader extends ClassLoader {
/**
* Indicates that a constructor does not declare any parameters.
*/
private static final Class>[] NO_PARAMETER = new Class>[0];
/**
* Indicates that a constructor does not require any arguments.
*/
private static final Object[] NO_ARGUMENT = new Object[0];
/**
* Creates a new dynamic class loader.
*
* @param target The proxied type.
*/
protected DynamicClassLoader(Class> target) {
super(target.getClassLoader());
RESOLVER.accept(this, target);
}
/**
* Creates a new proxied type.
*
* @param proxy The proxy type interface.
* @param dispatchers The dispatchers to implement.
* @return An instance of the proxied type.
*/
@SuppressFBWarnings(value = {"REC_CATCH_EXCEPTION", "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED"}, justification = "Expected internal invocation.")
protected static Object proxy(Class> proxy, Map dispatchers) {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(ClassFileVersion.ofThisVm(ClassFileVersion.JAVA_V5).getMinorMajorVersion(),
Opcodes.ACC_PUBLIC,
Type.getInternalName(proxy) + "$Proxy",
null,
Type.getInternalName(Object.class),
new String[]{Type.getInternalName(proxy)});
for (Map.Entry entry : dispatchers.entrySet()) {
Class>[] exceptionType = entry.getKey().getExceptionTypes();
String[] exceptionTypeName = new String[exceptionType.length];
for (int index = 0; index < exceptionType.length; index++) {
exceptionTypeName[index] = Type.getInternalName(exceptionType[index]);
}
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC,
entry.getKey().getName(),
Type.getMethodDescriptor(entry.getKey()),
null,
exceptionTypeName);
methodVisitor.visitCode();
int offset = (entry.getKey().getModifiers() & Opcodes.ACC_STATIC) == 0 ? 1 : 0;
for (Class> type : entry.getKey().getParameterTypes()) {
offset += Type.getType(type).getSize();
}
methodVisitor.visitMaxs(entry.getValue().apply(methodVisitor, entry.getKey()), offset);
methodVisitor.visitEnd();
}
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC,
MethodDescription.CONSTRUCTOR_INTERNAL_NAME,
Type.getMethodDescriptor(Type.VOID_TYPE),
null,
null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
Type.getInternalName(Object.class),
MethodDescription.CONSTRUCTOR_INTERNAL_NAME,
Type.getMethodDescriptor(Type.VOID_TYPE),
false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
classWriter.visitEnd();
byte[] binaryRepresentation = classWriter.toByteArray();
try {
return new DynamicClassLoader(proxy)
.defineClass(proxy.getName() + "$Proxy",
binaryRepresentation,
0,
binaryRepresentation.length,
JavaDispatcher.class.getProtectionDomain())
.getConstructor(NO_PARAMETER)
.newInstance(NO_ARGUMENT);
} catch (Exception exception) {
throw new IllegalStateException("Failed to create proxy for " + proxy.getName(), exception);
}
}
/**
* Resolves a {@link Invoker} for a separate class loader.
*
* @return The created {@link Invoker}.
*/
@SuppressFBWarnings(value = {"REC_CATCH_EXCEPTION", "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED"}, justification = "Expected internal invocation.")
protected static Invoker invoker() {
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(ClassFileVersion.ofThisVm(ClassFileVersion.JAVA_V5).getMinorMajorVersion(),
Opcodes.ACC_PUBLIC,
Type.getInternalName(Invoker.class) + "$Dispatcher",
null,
Type.getInternalName(Object.class),
new String[]{Type.getInternalName(Invoker.class)});
for (Method method : Invoker.class.getMethods()) {
Class>[] exceptionType = method.getExceptionTypes();
String[] exceptionTypeName = new String[exceptionType.length];
for (int index = 0; index < exceptionType.length; index++) {
exceptionTypeName[index] = Type.getInternalName(exceptionType[index]);
}
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC,
method.getName(),
Type.getMethodDescriptor(method),
null,
exceptionTypeName);
methodVisitor.visitCode();
int offset = 1;
Type[] parameter = new Type[method.getParameterTypes().length - 1];
for (int index = 0; index < method.getParameterTypes().length; index++) {
Type type = Type.getType(method.getParameterTypes()[index]);
if (index > 0) {
parameter[index - 1] = type;
}
methodVisitor.visitVarInsn(type.getOpcode(Opcodes.ILOAD), offset);
offset += type.getSize();
}
methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
Type.getInternalName(method.getParameterTypes()[0]),
method.getName(),
Type.getMethodDescriptor(Type.getReturnType(method), parameter),
false);
methodVisitor.visitInsn(Type.getReturnType(method).getOpcode(Opcodes.IRETURN));
methodVisitor.visitMaxs(Math.max(offset - 1, Type.getReturnType(method).getSize()), offset);
methodVisitor.visitEnd();
}
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC,
MethodDescription.CONSTRUCTOR_INTERNAL_NAME,
Type.getMethodDescriptor(Type.VOID_TYPE),
null,
null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,
Type.getInternalName(Object.class),
MethodDescription.CONSTRUCTOR_INTERNAL_NAME,
Type.getMethodDescriptor(Type.VOID_TYPE),
false);
methodVisitor.visitInsn(Opcodes.RETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
classWriter.visitEnd();
byte[] binaryRepresentation = classWriter.toByteArray();
try {
return (Invoker) new DynamicClassLoader(Invoker.class)
.defineClass(Invoker.class.getName() + "$Dispatcher",
binaryRepresentation,
0,
binaryRepresentation.length,
JavaDispatcher.class.getProtectionDomain())
.getConstructor(NO_PARAMETER)
.newInstance(NO_ARGUMENT);
} catch (UnsupportedOperationException ignored) {
return new DirectInvoker();
} catch (Exception exception) {
throw new IllegalStateException("Failed to create invoker for " + Invoker.class.getName(), exception);
}
}
/**
* A resolver to make adjustments that are possibly necessary to withhold module graph guarantees.
*/
protected interface Resolver {
/**
* Adjusts a module graph if necessary.
*
* @param classLoader The class loader to adjust.
* @param target The targeted class for which a proxy is created.
*/
void accept(@MaybeNull ClassLoader classLoader, Class> target);
/**
* An action to create a resolver.
*/
enum CreationAction implements PrivilegedAction {
/**
* The singleton instance.
*/
INSTANCE;
/**
* {@inheritDoc}
*/
@SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "Exception should not be rethrown but trigger a fallback.")
public Resolver run() {
try {
Class> module = Class.forName("java.lang.Module", false, null);
return new ForModuleSystem(Class.class.getMethod("getModule"),
module.getMethod("isExported", String.class),
module.getMethod("addExports", String.class, module),
ClassLoader.class.getMethod("getUnnamedModule"));
} catch (Exception ignored) {
return NoOp.INSTANCE;
}
}
}
/**
* A non-operational resolver for VMs that do not support the module system.
*/
enum NoOp implements Resolver {
/**
* The singleton instance.
*/
INSTANCE;
/**
* {@inheritDoc}
*/
public void accept(@MaybeNull ClassLoader classLoader, Class> target) {
/* do nothing */
}
}
/**
* A resolver for VMs that do support the module system.
*/
@HashCodeAndEqualsPlugin.Enhance
class ForModuleSystem implements Resolver {
/**
* The {@code java.lang.Class#getModule} method.
*/
private final Method getModule;
/**
* The {@code java.lang.Module#isExported} method.
*/
private final Method isExported;
/**
* The {@code java.lang.Module#addExports} method.
*/
private final Method addExports;
/**
* The {@code java.lang.ClassLoader#getUnnamedModule} method.
*/
private final Method getUnnamedModule;
/**
* Creates a new resolver for a VM that supports the module system.
*
* @param getModule The {@code java.lang.Class#getModule} method.
* @param isExported The {@code java.lang.Module#isExported} method.
* @param addExports The {@code java.lang.Module#addExports} method.
* @param getUnnamedModule The {@code java.lang.ClassLoader#getUnnamedModule} method.
*/
protected ForModuleSystem(Method getModule,
Method isExported,
Method addExports,
Method getUnnamedModule) {
this.getModule = getModule;
this.isExported = isExported;
this.addExports = addExports;
this.getUnnamedModule = getUnnamedModule;
}
/**
* {@inheritDoc}
*/
@SuppressFBWarnings(value = "REC_CATCH_EXCEPTION", justification = "Exception should always be wrapped for clarity.")
public void accept(@MaybeNull ClassLoader classLoader, Class> target) {
Package location = target.getPackage();
if (location != null) {
try {
Object module = getModule.invoke(target);
if (!(Boolean) isExported.invoke(module, location.getName())) {
addExports.invoke(module, location.getName(), getUnnamedModule.invoke(classLoader));
}
} catch (Exception exception) {
throw new IllegalStateException("Failed to adjust module graph for dispatcher", exception);
}
}
}
}
}
}
}