Cleanup SerdeResolverUtils
* Refactor SerdeResolverUtils logic * Add more tests in SerdeResolverUtilsTests * Fixed some random test warnings Fixes #2370 Code review feedback: remove FQ ref to inner method Fix checkstyles
This commit is contained in:
@@ -207,7 +207,7 @@ public class KeyValueSerdeResolver implements ApplicationContextAware {
|
||||
* @return {@link Serde} for the state store key.
|
||||
*/
|
||||
public Serde<?> getStateStoreKeySerde(String keySerdeString) {
|
||||
return getKeySerde(keySerdeString, (Map<String, ?>) null);
|
||||
return getKeySerde(keySerdeString, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +217,7 @@ public class KeyValueSerdeResolver implements ApplicationContextAware {
|
||||
*/
|
||||
public Serde<?> getStateStoreValueSerde(String valueSerdeString) {
|
||||
try {
|
||||
return getValueSerde(valueSerdeString, (Map<String, ?>) null);
|
||||
return getValueSerde(valueSerdeString, null);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new IllegalStateException("Serde class not found: ", ex);
|
||||
@@ -242,20 +242,18 @@ public class KeyValueSerdeResolver implements ApplicationContextAware {
|
||||
}
|
||||
|
||||
private Serde<?> getKeySerde(String keySerdeString, ResolvableType resolvableType, Map<String, ?> extendedConfiguration) {
|
||||
Serde<?> keySerde = null;
|
||||
Serde<?> keySerde;
|
||||
try {
|
||||
if (StringUtils.hasText(keySerdeString)) {
|
||||
keySerde = Utils.newInstance(keySerdeString, Serde.class);
|
||||
}
|
||||
else {
|
||||
keySerde = Serdes.ByteArray();
|
||||
if (resolvableType != null &&
|
||||
(isResolvalbeKafkaStreamsType(resolvableType) || isResolvableKStreamArrayType(resolvableType))) {
|
||||
ResolvableType generic = resolvableType.isArray() ? resolvableType.getComponentType().getGeneric(0) : resolvableType.getGeneric(0);
|
||||
ResolvableType targetType = resolvableType.isArray() ? resolvableType.getComponentType().getGeneric(0) : resolvableType.getGeneric(0);
|
||||
Serde<?> fallbackSerde = getFallbackSerde("default.key.serde");
|
||||
keySerde = SerdeResolverUtils.resolveForType(this.context, generic, fallbackSerde);
|
||||
}
|
||||
if (keySerde == null) {
|
||||
keySerde = Serdes.ByteArray();
|
||||
keySerde = SerdeResolverUtils.resolveForType(this.context, targetType, fallbackSerde);
|
||||
}
|
||||
}
|
||||
keySerde.configure(combineStreamConfigProperties(extendedConfiguration), true);
|
||||
@@ -298,24 +296,19 @@ public class KeyValueSerdeResolver implements ApplicationContextAware {
|
||||
: Serdes.ByteArray();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Serde<?> getValueSerde(String valueSerdeString, ResolvableType resolvableType, Map<String, ?> extendedConfiguration)
|
||||
throws ClassNotFoundException {
|
||||
Serde<?> valueSerde = null;
|
||||
Serde<?> valueSerde;
|
||||
if (StringUtils.hasText(valueSerdeString)) {
|
||||
valueSerde = Utils.newInstance(valueSerdeString, Serde.class);
|
||||
}
|
||||
else {
|
||||
|
||||
valueSerde = Serdes.ByteArray();
|
||||
if (resolvableType != null && ((isResolvalbeKafkaStreamsType(resolvableType)) ||
|
||||
(isResolvableKStreamArrayType(resolvableType)))) {
|
||||
ResolvableType targetType = resolvableType.isArray() ? resolvableType.getComponentType().getGeneric(1) : resolvableType.getGeneric(1);
|
||||
Serde<?> fallbackSerde = getFallbackSerde("default.value.serde");
|
||||
ResolvableType generic = resolvableType.isArray() ? resolvableType.getComponentType().getGeneric(1) : resolvableType.getGeneric(1);
|
||||
valueSerde = SerdeResolverUtils.resolveForType(this.context, generic, fallbackSerde);
|
||||
}
|
||||
if (valueSerde == null) {
|
||||
|
||||
valueSerde = Serdes.ByteArray();
|
||||
valueSerde = SerdeResolverUtils.resolveForType(this.context, targetType, fallbackSerde);
|
||||
}
|
||||
}
|
||||
valueSerde.configure(combineStreamConfigProperties(extendedConfiguration), false);
|
||||
@@ -329,7 +322,7 @@ public class KeyValueSerdeResolver implements ApplicationContextAware {
|
||||
|
||||
private Map<String, ?> combineStreamConfigProperties(Map<String, ?> extendedConfiguration) {
|
||||
if (extendedConfiguration != null && !extendedConfiguration.isEmpty()) {
|
||||
Map<String, Object> streamConfiguration = new HashMap(this.streamConfigGlobalProperties);
|
||||
Map<String, Object> streamConfiguration = new HashMap<>(this.streamConfigGlobalProperties);
|
||||
streamConfiguration.putAll(extendedConfiguration);
|
||||
return streamConfiguration;
|
||||
}
|
||||
|
||||
@@ -16,15 +16,13 @@
|
||||
|
||||
package org.springframework.cloud.stream.binder.kafka.streams;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -32,11 +30,12 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.common.serialization.Serde;
|
||||
import org.apache.kafka.common.serialization.Serdes;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.kafka.support.serializer.JsonSerde;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Utility class that contains various methods to help resolve {@link Serde Serdes}.
|
||||
@@ -48,124 +47,120 @@ abstract class SerdeResolverUtils {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(SerdeResolverUtils.class);
|
||||
|
||||
/** Classnames of the standard built-in Serdes supported in {@link Serdes#serdeFrom(Class)}. */
|
||||
private static final Set<String> STANDARD_SERDE_CLASSNAMES = Set.of(
|
||||
Serdes.String().getClass().getName(),
|
||||
Serdes.Short().getClass().getName(),
|
||||
Serdes.Integer().getClass().getName(),
|
||||
Serdes.Long().getClass().getName(),
|
||||
Serdes.Float().getClass().getName(),
|
||||
Serdes.Double().getClass().getName(),
|
||||
Serdes.ByteArray().getClass().getName(),
|
||||
Serdes.ByteBuffer().getClass().getName(),
|
||||
Serdes.Bytes().getClass().getName(),
|
||||
Serdes.UUID().getClass().getName());
|
||||
|
||||
/**
|
||||
* Return the closest matching configured {@code Serde<?>} bean if one exists, or the specified
|
||||
* {@code fallbackSerde}, or finally a standard default serde if no fallback specified.
|
||||
*
|
||||
* Return the {@code Serde<?>} to use for the specified type using the following steps until a match is found.
|
||||
* <p><ul>
|
||||
* <li>the closest matching configured {@code Serde<?>} bean if one exists</li>
|
||||
* <li>the Kafka Streams built-in serde if the target type is one of the built-in types exposed by Kafka Streams
|
||||
* (Integer, Long, Short, Double, Float, byte[], UUID and String)
|
||||
* <li>the fallback serde if specified and not one of the Kafka Streams exposed type serdes</li>
|
||||
* <li>the {@link JsonSerde} if the target type is not exactly {@code Object}</li>
|
||||
* <li>the fallback as the last resort</li>
|
||||
* </ul>
|
||||
* @param context the application context
|
||||
* @param targetType the target type to find the serde for
|
||||
* @param fallbackSerde the fallback serde in case no matching serde bean found in the context
|
||||
* @return serde to use for the target type
|
||||
* @param fallbackSerde the serde to use when no type can be inferred
|
||||
* @return serde to use for the target type or {@code fallbackSerde} as outlined in the method description
|
||||
*/
|
||||
static Serde<?> resolveForType(ConfigurableApplicationContext context, ResolvableType targetType, Serde<?> fallbackSerde) {
|
||||
static Serde<?> resolveForType(ConfigurableApplicationContext context, ResolvableType targetType, @Nullable Serde<?> fallbackSerde) {
|
||||
|
||||
List<Serde<?>> matchingSerdes = findMatchingSerdes(context, targetType);
|
||||
if (!matchingSerdes.isEmpty()) {
|
||||
return matchingSerdes.get(0);
|
||||
}
|
||||
|
||||
// We don't attempt to find a matching Serde for type '?'
|
||||
if (targetType.getRawClass() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Serde<?> serde = null;
|
||||
Class<?> genericRawClazz = targetType.getRawClass();
|
||||
if (Integer.class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.Integer();
|
||||
|
||||
// We don't attempt to find a matching Serde for type '?' - just return fallback
|
||||
if (genericRawClazz == null) {
|
||||
return fallbackSerde;
|
||||
}
|
||||
else if (Long.class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.Long();
|
||||
|
||||
List<String> matchingSerdes = beanNamesForMatchingSerdes(context, targetType);
|
||||
if (!matchingSerdes.isEmpty()) {
|
||||
return context.getBean(matchingSerdes.get(0), Serde.class);
|
||||
}
|
||||
else if (Short.class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.Short();
|
||||
|
||||
// Use standard serde for built-in types
|
||||
Serde<?> standardDefaultSerde = getStandardDefaultSerde(genericRawClazz);
|
||||
if (standardDefaultSerde != null) {
|
||||
return standardDefaultSerde;
|
||||
}
|
||||
else if (Double.class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.Double();
|
||||
|
||||
// Use fallback if specified and not from std defaults (we know from above that type is not std default
|
||||
// so using a fallback that is std default type would not work)
|
||||
if (fallbackSerde != null && !isSerdeFromStandardDefaults(fallbackSerde)) {
|
||||
return fallbackSerde;
|
||||
}
|
||||
else if (Float.class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.Float();
|
||||
|
||||
// Use JsonSerde if type is not exactly Object
|
||||
if (!genericRawClazz.isAssignableFrom((Object.class))) {
|
||||
return new JsonSerde<>(genericRawClazz);
|
||||
}
|
||||
else if (byte[].class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.ByteArray();
|
||||
|
||||
// Finally, just resort to using the fallback
|
||||
return fallbackSerde;
|
||||
}
|
||||
|
||||
private static Serde<?> getStandardDefaultSerde(Class<?> genericRawClazz) {
|
||||
try {
|
||||
return Serdes.serdeFrom(genericRawClazz);
|
||||
}
|
||||
else if (String.class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.String();
|
||||
}
|
||||
else if (UUID.class.isAssignableFrom(genericRawClazz)) {
|
||||
serde = Serdes.UUID();
|
||||
}
|
||||
else if (!isSerdeFromStandardDefaults(fallbackSerde)) {
|
||||
// User purposely set a default serde that is not one of the above
|
||||
serde = fallbackSerde;
|
||||
}
|
||||
else {
|
||||
// If the type is Object, then skip assigning the JsonSerde and let the fallback mechanism takes precedence.
|
||||
if (!genericRawClazz.isAssignableFrom((Object.class))) {
|
||||
serde = new JsonSerde(genericRawClazz);
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace(ex);
|
||||
}
|
||||
}
|
||||
return serde;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isSerdeFromStandardDefaults(Serde<?> serde) {
|
||||
if (serde != null) {
|
||||
if (Number.class.isAssignableFrom(serde.getClass())) {
|
||||
return true;
|
||||
}
|
||||
else if (Serdes.ByteArray().getClass().isAssignableFrom(serde.getClass())) {
|
||||
return true;
|
||||
}
|
||||
else if (Serdes.String().getClass().isAssignableFrom(serde.getClass())) {
|
||||
return true;
|
||||
}
|
||||
else if (Serdes.UUID().getClass().isAssignableFrom(serde.getClass())) {
|
||||
return true;
|
||||
}
|
||||
if (serde == null) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
return STANDARD_SERDE_CLASSNAMES.contains(serde.getClass().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all {@link Serde} beans that are assignable from {@code targetType}.
|
||||
* Find the names of all {@link Serde} beans that can be used for {@code targetType}.
|
||||
*
|
||||
* @param context the application context
|
||||
* @param targetType the target type the serdes are being matched for
|
||||
* @return list of matching serdes order by most specific match, or an empty list if no matches found
|
||||
* @return list of bean names for matching serdes ordered by most specific match, or an empty list if no matches found
|
||||
*/
|
||||
static List<Serde<?>> findMatchingSerdes(ConfigurableApplicationContext context, ResolvableType targetType) {
|
||||
static List<String> beanNamesForMatchingSerdes(ConfigurableApplicationContext context, ResolvableType targetType) {
|
||||
// We don't attempt to find a matching Serde for type '?'
|
||||
if (targetType.getRawClass() == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<SerdeWithSpecificityScore> matchingSerdes = new ArrayList<>();
|
||||
|
||||
context.getBeansOfType(Serde.class).forEach((beanName, serdeBean) -> {
|
||||
final Class<?> beanConfigClass = ClassUtils.resolveClassName(((AnnotatedBeanDefinition)
|
||||
context.getBeanFactory().getBeanDefinition(beanName))
|
||||
.getMetadata().getClassName(),
|
||||
ClassUtils.getDefaultClassLoader());
|
||||
ResolvableType serdeType = ResolvableType.forClassWithGenerics(Serde.class, targetType);
|
||||
String[] serdeBeanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context.getBeanFactory(),
|
||||
serdeType, false, false);
|
||||
Arrays.stream(serdeBeanNames).forEach((beanName) -> {
|
||||
try {
|
||||
Method[] methods = beanConfigClass.getMethods();
|
||||
Optional<Method> serdeBeanMethod = Arrays.stream(methods).filter(m -> m.getName().equals(beanName)).findFirst();
|
||||
serdeBeanMethod.ifPresent((method) -> {
|
||||
ResolvableType serdeBeanMethodReturnType = ResolvableType.forMethodReturnType(method, beanConfigClass);
|
||||
ResolvableType serdeBeanGeneric = serdeBeanMethodReturnType.getGeneric(0);
|
||||
// We don't attempt to use a Serde<?> as a match for anything currently
|
||||
if (serdeBeanGeneric.getRawClass() != null && serdeBeanGeneric.isAssignableFrom(targetType)) {
|
||||
matchingSerdes.add(new SerdeWithSpecificityScore(calculateScore(targetType, serdeBeanGeneric), serdeBean));
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
if (LOG.isTraceEnabled()) {
|
||||
LOG.trace("Failed to introspect Serde bean method '" + serdeBean + "'", e);
|
||||
BeanDefinition beanDefinition = context.getBeanFactory().getMergedBeanDefinition(beanName);
|
||||
ResolvableType serdeBeanGeneric = beanDefinition.getResolvableType().getGeneric(0);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Found matching Serde<" + serdeBeanGeneric.getType() + "> under beanName=" + beanName);
|
||||
}
|
||||
matchingSerdes.add(new SerdeWithSpecificityScore(calculateScore(targetType, serdeBeanGeneric), beanName));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
LOG.warn("Failed introspecting Serde bean '" + beanName + "'", ex);
|
||||
}
|
||||
});
|
||||
if (!matchingSerdes.isEmpty()) {
|
||||
return matchingSerdes.stream().sorted(Collections.reverseOrder())
|
||||
.map(SerdeWithSpecificityScore::getSerde)
|
||||
.map(SerdeWithSpecificityScore::getSerdeBeanName)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
@@ -182,7 +177,7 @@ abstract class SerdeResolverUtils {
|
||||
* <p><br><b>Example:</b>
|
||||
* <pre>{@code
|
||||
* -------------------------------------------------------------------------------------------------------
|
||||
* targetType: Foo<Date> toString='Foo<Date>' typeName='Foo<java.util.Date>'
|
||||
* targetType: Foo<Date> toString='Foo<Date>' typeName='Foo<java.util.Date>'
|
||||
* typeToCheck1: Foo<Date> toString='Foo<Date>' typeName='Foo<java.util.Date>'
|
||||
* typeToCheck2: Foo<? extends Date> toString='Foo<Date>' typeName='Foo<? extends java.util.Date>'
|
||||
* -------------------------------------------------------------------------------------------------------
|
||||
@@ -221,15 +216,15 @@ abstract class SerdeResolverUtils {
|
||||
*/
|
||||
private static class SerdeWithSpecificityScore implements Comparable<SerdeWithSpecificityScore> {
|
||||
private Integer score;
|
||||
private Serde<?> serde;
|
||||
private String serdeBeanName;
|
||||
|
||||
SerdeWithSpecificityScore(Integer score, Serde<?> serde) {
|
||||
SerdeWithSpecificityScore(Integer score, String serdeBeanName) {
|
||||
this.score = Objects.requireNonNull(score);
|
||||
this.serde = Objects.requireNonNull(serde);
|
||||
this.serdeBeanName = Objects.requireNonNull(serdeBeanName);
|
||||
}
|
||||
|
||||
Serde<?> getSerde() {
|
||||
return serde;
|
||||
String getSerdeBeanName() {
|
||||
return serdeBeanName;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -16,77 +16,349 @@
|
||||
|
||||
package org.springframework.cloud.stream.binder.kafka.streams;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.kafka.common.serialization.Deserializer;
|
||||
import org.apache.kafka.common.serialization.Serde;
|
||||
import org.apache.kafka.common.serialization.Serdes;
|
||||
import org.apache.kafka.common.serialization.Serializer;
|
||||
import org.apache.kafka.common.utils.Bytes;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.kafka.support.serializer.JsonSerde;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SerdeResolverUtils}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "NewClassNamingConvention", "unchecked" })
|
||||
class SerdeResolverUtilsTests {
|
||||
|
||||
/**
|
||||
* Verify that {@link SerdeResolverUtils#findMatchingSerdes} returns the proper serdes
|
||||
* in the proper order for the following grid:
|
||||
* <p><br>
|
||||
* <pre>{@code
|
||||
* ------------------------------------------------------------------
|
||||
* KStream type | Serde type
|
||||
* ------------------------------------------------------------------
|
||||
* | GE<Date> | GE<? extends Date> | GE<?> | GE
|
||||
* ------------------------------------------------------------------
|
||||
* GE<Date> | Y | Y | Y | Y
|
||||
* GE<? extends Date> | N | Y | Y | N
|
||||
* GE<?> | N | N | Y | N
|
||||
* GE | N | N | N | N
|
||||
* ------------------------------------------------------------------
|
||||
* }</pre>
|
||||
*/
|
||||
@Test
|
||||
void findMatchingSerdesForSimpleGenericType() {
|
||||
@Nested
|
||||
class ResolveForType {
|
||||
|
||||
ResolvableType geDate = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<Date>>() { });
|
||||
ResolvableType geBounded = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<? extends Date>>() { });
|
||||
ResolvableType geWildcard = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<?>>() { });
|
||||
ResolvableType geRaw = ResolvableType.forRawClass(GenericEvent.class);
|
||||
private ApplicationContextRunner contextRunner = new ApplicationContextRunner();
|
||||
|
||||
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withUserConfiguration(SerdeResolverTestApp.class);
|
||||
private Serde<?> fallback = mock(Serde.class);
|
||||
|
||||
contextRunner.run((context) -> {
|
||||
@Test
|
||||
void returnsFallbackSerdeForWildcard() {
|
||||
this.contextRunner
|
||||
.withConfiguration(AutoConfigurations.of(SerdeResolverSimpleTestApp.class))
|
||||
.run((context) -> {
|
||||
ResolvableType wildcardType = ResolvableType.forClass(Serde.class).getGeneric(0);
|
||||
assertThat(SerdeResolverUtils.resolveForType(context, wildcardType, fallback)).isSameAs(fallback);
|
||||
});
|
||||
}
|
||||
|
||||
assertThat(SerdeResolverUtils.findMatchingSerdes(context, geDate))
|
||||
.extracting("name")
|
||||
.containsExactly("genericEventDateSerde", "genericEventDateBoundedSerde", "genericEventWildcardSerde", "genericEventRawSerde");
|
||||
@Test
|
||||
void returnsSerdeBeanForMatchingType() {
|
||||
this.contextRunner
|
||||
.withConfiguration(AutoConfigurations.of(SerdeResolverSimpleTestApp.class))
|
||||
.run((context) -> {
|
||||
ResolvableType fooType = ResolvableType.forClass(Foo.class);
|
||||
assertThat(SerdeResolverUtils.resolveForType(context, fooType, fallback)).isInstanceOf(FooSerde.class);
|
||||
});
|
||||
}
|
||||
|
||||
assertThat(SerdeResolverUtils.findMatchingSerdes(context, geBounded))
|
||||
.extracting("name")
|
||||
.containsExactly("genericEventDateBoundedSerde", "genericEventWildcardSerde");
|
||||
@Nested
|
||||
class NoMatchingSerdeBeans {
|
||||
|
||||
assertThat(SerdeResolverUtils.findMatchingSerdes(context, geWildcard))
|
||||
.extracting("name")
|
||||
.containsExactly("genericEventWildcardSerde");
|
||||
@ParameterizedTest
|
||||
@MethodSource("kafkaStreamsBuiltInTypes")
|
||||
void returnsStandardSerdeForKafkaStreamsBuiltInType(Class<?> builtInType, Serde<?> expectedBuiltInSerde) {
|
||||
contextRunner.run((context) ->
|
||||
assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(builtInType), fallback))
|
||||
.isInstanceOf(expectedBuiltInSerde.getClass()));
|
||||
}
|
||||
|
||||
// Because GenericEvent is a parameterized type, Serde<GenericEvent> resolves to Serde<GenericEvent<?>>
|
||||
// which is not assignable from GenericEvent
|
||||
assertThat(SerdeResolverUtils.findMatchingSerdes(context, geRaw))
|
||||
.extracting("name")
|
||||
.isEmpty();
|
||||
});
|
||||
static Stream<Arguments> kafkaStreamsBuiltInTypes() {
|
||||
return Stream.of(
|
||||
arguments(String.class, Serdes.String()),
|
||||
arguments(Short.class, Serdes.Short()),
|
||||
arguments(Integer.class, Serdes.Integer()),
|
||||
arguments(Long.class, Serdes.Long()),
|
||||
arguments(Float.class, Serdes.Float()),
|
||||
arguments(Double.class, Serdes.Double()),
|
||||
arguments(byte[].class, Serdes.ByteArray()),
|
||||
arguments(ByteBuffer.class, Serdes.ByteBuffer()),
|
||||
arguments(Bytes.class, Serdes.Bytes()),
|
||||
arguments(UUID.class, Serdes.UUID())
|
||||
);
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ForNonKafkaStreamsBuiltInType {
|
||||
|
||||
@Test
|
||||
void returnsFallbackSerdeWhenValidFallbackSpecified() {
|
||||
contextRunner.run((context) ->
|
||||
assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Foo.class), fallback))
|
||||
.isSameAs(fallback));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidFallbackSerdeProvider")
|
||||
void returnsJsonSerdeWhenInvalidFallbackSpecified(Serde<?> invalidFallback) {
|
||||
contextRunner.run((context) ->
|
||||
assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Foo.class), invalidFallback))
|
||||
.isInstanceOf(JsonSerde.class));
|
||||
}
|
||||
|
||||
static Stream<Serde<?>> invalidFallbackSerdeProvider() {
|
||||
return Stream.of(
|
||||
Serdes.String(),
|
||||
Serdes.Short(),
|
||||
Serdes.Integer(),
|
||||
Serdes.Long(),
|
||||
Serdes.Float(),
|
||||
Serdes.Double(),
|
||||
Serdes.ByteArray(),
|
||||
Serdes.ByteBuffer(),
|
||||
Serdes.Bytes(),
|
||||
Serdes.UUID()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsJsonSerdeWhenFallbackNotSpecified() {
|
||||
contextRunner.run((context ->
|
||||
assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Foo.class), null))
|
||||
.isInstanceOf(JsonSerde.class)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsFallbackSerdeForJavaLangObject() {
|
||||
// This is an edge case as the only way to get to the JsonSerde step in the 1st place is when
|
||||
// no fallback is specified or a fallback is specified but its invalid (for a built-in type).
|
||||
// We will use the 'fallback is not specified' scenario for this test.
|
||||
contextRunner.run((context) ->
|
||||
assertThat(SerdeResolverUtils.resolveForType(context, ResolvableType.forClass(Object.class), null))
|
||||
.isNull());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class BeanNamesForMatchingSerdes {
|
||||
|
||||
@Test
|
||||
void returnsNoSerdesForWildcardType() {
|
||||
new ApplicationContextRunner().withUserConfiguration(SerdeResolverSimpleTestApp.class)
|
||||
.run((context) -> {
|
||||
ResolvableType wildcardType = ResolvableType.forClass(Serde.class).getGeneric(0);
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, wildcardType)).isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Verify that {@link SerdeResolverUtils#beanNamesForMatchingSerdes} returns the proper serdes in the proper order
|
||||
* for the following grid:
|
||||
* <p><br>
|
||||
* <i>NOTE:</i> {@code GE = GenericEvent}
|
||||
* <p><pre>{@code
|
||||
* ------------------------------------------------------------------
|
||||
* KStream type | Serde type
|
||||
* ------------------------------------------------------------------
|
||||
* | GE<Date> | GE<? extends Date> | GE<?> | GE
|
||||
* ------------------------------------------------------------------
|
||||
* GE<Date> | 1 | - | - | -
|
||||
* GE<? extends Date> | 2 | 1 | - | -
|
||||
* GE<?> | 4 | 2 | 1 | 3
|
||||
* GE | 1 | - | - | 2
|
||||
* ------------------------------------------------------------------
|
||||
* }</pre>
|
||||
* <p><br>
|
||||
* <i>NOTE:</i> On the last row, one might expect the {@code GE} serde to be the top match with the {@code GE}
|
||||
* kstream. However, that is not the case because {@code GE} is a parameterized type and therefore when
|
||||
* specified as a raw type (without any type info) it resolves to {@code GE<?>} which throws off the ordering.
|
||||
* A best practice is to specify the type info (even if it is wildcard) for KStream parameterized types.
|
||||
*/
|
||||
@Test
|
||||
void returnsProperlyOrderedSerdesForSimpleGenericTypes() {
|
||||
|
||||
ResolvableType geDate = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<Date>>() { });
|
||||
ResolvableType geBounded = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<? extends Date>>() { });
|
||||
ResolvableType geWildcard = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<?>>() { });
|
||||
ResolvableType geRaw = ResolvableType.forRawClass(GenericEvent.class);
|
||||
|
||||
new ApplicationContextRunner().withUserConfiguration(SerdeResolverSimpleTestApp.class).run((context) -> {
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geDate))
|
||||
.containsExactly(
|
||||
"geDateSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geBounded))
|
||||
.containsExactly(
|
||||
"geDateBoundedSerde",
|
||||
"geDateSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geWildcard))
|
||||
.containsExactly(
|
||||
"geWildcardSerde",
|
||||
"geDateBoundedSerde",
|
||||
"geRawSerde",
|
||||
"geDateSerde",
|
||||
"geStringSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geRaw))
|
||||
.containsExactly(
|
||||
"geDateSerde",
|
||||
"geStringSerde",
|
||||
"geRawSerde");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that {@link SerdeResolverUtils#beanNamesForMatchingSerdes} returns the proper serdes in the proper order
|
||||
* for the following grid:
|
||||
* <p><br>
|
||||
* <i>NOTE:</i> {@code GE = GenericEvent}
|
||||
* <p><pre>{@code
|
||||
* -------------------------------------------------------------------------------------------------------------------
|
||||
* KStream type | Serde type
|
||||
* -------------------------------------------------------------------------------------------------------------------
|
||||
* | GE<F<D>> | GE<F<?D>> | GE<F<?>> | GE<F> | GE<?F<D>> | GE<?F<?D>> | GE<?F<?>> | GE<?F> | GE<?> | GE
|
||||
* -------------------------------------------------------------------------------------------------------------------
|
||||
* GE<F<D>> | 1 | - | - | - | - | - | - | - | - | -
|
||||
* GE<F<?D>> | 2 | 1 | - | - | - | - | - | - | - | -
|
||||
* GE<F<?>> | 4 | 3 | 1 | 2 | - | - | - | - | - | -
|
||||
* GE<F> | 2 | - | - | 1 | - | - | - | - | - | -
|
||||
* GE<?F<D>> | 2 | - | - | - | 1 | - | - | - | - | -
|
||||
* GE<?F<?D>> | 3 | 4 | - | - | 2 | 1 | - | - | - | -
|
||||
* GE<?F<?>> | 7 | 8 | 5 | 6 | 4 | 3 | 1 | 2 | - | -
|
||||
* GE<?F> | 4 | - | - | 3 | 2 | - | - | 1 | - | -
|
||||
* GE<?> | 7 | 8 | 9 | 10 | 2 | 3 | 4 | 5 | 1 | 6
|
||||
* GE | 1 | 2 | 3 | 4 | - | - | - | - | - | 5
|
||||
* -------------------------------------------------------------------------------------------------------------------
|
||||
* }</pre>
|
||||
* <p><br>
|
||||
* <i>NOTE:</i> On the last row, one might expect the {@code GE} serde to be the top match with the {@code GE}
|
||||
* kstream. However, that is not the case because {@code GE} is a parameterized type and therefore when
|
||||
* specified as a raw type (without any type info) it resolves to {@code GE<?>} which throws off the ordering.
|
||||
* A best practice is to specify the type info (even if it is wildcard) for KStream parameterized types.
|
||||
*/
|
||||
@Test
|
||||
void returnsProperlyOrderedSerdesForComplexGenericTypes() {
|
||||
|
||||
ResolvableType geFooDate = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<Foo<Date>>>() { });
|
||||
ResolvableType geFooDateBounded = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<Foo<? extends Date>>>() { });
|
||||
ResolvableType geFooWildcard = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<Foo<?>>>() { });
|
||||
ResolvableType geFooRaw = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<Foo>>() { });
|
||||
ResolvableType geFooBoundedDate = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<? extends Foo<Date>>>() { });
|
||||
ResolvableType geFooBoundedDateBounded = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<? extends Foo<? extends Date>>>() { });
|
||||
ResolvableType geFooBoundedWildcard = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<? extends Foo<?>>>() { });
|
||||
ResolvableType geFooBoundedRaw = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<? extends Foo>>() { });
|
||||
ResolvableType geWildcard = ResolvableType.forType(new ParameterizedTypeReference<GenericEvent<?>>() { });
|
||||
ResolvableType geRaw = ResolvableType.forRawClass(GenericEvent.class);
|
||||
|
||||
new ApplicationContextRunner().withUserConfiguration(SerdeResolverComplexTestApp.class).run((context) -> {
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooDate))
|
||||
.containsExactly(
|
||||
"geFooDateSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooDateBounded))
|
||||
.containsExactly(
|
||||
"geFooDateBoundedSerde",
|
||||
"geFooDateSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooWildcard))
|
||||
.containsExactly(
|
||||
"geFooWildcardSerde",
|
||||
"geFooRawSerde",
|
||||
"geFooDateBoundedSerde",
|
||||
"geFooDateSerde",
|
||||
"geFooStringSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooRaw))
|
||||
.containsExactly(
|
||||
"geFooRawSerde",
|
||||
"geFooDateSerde",
|
||||
"geFooStringSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedDate))
|
||||
.containsExactly(
|
||||
"geFooBoundedDateSerde",
|
||||
"geFooDateSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedDateBounded))
|
||||
.containsExactly(
|
||||
"geFooBoundedDateBoundedSerde",
|
||||
"geFooBoundedDateSerde",
|
||||
"geFooDateSerde",
|
||||
"geFooDateBoundedSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedWildcard))
|
||||
.containsExactly(
|
||||
"geFooBoundedWildcardSerde",
|
||||
"geFooBoundedRawSerde",
|
||||
"geFooBoundedDateBoundedSerde",
|
||||
"geFooBoundedDateSerde",
|
||||
"geFooBoundedStringSerde",
|
||||
"geFooWildcardSerde",
|
||||
"geFooRawSerde",
|
||||
"geFooDateSerde",
|
||||
"geFooDateBoundedSerde",
|
||||
"geFooStringSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geFooBoundedRaw))
|
||||
.containsExactly(
|
||||
"geFooBoundedRawSerde",
|
||||
"geFooBoundedDateSerde",
|
||||
"geFooBoundedStringSerde",
|
||||
"geFooRawSerde",
|
||||
"geFooDateSerde",
|
||||
"geFooStringSerde");
|
||||
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geWildcard))
|
||||
.containsExactly(
|
||||
"geWildcardSerde",
|
||||
"geFooBoundedDateSerde",
|
||||
"geFooBoundedDateBoundedSerde",
|
||||
"geFooBoundedStringSerde",
|
||||
"geFooBoundedWildcardSerde",
|
||||
"geFooBoundedRawSerde",
|
||||
"geRawSerde",
|
||||
"geFooDateSerde",
|
||||
"geFooDateBoundedSerde",
|
||||
"geFooStringSerde",
|
||||
"geFooWildcardSerde",
|
||||
"geFooRawSerde");
|
||||
|
||||
// One might expect geRawSerde to win in order, but it does not because GE is a parameterized type and
|
||||
// therefore GE resolves to GE<?> which throws off the ordering. Bottom line, for parameterized types
|
||||
// be sure to specify a type (even if it's wildcard) in the KStream<T> definition.
|
||||
assertThat(SerdeResolverUtils.beanNamesForMatchingSerdes(context, geRaw))
|
||||
.containsExactly(
|
||||
"geFooDateSerde",
|
||||
"geFooDateBoundedSerde",
|
||||
"geFooStringSerde",
|
||||
"geFooWildcardSerde",
|
||||
"geFooRawSerde",
|
||||
"geRawSerde");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static class GenericEventSerde<T> implements Serde<GenericEvent<? extends T>> {
|
||||
@@ -96,10 +368,6 @@ class SerdeResolverUtilsTests {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Serializer<GenericEvent<? extends T>> serializer() {
|
||||
return null;
|
||||
@@ -109,41 +377,141 @@ class SerdeResolverUtilsTests {
|
||||
public Deserializer<GenericEvent<? extends T>> deserializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GenericEventSerde(" + name + ")";
|
||||
}
|
||||
}
|
||||
|
||||
static class GenericEvent<T> { }
|
||||
|
||||
static class FooSerde implements Serde<Foo> {
|
||||
|
||||
@Override
|
||||
public Serializer<Foo> serializer() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Deserializer<Foo> deserializer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class Foo<T> { }
|
||||
|
||||
@EnableAutoConfiguration
|
||||
static class SerdeResolverTestApp {
|
||||
static class SerdeResolverSimpleTestApp {
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<Date>> genericEventDateSerde() {
|
||||
return new GenericEventSerde("genericEventDateSerde");
|
||||
public Serde<GenericEvent<Date>> geDateSerde() {
|
||||
return new GenericEventSerde("geDateSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<? extends Date>> genericEventDateBoundedSerde() {
|
||||
return new GenericEventSerde("genericEventDateBoundedSerde");
|
||||
public Serde<GenericEvent<? extends Date>> geDateBoundedSerde() {
|
||||
return new GenericEventSerde("geDateBoundedSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<String>> genericEventStringSerde() {
|
||||
return new GenericEventSerde("genericEventStringSerde");
|
||||
public Serde<GenericEvent<String>> geStringSerde() {
|
||||
return new GenericEventSerde("geStringSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<?>> genericEventWildcardSerde() {
|
||||
return new GenericEventSerde("genericEventWildcardSerde");
|
||||
public Serde<GenericEvent<?>> geWildcardSerde() {
|
||||
return new GenericEventSerde("geWildcardSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent> genericEventRawSerde() {
|
||||
return new GenericEventSerde("genericEventRawSerde");
|
||||
public Serde<GenericEvent> geRawSerde() {
|
||||
return new GenericEventSerde("geRawSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<?> widlcardSerde() {
|
||||
return Serdes.Void();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<Foo> fooSerde() {
|
||||
return new FooSerde();
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
static class SerdeResolverComplexTestApp {
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<Foo<Date>>> geFooDateSerde() {
|
||||
return new GenericEventSerde("geFooDateSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<Foo<? extends Date>>> geFooDateBoundedSerde() {
|
||||
return new GenericEventSerde("geFooDateBoundedSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<Foo<String>>> geFooStringSerde() {
|
||||
return new GenericEventSerde("geFooStringSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<Foo<?>>> geFooWildcardSerde() {
|
||||
return new GenericEventSerde("geFooWildcardSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<Foo>> geFooRawSerde() {
|
||||
return new GenericEventSerde("geFooRawSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<? extends Foo<Date>>> geFooBoundedDateSerde() {
|
||||
return new GenericEventSerde("geFooBoundedDateSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<? extends Foo<? extends Date>>> geFooBoundedDateBoundedSerde() {
|
||||
return new GenericEventSerde("geFooBoundedDateBoundedSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<? extends Foo<String>>> geFooBoundedStringSerde() {
|
||||
return new GenericEventSerde("geFooBoundedStringSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<? extends Foo<?>>> geFooBoundedWildcardSerde() {
|
||||
return new GenericEventSerde("geFooBoundedWildcardSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<? extends Foo>> geFooBoundedRawSerde() {
|
||||
return new GenericEventSerde("geFooBoundedRawSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent<?>> geWildcardSerde() {
|
||||
return new GenericEventSerde("geWildcardSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<GenericEvent> geRawSerde() {
|
||||
return new GenericEventSerde("geRawSerde");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<?> widlcardSerde() {
|
||||
return Serdes.Void();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Serde<Foo> fooSerde() {
|
||||
return new FooSerde();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user