Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TypeReference.createInstance(Class<T>) #13600

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
import com.azure.core.util.logging.ClientLogger;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* This class represents a generic Java type, retaining information about generics.
Expand All @@ -16,6 +19,8 @@ public abstract class TypeReference<T> {
private static final ClientLogger LOGGER = new ClientLogger(TypeReference.class);
private static final String MISSING_TYPE = "Type constructed without type information.";

private static final Map<Class<?>, TypeReference<?>> CACHE = new ConcurrentHashMap<>();
alzimmermsft marked this conversation as resolved.
Show resolved Hide resolved

private final java.lang.reflect.Type javaType;

/**
Expand All @@ -24,19 +29,44 @@ public abstract class TypeReference<T> {
* @throws IllegalArgumentException If the reference is constructed without type information.
*/
public TypeReference() {
java.lang.reflect.Type superClass = this.getClass().getGenericSuperclass();
Type superClass = this.getClass().getGenericSuperclass();
if (superClass instanceof Class) {
throw LOGGER.logExceptionAsError(new IllegalArgumentException(MISSING_TYPE));
} else {
this.javaType = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
}

private TypeReference(Class<T> clazz) {
this.javaType = clazz;
}

/**
* Return class T type
* @return type
* Returns the {@link Type} representing {@code T}.
*
* @return The {@link Type} representing {@code T}.
*/
public java.lang.reflect.Type getJavaType() {
public Type getJavaType() {
return javaType;
}

/**
* Creates and instance of {@link TypeReference} which maintains the generic {@code T} of the passed {@link Class}.
* <p>
* This method will cache the instance of {@link TypeReference} using the passed {@link Class} as the key. This is
* meant to be used with non-generic types such as primitive object types and POJOs, not {@code Map<String, Object>}
* or {@code List<Integer>} parameterized types.
*
* @param clazz {@link Class} that contains generic information used to create the {@link TypeReference}.
* @param <T> The generic type.
* @return Either the cached or new instance of {@link TypeReference}.
*/
@SuppressWarnings("unchecked")
public static <T> TypeReference<T> createInstance(Class<T> clazz) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks a little weird by having both createInstance and public constructor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They serve different purposes. The public constructor is for anonymous instances that use parameterized types for their T, such as Map<String, String>. This factory method is meant for non-parameterized Class types that can be safely cached and maintain T.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is a bit odd. They serve different purposes but the constructor works for all cases and if the caller always uses the ctor not knowing about the static createInstance method, it may defeat the purpose of caching.
Also, in azure-core, there is a TypeUtil that has a utility method to createParameterizedType() that may be useful.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The public constructor work with most of the cases like (concrete class, paramterized type), but not the clazz variable cases.
E.g.

public void someMethod(Class<T> clazz) {
    TypeReference<T> type = new TypeReference<T>() {} // type is null in this case, no matter what clazz you passed in.
    ...
 }

In this case, we must use the createInstance API. However, createInstance API does not serve all purpose as well, we cannot pass the parameterized type here.

So both APIs have hard limitations and caching strategy are different behind the API. If I am not expert users I would confuse of the usage here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed @sima-zhu, having both does lead to some confusion. Luckily, this type is mainly going to be used internally where determining whether to use createInstance(Class<T>) or the constructor will be based on the API. If the type is known ahead of time and it is parameterized the only choice is to use the constructor, if the type is known but is a Class<T> or is passed into the API createInstance(Class<T>) will be used.

/*
* When computing the TypeReference if the key is absent ignore the parameter from the compute function. The
* compute function wildcards to T type which causes the type system to breakdown.
*/
return (TypeReference<T>) CACHE.computeIfAbsent(clazz, c -> new TypeReference<T>(clazz) { });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,47 +63,48 @@ public <T> void deserializePrimitiveTypes(InputStream avro, String schema, TypeR

private static Stream<Arguments> deserializePrimitiveTypesSupplier() {
return Stream.of(
Arguments.of(streamCreator(0), schemaCreator("boolean"), new TypeReference<Boolean>() { }, false),
Arguments.of(streamCreator(1), schemaCreator("boolean"), new TypeReference<Boolean>() { }, true),
Arguments.of(streamCreator(0), schemaCreator("boolean"), createInstance(Boolean.class), false),
Arguments.of(streamCreator(1), schemaCreator("boolean"), createInstance(Boolean.class), true),

// INT and LONG use zigzag encoding.
Arguments.of(streamCreator(42), schemaCreator("int"), new TypeReference<Integer>() { }, 21),
Arguments.of(streamCreator(42), schemaCreator("long"), new TypeReference<Long>() { }, 21L),
Arguments.of(streamCreator(42), schemaCreator("int"), createInstance(int.class), 21),
Arguments.of(streamCreator(42), schemaCreator("long"), createInstance(int.class), 21L),

// FLOAT and DOUBLE use little endian.
Arguments.of(streamCreator(0x00, 0x00, 0x28, 0x42), schemaCreator("float"), new TypeReference<Float>() { }, 42F),
Arguments.of(streamCreator(0x00, 0x00, 0x28, 0x42), schemaCreator("float"), createInstance(float.class), 42F),
Arguments.of(streamCreator(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x40), schemaCreator("double"),
new TypeReference<Double>() { }, 42D),
createInstance(double.class), 42D),

// STRING has an additional property 'avro.java.string' which indicates the deserialization type.
// Using Java's String class.
Arguments.of(streamCreator(0), SPECIFIED_STRING_SCHEMA, new TypeReference<String>() { }, ""),
Arguments.of(streamCreator(0x06, 0x66, 0x6F, 0x6F), SPECIFIED_STRING_SCHEMA, new TypeReference<String>() { }, "foo"),
Arguments.of(streamCreator(0), SPECIFIED_STRING_SCHEMA, createInstance(String.class), ""),
Arguments.of(streamCreator(0x06, 0x66, 0x6F, 0x6F), SPECIFIED_STRING_SCHEMA, createInstance(String.class), "foo"),

// Using Java's CharSequence class that gets wrapped in Apache's Utf8.
Arguments.of(streamCreator(0), SPECIFIED_CHAR_SEQUENCE_SCHEMA, new TypeReference<Utf8>() { }, new Utf8("")),
Arguments.of(streamCreator(0x06, 0x66, 0x6F, 0x6F), SPECIFIED_CHAR_SEQUENCE_SCHEMA, new TypeReference<Utf8>() { },
Arguments.of(streamCreator(0), SPECIFIED_CHAR_SEQUENCE_SCHEMA, createInstance(Utf8.class), new Utf8("")),
Arguments.of(streamCreator(0x06, 0x66, 0x6F, 0x6F), SPECIFIED_CHAR_SEQUENCE_SCHEMA, createInstance(Utf8.class),
new Utf8("foo")),

// BYTES deserializes into ByteBuffers.
Arguments.of(streamCreator(0), schemaCreator("bytes"), new TypeReference<ByteBuffer>() { },
Arguments.of(streamCreator(0), schemaCreator("bytes"), createInstance(ByteBuffer.class),
ByteBuffer.wrap(new byte[0])),
Arguments.of(streamCreator(4, 42, 42), schemaCreator("bytes"), new TypeReference<ByteBuffer>() { },
Arguments.of(streamCreator(4, 42, 42), schemaCreator("bytes"), createInstance(ByteBuffer.class),
ByteBuffer.wrap(new byte[] {42, 42 }))
);
}

@Test
public void deserializeNull() {
StepVerifier.create(getSerializer(schemaCreator("null")).deserializeAsync(new ByteArrayInputStream(new byte[0]),
new TypeReference<Void>() { })).verifyComplete();
StepVerifier.create(getSerializer(schemaCreator("null"))
.deserializeAsync(new ByteArrayInputStream(new byte[0]), createInstance(void.class)))
.verifyComplete();
}

@ParameterizedTest
@MethodSource("deserializeEnumSupplier")
public void deserializeEnum(InputStream avro, PlayingCardSuit expected) {
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString()).deserializeAsync(avro,
new TypeReference<PlayingCardSuit>() { }))
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString())
.deserializeAsync(avro, createInstance(PlayingCardSuit.class)))
.assertNext(actual -> assertEquals(expected, actual))
.verifyComplete();
}
Expand All @@ -119,8 +120,8 @@ private static Stream<Arguments> deserializeEnumSupplier() {

@Test
public void deserializeInvalidEnum() {
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString()).deserializeAsync(streamCreator(8),
new TypeReference<PlayingCardSuit>() { }))
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString())
.deserializeAsync(streamCreator(8), createInstance(PlayingCardSuit.class)))
.verifyError();
}

Expand Down Expand Up @@ -205,20 +206,21 @@ private static Stream<Arguments> deserializeRecordSupplier() {
LongLinkedList expectedTwoNodeLinkedList = new LongLinkedList(0L, new LongLinkedList(1L, null));

return Stream.of(
Arguments.of(streamCreator(0), handOfCardsSchema, new TypeReference<HandOfCards>() { },
Arguments.of(streamCreator(0), handOfCardsSchema, createInstance(HandOfCards.class),
new HandOfCards(Collections.emptyList())),
Arguments.of(pairOfAcesHand, handOfCardsSchema, new TypeReference<HandOfCards>() { }, expectedPairOfAces),
Arguments.of(royalFlushHand, handOfCardsSchema, new TypeReference<HandOfCards>() { }, expectedRoyalFlushHand),
Arguments.of(streamCreator(0, 0), longLinkedListSchema, new TypeReference<LongLinkedList>() { },
Arguments.of(pairOfAcesHand, handOfCardsSchema, createInstance(HandOfCards.class), expectedPairOfAces),
Arguments.of(royalFlushHand, handOfCardsSchema, createInstance(HandOfCards.class), expectedRoyalFlushHand),
Arguments.of(streamCreator(0, 0), longLinkedListSchema, createInstance(LongLinkedList.class),
new LongLinkedList(0L, null)),
Arguments.of(twoNodeLinkedList, longLinkedListSchema, new TypeReference<LongLinkedList>() { },
Arguments.of(twoNodeLinkedList, longLinkedListSchema, createInstance(LongLinkedList.class),
expectedTwoNodeLinkedList)
);
}

@Test
public void deserializeNullReturnsNull() {
StepVerifier.create(getSerializer(schemaCreator("null")).deserializeAsync(null, new TypeReference<Void>() { }))
StepVerifier.create(getSerializer(schemaCreator("null"))
.deserializeAsync(null, createInstance(void.class)))
.verifyComplete();
}

Expand Down Expand Up @@ -389,4 +391,8 @@ private static InputStream streamCreator(int... bytes) {

return new ByteArrayInputStream(converted);
}

private static <T> TypeReference<T> createInstance(Class<T> clazz) {
return TypeReference.createInstance(clazz);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,47 +62,48 @@ public <T> void deserializePrimitiveTypes(InputStream avro, String schema, TypeR

private static Stream<Arguments> deserializePrimitiveTypesSupplier() {
return Stream.of(
Arguments.of(streamCreator(0), schemaCreator("boolean"), new TypeReference<Boolean>() { }, false),
Arguments.of(streamCreator(1), schemaCreator("boolean"), new TypeReference<Boolean>() { }, true),
Arguments.of(streamCreator(0), schemaCreator("boolean"), createInstance(Boolean.class), false),
Arguments.of(streamCreator(1), schemaCreator("boolean"), createInstance(Boolean.class), true),

// INT and LONG use zigzag encoding.
Arguments.of(streamCreator(42), schemaCreator("int"), new TypeReference<Integer>() { }, 21),
Arguments.of(streamCreator(42), schemaCreator("long"), new TypeReference<Long>() { }, 21L),
Arguments.of(streamCreator(42), schemaCreator("int"), createInstance(Integer.class), 21),
Arguments.of(streamCreator(42), schemaCreator("long"), createInstance(Long.class), 21L),

// FLOAT and DOUBLE use little endian.
Arguments.of(streamCreator(0x00, 0x00, 0x28, 0x42), schemaCreator("float"), new TypeReference<Float>() { }, 42F),
Arguments.of(streamCreator(0x00, 0x00, 0x28, 0x42), schemaCreator("float"), createInstance(Float.class), 42F),
Arguments.of(streamCreator(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x40), schemaCreator("double"),
new TypeReference<Double>() { }, 42D),
createInstance(Double.class), 42D),

// STRING has an additional property 'avro.java.string' which indicates the deserialization type.
// Using Java's String class.
Arguments.of(streamCreator(0), SPECIFIED_STRING_SCHEMA, new TypeReference<String>() { }, ""),
Arguments.of(streamCreator(0x06, 0x66, 0x6F, 0x6F), SPECIFIED_STRING_SCHEMA, new TypeReference<String>() { }, "foo"),
Arguments.of(streamCreator(0), SPECIFIED_STRING_SCHEMA, createInstance(String.class), ""),
Arguments.of(streamCreator(0x06, 0x66, 0x6F, 0x6F), SPECIFIED_STRING_SCHEMA, createInstance(String.class), "foo"),

// Using Jackson doesn't use CharSequence, so it won't use Utf8.
Arguments.of(streamCreator(0), SPECIFIED_CHAR_SEQUENCE_SCHEMA, new TypeReference<String>() { }, ""),
Arguments.of(streamCreator(0), SPECIFIED_CHAR_SEQUENCE_SCHEMA, createInstance(String.class), ""),
Arguments.of(streamCreator(0x06, 0x66, 0x6F, 0x6F), SPECIFIED_CHAR_SEQUENCE_SCHEMA,
new TypeReference<String>() { }, "foo"),
createInstance(String.class), "foo"),

// BYTES deserializes into ByteBuffers.
Arguments.of(streamCreator(0), schemaCreator("bytes"), new TypeReference<ByteBuffer>() { },
Arguments.of(streamCreator(0), schemaCreator("bytes"), createInstance(ByteBuffer.class),
ByteBuffer.wrap(new byte[0])),
Arguments.of(streamCreator(4, 42, 42), schemaCreator("bytes"), new TypeReference<ByteBuffer>() { },
Arguments.of(streamCreator(4, 42, 42), schemaCreator("bytes"), createInstance(ByteBuffer.class),
ByteBuffer.wrap(new byte[] { 42, 42 }))
);
}

@Test
public void deserializeNull() {
StepVerifier.create(getSerializer(schemaCreator("null")).deserializeAsync(new ByteArrayInputStream(new byte[0]),
new TypeReference<Void>() { })).verifyComplete();
StepVerifier.create(getSerializer(schemaCreator("null"))
.deserializeAsync(new ByteArrayInputStream(new byte[0]), createInstance(Void.class)))
.verifyComplete();
}

@ParameterizedTest
@MethodSource("deserializeEnumSupplier")
public void deserializeEnum(InputStream avro, PlayingCardSuit expected) {
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString()).deserializeAsync(avro,
new TypeReference<PlayingCardSuit>() { }))
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString())
.deserializeAsync(avro, createInstance(PlayingCardSuit.class)))
.assertNext(actual -> assertEquals(expected, actual))
.verifyComplete();
}
Expand All @@ -118,8 +119,8 @@ private static Stream<Arguments> deserializeEnumSupplier() {

@Test
public void deserializeInvalidEnum() {
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString()).deserializeAsync(streamCreator(8),
new TypeReference<PlayingCardSuit>() { }))
StepVerifier.create(getSerializer(PlayingCardSuit.getClassSchema().toString())
.deserializeAsync(streamCreator(8), createInstance(PlayingCardSuit.class)))
.verifyError();
}

Expand Down Expand Up @@ -213,20 +214,21 @@ private static Stream<Arguments> deserializeRecordSupplier() {
LongLinkedList expectedTwoNodeLinkedList = new LongLinkedList(0L, new LongLinkedList(1L, null));

return Stream.of(
Arguments.of(streamCreator(0), handOfCardsSchema, new TypeReference<HandOfCards>() { },
Arguments.of(streamCreator(0), handOfCardsSchema, createInstance(HandOfCards.class),
new HandOfCards(Collections.emptyList())),
Arguments.of(pairOfAcesHand, handOfCardsSchema, new TypeReference<HandOfCards>() { }, expectedPairOfAces),
Arguments.of(royalFlushHand, handOfCardsSchema, new TypeReference<HandOfCards>() { }, expectedRoyalFlushHand),
Arguments.of(streamCreator(0, 0), longLinkedListSchema, new TypeReference<LongLinkedList>() { },
Arguments.of(pairOfAcesHand, handOfCardsSchema, createInstance(HandOfCards.class), expectedPairOfAces),
Arguments.of(royalFlushHand, handOfCardsSchema, createInstance(HandOfCards.class), expectedRoyalFlushHand),
Arguments.of(streamCreator(0, 0), longLinkedListSchema, createInstance(LongLinkedList.class),
new LongLinkedList(0L, null)),
Arguments.of(twoNodeLinkedList, longLinkedListSchema, new TypeReference<LongLinkedList>() { },
Arguments.of(twoNodeLinkedList, longLinkedListSchema, createInstance(LongLinkedList.class),
expectedTwoNodeLinkedList)
);
}

@Test
public void deserializeNullReturnsNull() {
StepVerifier.create(getSerializer(schemaCreator("null")).deserializeAsync(null, new TypeReference<Void>() { }))
StepVerifier.create(getSerializer(schemaCreator("null"))
.deserializeAsync(null, createInstance(Void.class)))
.verifyComplete();
}

Expand Down Expand Up @@ -397,4 +399,8 @@ private static InputStream streamCreator(int... bytes) {

return new ByteArrayInputStream(converted);
}

private static <T> TypeReference<T> createInstance(Class<T> clazz) {
return TypeReference.createInstance(clazz);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public void deserializeWithDefaultSerializer() {

InputStream jsonStream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));

StepVerifier.create(DEFAULT_SERIALIZER.deserializeAsync(jsonStream, new TypeReference<Person>() { }))
StepVerifier.create(DEFAULT_SERIALIZER.deserializeAsync(jsonStream, TypeReference.createInstance(Person.class)))
.assertNext(actual -> assertEquals(expected, actual))
.verifyComplete();
}
Expand All @@ -53,7 +53,7 @@ public void deserializeWithCustomSerializer() {

InputStream jsonStream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));

StepVerifier.create(CUSTOM_SERIALIZER.deserializeAsync(jsonStream, new TypeReference<Person>() { }))
StepVerifier.create(CUSTOM_SERIALIZER.deserializeAsync(jsonStream, TypeReference.createInstance(Person.class)))
.assertNext(actual -> assertEquals(expected, actual))
.verifyComplete();
}
Expand All @@ -64,7 +64,7 @@ public void deserializeWithDefaultSerializerToJsonObject() {

InputStream jsonStream = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8));

StepVerifier.create(DEFAULT_SERIALIZER.deserializeAsync(jsonStream, new TypeReference<JsonObject>() { }))
StepVerifier.create(DEFAULT_SERIALIZER.deserializeAsync(jsonStream, TypeReference.createInstance(JsonObject.class)))
.assertNext(actual -> {
assertEquals(50, actual.get("age").getAsInt());
assertTrue(actual.get("name").isJsonNull());
Expand Down
Loading