Skip to content

Commit

Permalink
Custom lookup for reflective schemas
Browse files Browse the repository at this point in the history
This commit updates `BeanTableSchema` and `ImmutableTableSchema` to allow
customers to provide their own instance of `MethodHandle.Lookup`.

This is a necessary feature for situations where either of these schemas
are used in an application where the item classes and the SDK classes
are loaded by different classloaders. Since these schemas work by
reflectively scanning the item class to find the attributes' getters and
setters (or builder for immutable classes), the enhanced client makes
use of a LambdaMetaFactory to construct lambdas that directly call these
methods to avoid reflection when mapping to and from a
`AttributeValue`s. At least as of Java 21, the way that lambdas are
constructed (all lambdas, not just the ones created by the enhanced
client), is they are made internal classes of the "caller" class, i.e.
the class that defined it. The calling class is identified by the class
that created the `Lookup` object. This means that when using the default
`Lookup` created `LambdaToMethodBridgeBuilder`, all of these lambdas are
inner classes of of `LambdaToMethodBridgeBuilder`. This is an issue when
the item class is from a different classloader (or module) because when
the lambda attempts to reference the item class (e.g. to call a getter),
it's `ClassLoader` will throw a `ClassNotFoundException` because because
it's not a class that it knows about.

By allowing the customer to provide a `Lookup` object, they can
guarantee that the the `Lookup` has visibility and access to the item
classes.
  • Loading branch information
dagnir committed Feb 18, 2025
1 parent 15fe972 commit bcccbc6
Show file tree
Hide file tree
Showing 15 changed files with 725 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "DynamoDB Enhanced Client",
"contributor": "",
"description": "Add ability to provide a custom `MethodHandles.Lookup` object when using either `BeanTableSchema` or `ImmutableTableSchema`. By setting a custom `MethodHandles.Lookup` it allows these schemas to be used in applications where the item class and the SDK are loaded by different `ClassLoader`s."
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.enhanced.dynamodb.internal.mapper;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.function.Function;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -24,12 +25,14 @@
@SdkInternalApi
@SuppressWarnings("unchecked")
public interface BeanAttributeGetter<BeanT, GetterT> extends Function<BeanT, GetterT> {
static <BeanT, GetterT> BeanAttributeGetter<BeanT, GetterT> create(Class<BeanT> beanClass, Method getter) {
static <BeanT, GetterT> BeanAttributeGetter<BeanT, GetterT> create(Class<BeanT> beanClass, Method getter,
MethodHandles.Lookup lookup) {
Validate.isTrue(getter.getParameterCount() == 0,
"%s.%s has parameters, despite being named like a getter.",
beanClass, getter.getName());

return LambdaToMethodBridgeBuilder.create(BeanAttributeGetter.class)
.lookup(lookup)
.lambdaMethodName("apply")
.runtimeLambdaSignature(Object.class, Object.class)
.compileTimeLambdaSignature(getter.getReturnType(), beanClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.enhanced.dynamodb.internal.mapper;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.function.BiConsumer;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -25,7 +26,9 @@
@SdkInternalApi
public interface BeanAttributeSetter<BeanT, GetterT> extends BiConsumer<BeanT, GetterT> {
@SuppressWarnings("unchecked")
static <BeanT, SetterT> BeanAttributeSetter<BeanT, SetterT> create(Class<BeanT> beanClass, Method setter) {
static <BeanT, SetterT> BeanAttributeSetter<BeanT, SetterT> create(Class<BeanT> beanClass,
Method setter,
MethodHandles.Lookup lookup) {
Validate.isTrue(setter.getParameterCount() == 1,
"%s.%s doesn't have just 1 parameter, despite being named like a setter.",
beanClass, setter.getName());
Expand All @@ -34,6 +37,7 @@ static <BeanT, SetterT> BeanAttributeSetter<BeanT, SetterT> create(Class<BeanT>
Class<?> boxedInputClass = ReflectionUtils.getWrappedClass(setterInputClass);

return LambdaToMethodBridgeBuilder.create(BeanAttributeSetter.class)
.lookup(lookup)
.lambdaMethodName("accept")
.runtimeLambdaSignature(void.class, Object.class, Object.class)
.compileTimeLambdaSignature(void.class, beanClass, boxedInputClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@

@SdkInternalApi
public class LambdaToMethodBridgeBuilder<T> {
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
private static final MethodHandles.Lookup SELF_LOOKUP = MethodHandles.lookup();

private final Class<T> lambdaType;
private MethodHandles.Lookup lookup;
private String lambdaMethodName;
private Class<?> postEraseLambdaReturnType;
private Class<?>[] postEraseLambdaParameters;
Expand Down Expand Up @@ -73,14 +74,20 @@ public LambdaToMethodBridgeBuilder<T> targetMethod(Constructor<?> method) {
return this;
}

public LambdaToMethodBridgeBuilder<T> lookup(MethodHandles.Lookup lookup) {
this.lookup = lookup;
return this;
}

public T build() {
MethodHandles.Lookup metaFactoryLookup = resolveLookup();
try {
MethodHandle targetMethodHandle = targetMethod.map(
m -> invokeSafely(() -> LOOKUP.unreflect(m)),
c -> invokeSafely(() -> LOOKUP.unreflectConstructor(c)));
m -> invokeSafely(() -> metaFactoryLookup.unreflect(m)),
c -> invokeSafely(() -> metaFactoryLookup.unreflectConstructor(c)));

return lambdaType.cast(
LambdaMetafactory.metafactory(LOOKUP,
LambdaMetafactory.metafactory(metaFactoryLookup,
lambdaMethodName,
MethodType.methodType(lambdaType),
MethodType.methodType(postEraseLambdaReturnType, postEraseLambdaParameters),
Expand All @@ -92,4 +99,12 @@ public T build() {
throw new IllegalArgumentException("Failed to generate method handle.", e);
}
}

private MethodHandles.Lookup resolveLookup() {
if (lookup != null) {
return lookup;
}

return SELF_LOOKUP;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.enhanced.dynamodb.internal.mapper;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -24,12 +25,15 @@
@SdkInternalApi
@SuppressWarnings("unchecked")
public interface ObjectConstructor<BeanT> extends Supplier<BeanT> {
static <BeanT> ObjectConstructor<BeanT> create(Class<BeanT> beanClass, Constructor<BeanT> noArgsConstructor) {
static <BeanT> ObjectConstructor<BeanT> create(Class<BeanT> beanClass, Constructor<BeanT> noArgsConstructor,
MethodHandles.Lookup lookup) {

Validate.isTrue(noArgsConstructor.getParameterCount() == 0,
"%s has no default constructor.",
beanClass);

return LambdaToMethodBridgeBuilder.create(ObjectConstructor.class)
.lookup(lookup)
.lambdaMethodName("get")
.runtimeLambdaSignature(Object.class)
.compileTimeLambdaSignature(beanClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.enhanced.dynamodb.internal.mapper;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.function.Function;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -23,8 +24,10 @@
@SdkInternalApi
@SuppressWarnings("unchecked")
public interface ObjectGetterMethod<BeanT, GetterT> extends Function<BeanT, GetterT> {
static <BeanT, GetterT> ObjectGetterMethod<BeanT, GetterT> create(Class<BeanT> beanClass, Method buildMethod) {
static <BeanT, GetterT> ObjectGetterMethod<BeanT, GetterT> create(Class<BeanT> beanClass, Method buildMethod,
MethodHandles.Lookup lookup) {
return LambdaToMethodBridgeBuilder.create(ObjectGetterMethod.class)
.lookup(lookup)
.lambdaMethodName("apply")
.runtimeLambdaSignature(Object.class, Object.class)
.compileTimeLambdaSignature(buildMethod.getReturnType(), beanClass)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.awssdk.enhanced.dynamodb.internal.mapper;

import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;
import java.util.function.Supplier;
import software.amazon.awssdk.annotations.SdkInternalApi;
Expand All @@ -23,8 +24,10 @@
@SdkInternalApi
@SuppressWarnings("unchecked")
public interface StaticGetterMethod<GetterT> extends Supplier<GetterT> {
static <GetterT> StaticGetterMethod<GetterT> create(Method buildMethod) {
static <GetterT> StaticGetterMethod<GetterT> create(Method buildMethod,
MethodHandles.Lookup lookup) {
return LambdaToMethodBridgeBuilder.create(StaticGetterMethod.class)
.lookup(lookup)
.lambdaMethodName("get")
.runtimeLambdaSignature(Object.class)
.compileTimeLambdaSignature(buildMethod.getReturnType())
Expand Down
Loading

0 comments on commit bcccbc6

Please sign in to comment.