Skip to content

Commit bfd5461

Browse files
authored
Custom lookup for reflective schemas (#5885)
* Custom lookup for reflective schemas 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. * Review comments
1 parent f461838 commit bfd5461

File tree

16 files changed

+782
-73
lines changed

16 files changed

+782
-73
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "DynamoDB Enhanced Client",
4+
"contributor": "",
5+
"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."
6+
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/TableSchema.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
package software.amazon.awssdk.enhanced.dynamodb;
1717

18+
import java.lang.invoke.MethodHandles;
1819
import java.util.Collection;
1920
import java.util.List;
2021
import java.util.Map;
@@ -23,7 +24,9 @@
2324
import software.amazon.awssdk.enhanced.dynamodb.document.DocumentTableSchema;
2425
import software.amazon.awssdk.enhanced.dynamodb.document.EnhancedDocument;
2526
import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchema;
27+
import software.amazon.awssdk.enhanced.dynamodb.mapper.BeanTableSchemaParams;
2628
import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchema;
29+
import software.amazon.awssdk.enhanced.dynamodb.mapper.ImmutableTableSchemaParams;
2730
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticImmutableTableSchema;
2831
import software.amazon.awssdk.enhanced.dynamodb.mapper.StaticTableSchema;
2932
import software.amazon.awssdk.enhanced.dynamodb.mapper.annotations.DynamoDbBean;
@@ -114,6 +117,26 @@ static <T> BeanTableSchema<T> fromBean(Class<T> beanClass) {
114117
return BeanTableSchema.create(beanClass);
115118
}
116119

120+
/**
121+
* Scans a bean class that has been annotated with DynamoDb bean annotations and then returns a
122+
* {@link BeanTableSchema} implementation of this interface that can map records to and from items of that bean
123+
* class.
124+
* <p>
125+
* It's recommended to only create a {@link BeanTableSchema} once for a single bean class, usually at application start up,
126+
* because it's a moderately expensive operation.
127+
* <p>
128+
* Generally, this method should be preferred over {@link #fromBean(Class)} because it allows you to use a custom
129+
* {@link MethodHandles.Lookup} instance, which is necessary when your application runs in an environment where your
130+
* application code and dependencies like the AWS SDK for Java are loaded by different classloaders.
131+
*
132+
* @param params The parameters used to create the {@link BeanTableSchema}.
133+
* @param <T> The type of the item this {@link TableSchema} will map records to.
134+
* @return An initialized {@link BeanTableSchema}.
135+
*/
136+
static <T> BeanTableSchema<T> fromBean(BeanTableSchemaParams<T> params) {
137+
return BeanTableSchema.create(params);
138+
}
139+
117140
/**
118141
* Provides interfaces to interact with DynamoDB tables as {@link EnhancedDocument} where the complete Schema of the table is
119142
* not required.
@@ -141,6 +164,23 @@ static <T> ImmutableTableSchema<T> fromImmutableClass(Class<T> immutableClass) {
141164
return ImmutableTableSchema.create(immutableClass);
142165
}
143166

167+
/**
168+
* Scans an immutable class that has been annotated with DynamoDb immutable annotations and then returns a
169+
* {@link ImmutableTableSchema} implementation of this interface that can map records to and from items of that
170+
* immutable class.
171+
*
172+
* <p>
173+
* It's recommended to only create an {@link ImmutableTableSchema} once for a single immutable class, usually at application
174+
* start up, because it's a moderately expensive operation.
175+
*
176+
* @param params The parameters used to create the {@link ImmutableTableSchema}.
177+
* @param <T> The type of the item this {@link TableSchema} will map records to.
178+
* @return An initialized {@link ImmutableTableSchema}.
179+
*/
180+
static <T> ImmutableTableSchema<T> fromImmutableClass(ImmutableTableSchemaParams<T> params) {
181+
return ImmutableTableSchema.create(params);
182+
}
183+
144184
/**
145185
* Scans a class that has been annotated with DynamoDb enhanced client annotations and then returns an appropriate
146186
* {@link TableSchema} implementation that can map records to and from items of that class. Currently supported

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeGetter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

18+
import java.lang.invoke.MethodHandles;
1819
import java.lang.reflect.Method;
1920
import java.util.function.Function;
2021
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -24,12 +25,14 @@
2425
@SdkInternalApi
2526
@SuppressWarnings("unchecked")
2627
public interface BeanAttributeGetter<BeanT, GetterT> extends Function<BeanT, GetterT> {
27-
static <BeanT, GetterT> BeanAttributeGetter<BeanT, GetterT> create(Class<BeanT> beanClass, Method getter) {
28+
static <BeanT, GetterT> BeanAttributeGetter<BeanT, GetterT> create(Class<BeanT> beanClass, Method getter,
29+
MethodHandles.Lookup lookup) {
2830
Validate.isTrue(getter.getParameterCount() == 0,
2931
"%s.%s has parameters, despite being named like a getter.",
3032
beanClass, getter.getName());
3133

3234
return LambdaToMethodBridgeBuilder.create(BeanAttributeGetter.class)
35+
.lookup(lookup)
3336
.lambdaMethodName("apply")
3437
.runtimeLambdaSignature(Object.class, Object.class)
3538
.compileTimeLambdaSignature(getter.getReturnType(), beanClass)

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/BeanAttributeSetter.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

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

3639
return LambdaToMethodBridgeBuilder.create(BeanAttributeSetter.class)
40+
.lookup(lookup)
3741
.lambdaMethodName("accept")
3842
.runtimeLambdaSignature(void.class, Object.class, Object.class)
3943
.compileTimeLambdaSignature(void.class, beanClass, boxedInputClass)

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/LambdaToMethodBridgeBuilder.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828

2929
@SdkInternalApi
3030
public class LambdaToMethodBridgeBuilder<T> {
31-
private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
31+
private static final MethodHandles.Lookup SELF_LOOKUP = MethodHandles.lookup();
3232

3333
private final Class<T> lambdaType;
34+
private MethodHandles.Lookup lookup;
3435
private String lambdaMethodName;
3536
private Class<?> postEraseLambdaReturnType;
3637
private Class<?>[] postEraseLambdaParameters;
@@ -73,14 +74,20 @@ public LambdaToMethodBridgeBuilder<T> targetMethod(Constructor<?> method) {
7374
return this;
7475
}
7576

77+
public LambdaToMethodBridgeBuilder<T> lookup(MethodHandles.Lookup lookup) {
78+
this.lookup = lookup;
79+
return this;
80+
}
81+
7682
public T build() {
83+
MethodHandles.Lookup metaFactoryLookup = resolveLookup();
7784
try {
7885
MethodHandle targetMethodHandle = targetMethod.map(
79-
m -> invokeSafely(() -> LOOKUP.unreflect(m)),
80-
c -> invokeSafely(() -> LOOKUP.unreflectConstructor(c)));
86+
m -> invokeSafely(() -> metaFactoryLookup.unreflect(m)),
87+
c -> invokeSafely(() -> metaFactoryLookup.unreflectConstructor(c)));
8188

8289
return lambdaType.cast(
83-
LambdaMetafactory.metafactory(LOOKUP,
90+
LambdaMetafactory.metafactory(metaFactoryLookup,
8491
lambdaMethodName,
8592
MethodType.methodType(lambdaType),
8693
MethodType.methodType(postEraseLambdaReturnType, postEraseLambdaParameters),
@@ -92,4 +99,12 @@ public T build() {
9299
throw new IllegalArgumentException("Failed to generate method handle.", e);
93100
}
94101
}
102+
103+
private MethodHandles.Lookup resolveLookup() {
104+
if (lookup != null) {
105+
return lookup;
106+
}
107+
108+
return SELF_LOOKUP;
109+
}
95110
}

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectConstructor.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

18+
import java.lang.invoke.MethodHandles;
1819
import java.lang.reflect.Constructor;
1920
import java.util.function.Supplier;
2021
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -24,12 +25,15 @@
2425
@SdkInternalApi
2526
@SuppressWarnings("unchecked")
2627
public interface ObjectConstructor<BeanT> extends Supplier<BeanT> {
27-
static <BeanT> ObjectConstructor<BeanT> create(Class<BeanT> beanClass, Constructor<BeanT> noArgsConstructor) {
28+
static <BeanT> ObjectConstructor<BeanT> create(Class<BeanT> beanClass, Constructor<BeanT> noArgsConstructor,
29+
MethodHandles.Lookup lookup) {
30+
2831
Validate.isTrue(noArgsConstructor.getParameterCount() == 0,
2932
"%s has no default constructor.",
3033
beanClass);
3134

3235
return LambdaToMethodBridgeBuilder.create(ObjectConstructor.class)
36+
.lookup(lookup)
3337
.lambdaMethodName("get")
3438
.runtimeLambdaSignature(Object.class)
3539
.compileTimeLambdaSignature(beanClass)

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/ObjectGetterMethod.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

18+
import java.lang.invoke.MethodHandles;
1819
import java.lang.reflect.Method;
1920
import java.util.function.Function;
2021
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -23,8 +24,10 @@
2324
@SdkInternalApi
2425
@SuppressWarnings("unchecked")
2526
public interface ObjectGetterMethod<BeanT, GetterT> extends Function<BeanT, GetterT> {
26-
static <BeanT, GetterT> ObjectGetterMethod<BeanT, GetterT> create(Class<BeanT> beanClass, Method buildMethod) {
27+
static <BeanT, GetterT> ObjectGetterMethod<BeanT, GetterT> create(Class<BeanT> beanClass, Method buildMethod,
28+
MethodHandles.Lookup lookup) {
2729
return LambdaToMethodBridgeBuilder.create(ObjectGetterMethod.class)
30+
.lookup(lookup)
2831
.lambdaMethodName("apply")
2932
.runtimeLambdaSignature(Object.class, Object.class)
3033
.compileTimeLambdaSignature(buildMethod.getReturnType(), beanClass)

services-custom/dynamodb-enhanced/src/main/java/software/amazon/awssdk/enhanced/dynamodb/internal/mapper/StaticGetterMethod.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

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

18+
import java.lang.invoke.MethodHandles;
1819
import java.lang.reflect.Method;
1920
import java.util.function.Supplier;
2021
import software.amazon.awssdk.annotations.SdkInternalApi;
@@ -23,8 +24,10 @@
2324
@SdkInternalApi
2425
@SuppressWarnings("unchecked")
2526
public interface StaticGetterMethod<GetterT> extends Supplier<GetterT> {
26-
static <GetterT> StaticGetterMethod<GetterT> create(Method buildMethod) {
27+
static <GetterT> StaticGetterMethod<GetterT> create(Method buildMethod,
28+
MethodHandles.Lookup lookup) {
2729
return LambdaToMethodBridgeBuilder.create(StaticGetterMethod.class)
30+
.lookup(lookup)
2831
.lambdaMethodName("get")
2932
.runtimeLambdaSignature(Object.class)
3033
.compileTimeLambdaSignature(buildMethod.getReturnType())

0 commit comments

Comments
 (0)