|
| 1 | +package dev.slne.surf.redis.invoker; |
| 2 | + |
| 3 | +import org.jspecify.annotations.NullMarked; |
| 4 | + |
| 5 | +import java.lang.constant.ConstantDescs; |
| 6 | +import java.lang.invoke.MethodHandle; |
| 7 | +import java.lang.invoke.MethodHandles; |
| 8 | +import java.lang.invoke.MethodType; |
| 9 | +import java.lang.reflect.Method; |
| 10 | +import java.util.List; |
| 11 | + |
| 12 | +/** |
| 13 | + * Shared utility for creating and initializing hidden class–based invokers. |
| 14 | + * |
| 15 | + * <p>This class encapsulates the low-level JVM hidden class machinery used by |
| 16 | + * {@link RedisEventInvokerFactory} and {@link RedisRequestHandlerInvokerFactory}. |
| 17 | + * |
| 18 | + * <h2>Hidden class lifecycle</h2> |
| 19 | + * <ol> |
| 20 | + * <li>{@link #createInvoker} packs the target instance, payload class, method, and a private |
| 21 | + * lookup into a {@link List} and passes it as class data to |
| 22 | + * {@link MethodHandles.Lookup#defineHiddenClassWithClassData(byte[], Object, boolean, MethodHandles.Lookup.ClassOption...)}.</li> |
| 23 | + * <li>The hidden class's static initializer calls back to the factory's {@code classData()} method, |
| 24 | + * which delegates to {@link #loadClassData} to unpack the class data.</li> |
| 25 | + * <li>{@link #loadClassData} extracts the individual components via |
| 26 | + * {@link MethodHandles#classDataAt}, resolves the method into a bound {@link MethodHandle}, |
| 27 | + * and returns them as a {@link ClassData} record.</li> |
| 28 | + * </ol> |
| 29 | + * |
| 30 | + * <p>This class is package-private and not intended for external use. |
| 31 | + */ |
| 32 | +@NullMarked |
| 33 | +final class RedisHiddenInvokerUtil { |
| 34 | + |
| 35 | + private RedisHiddenInvokerUtil() { |
| 36 | + throw new UnsupportedOperationException(); |
| 37 | + } |
| 38 | + |
| 39 | + /** |
| 40 | + * Defines a new hidden class from the given template bytecode and returns a new instance |
| 41 | + * of the specified invoker interface. |
| 42 | + * |
| 43 | + * <p>The class data passed to the hidden class consists of: |
| 44 | + * <ol> |
| 45 | + * <li>The target handler/listener instance</li> |
| 46 | + * <li>The payload class (event or request type)</li> |
| 47 | + * <li>The handler {@link Method}</li> |
| 48 | + * <li>A {@link MethodHandles.Lookup} with private access to the target's class</li> |
| 49 | + * </ol> |
| 50 | + * |
| 51 | + * @param <I> the invoker interface type (e.g. {@code RedisEventInvoker}) |
| 52 | + * @param lookup the lookup used to define the hidden class |
| 53 | + * @param templateBytes the bytecode of the template class |
| 54 | + * @param invokerInterface the interface the hidden class implements |
| 55 | + * @param target the handler/listener instance to bind the MethodHandle to |
| 56 | + * @param method the handler method to invoke |
| 57 | + * @param payloadClass the concrete event or request class the handler accepts |
| 58 | + * @return a new instance of the hidden class, cast to {@code I} |
| 59 | + * @throws ReflectiveOperationException if hidden class definition or instantiation fails |
| 60 | + */ |
| 61 | + static <I> I createInvoker(MethodHandles.Lookup lookup, byte[] templateBytes, Class<I> invokerInterface, Object target, Method method, Class<?> payloadClass) throws ReflectiveOperationException { |
| 62 | + final MethodHandles.Lookup privateLookupIn = MethodHandles.privateLookupIn(target.getClass(), lookup); |
| 63 | + final List<Object> classData = List.of(target, payloadClass, method, privateLookupIn); |
| 64 | + final MethodHandles.Lookup hiddenClassLookup = lookup.defineHiddenClassWithClassData(templateBytes, classData, true); |
| 65 | + |
| 66 | + return hiddenClassLookup.lookupClass() |
| 67 | + .asSubclass(invokerInterface) |
| 68 | + .getDeclaredConstructor() |
| 69 | + .newInstance(); |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * Immutable carrier for the resolved class data of a hidden invoker class. |
| 74 | + * |
| 75 | + * @param <P> the payload supertype ({@link dev.slne.surf.redis.event.RedisEvent} |
| 76 | + * or {@link dev.slne.surf.redis.request.RedisRequest}) |
| 77 | + * @param method the original handler method |
| 78 | + * @param methodHandle the resolved and bound {@link MethodHandle} for the handler |
| 79 | + * @param payloadClass the concrete payload class the handler accepts |
| 80 | + */ |
| 81 | + record ClassData<P>(Method method, MethodHandle methodHandle, Class<? extends P> payloadClass) { |
| 82 | + } |
| 83 | + |
| 84 | + /** |
| 85 | + * Extracts and resolves the class data that was passed to |
| 86 | + * {@link MethodHandles.Lookup#defineHiddenClassWithClassData}. |
| 87 | + * |
| 88 | + * <p>This method is called from within the static initializer of the hidden class to |
| 89 | + * unpack the target, payload class, method, and private lookup. The method is then |
| 90 | + * unreflected and bound to the target, producing a type-erased {@link MethodHandle} |
| 91 | + * matching the given {@code methodType}. |
| 92 | + * |
| 93 | + * @param <P> the payload supertype |
| 94 | + * @param lookup the hidden class's own lookup (provides access to its class data) |
| 95 | + * @param methodType the expected method type for the resolved MethodHandle |
| 96 | + * @param payloadSuperType the expected supertype of the payload class |
| 97 | + * @return a {@link ClassData} record containing the method, bound handle, and payload class |
| 98 | + * @throws ReflectiveOperationException if class data extraction or handle resolution fails |
| 99 | + */ |
| 100 | + static <P> ClassData<P> loadClassData(MethodHandles.Lookup lookup, MethodType methodType, Class<P> payloadSuperType) throws ReflectiveOperationException { |
| 101 | + final Object target = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Object.class, 0); |
| 102 | + final Class<?> payload = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Class.class, 1); |
| 103 | + final Method method = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Method.class, 2); |
| 104 | + final MethodHandles.Lookup privateLookupIn = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, MethodHandles.Lookup.class, 3); |
| 105 | + |
| 106 | + final MethodHandle handle = privateLookupIn.unreflect(method).bindTo(target).asType(methodType); |
| 107 | + return new ClassData<P>(method, handle, payload.asSubclass(payloadSuperType)); |
| 108 | + } |
| 109 | +} |
0 commit comments