From 605bcde87e99e4efcf6a616b087b13ffbdfc7f26 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Mon, 21 Apr 2025 13:03:23 -0500 Subject: [PATCH] Stop using Class as map key This commit updates the DefaultTopicResolver and DefaulSchemaResolver to use String class names rather than Class instances as map keys for their custom mappings. Resolves #1078 --- .../pulsar/core/DefaultSchemaResolver.java | 44 +++++++++++++++---- .../pulsar/core/DefaultTopicResolver.java | 44 ++++++++++++++++--- .../core/DefaultSchemaResolverTests.java | 13 +++--- .../core/DefaultTopicResolverTests.java | 10 ++--- 4 files changed, 85 insertions(+), 26 deletions(-) diff --git a/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultSchemaResolver.java b/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultSchemaResolver.java index 201eef11..3ccc5a91 100644 --- a/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultSchemaResolver.java +++ b/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultSchemaResolver.java @@ -23,12 +23,12 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.Optional; import org.apache.pulsar.client.api.Schema; import org.apache.pulsar.client.impl.schema.AvroSchema; @@ -92,7 +92,7 @@ public class DefaultSchemaResolver implements SchemaResolver { BASE_SCHEMA_MAPPINGS.put(LocalTime.class, Schema.LOCAL_TIME); } - private final Map, Schema> customSchemaMappings = new LinkedHashMap<>(); + private final Map> customSchemaMappings = new LinkedHashMap<>(); private final PulsarMessageAnnotationRegistry pulsarMessageAnnotationRegistry = new PulsarMessageAnnotationRegistry(); @@ -122,7 +122,7 @@ public class DefaultSchemaResolver implements SchemaResolver { */ @Nullable public Schema addCustomSchemaMapping(Class messageType, Schema schema) { - return this.customSchemaMappings.put(messageType, schema); + return this.customSchemaMappings.put(this.toMessageTypeMapKey(messageType), schema); } /** @@ -133,15 +133,30 @@ public class DefaultSchemaResolver implements SchemaResolver { */ @Nullable public Schema removeCustomMapping(Class messageType) { - return this.customSchemaMappings.remove(messageType); + return this.customSchemaMappings.remove(this.toMessageTypeMapKey(messageType)); } /** - * Gets the currently registered custom mappings from message type to schema. - * @return unmodifiable map of custom mappings + * Gets the currently registered custom mapping for the specified message type. + * @return optional custom topic registered for the message type + * @deprecated deprecated in favor of {@link #getCustomSchemaMapping(Class)} (Class)} */ + @Deprecated(since = "1.2.5", forRemoval = true) public Map, Schema> getCustomSchemaMappings() { - return Collections.unmodifiableMap(this.customSchemaMappings); + Map, Schema> copyOfMappings = new HashMap<>(); + this.customSchemaMappings.entrySet() + .stream() + .map((e) -> copyOfMappings.put(this.fromMessageTypeMapKey(e.getKey()), e.getValue())); + return copyOfMappings; + } + + /** + * Gets the currently registered custom mapping for the specified message type. + * @param messageType the message type + * @return optional custom topic registered for the message type + */ + public Optional> getCustomSchemaMapping(Class messageType) { + return Optional.ofNullable(this.customSchemaMappings.get(this.toMessageTypeMapKey(messageType))); } @Override @@ -162,7 +177,7 @@ public class DefaultSchemaResolver implements SchemaResolver { @Nullable protected Schema getCustomSchemaOrMaybeDefault(@Nullable Class messageClass, boolean returnDefault) { // Check for custom schema mapping - Schema schema = this.customSchemaMappings.get(messageClass); + var schema = this.getCustomSchemaMapping(messageClass).orElse(null); // If no custom schema mapping found, look for @PulsarMessage (if enabled) if (this.usePulsarMessageAnnotations && schema == null && messageClass != null) { @@ -289,4 +304,17 @@ public class DefaultSchemaResolver implements SchemaResolver { return (Schema) rawSchema; } + private Class fromMessageTypeMapKey(String messageTypeKey) { + try { + return Class.forName(messageTypeKey); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private String toMessageTypeMapKey(Class messageType) { + return messageType.getName(); + } + } diff --git a/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultTopicResolver.java b/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultTopicResolver.java index c4d22c5d..3c7b35b2 100644 --- a/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultTopicResolver.java +++ b/spring-pulsar/src/main/java/org/springframework/pulsar/core/DefaultTopicResolver.java @@ -16,9 +16,10 @@ package org.springframework.pulsar.core; -import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; import org.springframework.beans.BeansException; @@ -45,7 +46,7 @@ public class DefaultTopicResolver implements TopicResolver, BeanFactoryAware { private final LogAccessor logger = new LogAccessor(this.getClass()); - private final Map, String> customTopicMappings = new LinkedHashMap<>(); + private final Map customTopicMappings = new LinkedHashMap<>(); private final PulsarMessageAnnotationRegistry pulsarMessageAnnotationRegistry = new PulsarMessageAnnotationRegistry(); @@ -86,7 +87,7 @@ public class DefaultTopicResolver implements TopicResolver, BeanFactoryAware { */ @Nullable public String addCustomTopicMapping(Class messageType, String topic) { - return this.customTopicMappings.put(messageType, topic); + return this.customTopicMappings.put(this.toMessageTypeMapKey(messageType), topic); } /** @@ -97,15 +98,31 @@ public class DefaultTopicResolver implements TopicResolver, BeanFactoryAware { */ @Nullable public String removeCustomMapping(Class messageType) { - return this.customTopicMappings.remove(messageType); + return this.customTopicMappings.remove(this.toMessageTypeMapKey(messageType)); } /** - * Gets the currently registered custom mappings from message type to topic. + * Gets the currently registered custom mappings from message type class name to + * topic. * @return unmodifiable map of custom mappings + * @deprecated deprecated in favor of {@link #getCustomTopicMapping(Class)} */ + @Deprecated(since = "1.2.5", forRemoval = true) public Map, String> getCustomTopicMappings() { - return Collections.unmodifiableMap(this.customTopicMappings); + Map, String> copyOfMappings = new HashMap<>(); + this.customTopicMappings.entrySet() + .stream() + .map((e) -> copyOfMappings.put(this.fromMessageTypeMapKey(e.getKey()), e.getValue())); + return copyOfMappings; + } + + /** + * Gets the currently registered custom mapping for the specified message type. + * @param messageType the message type + * @return optional custom topic registered for the message type + */ + public Optional getCustomTopicMapping(Class messageType) { + return Optional.ofNullable(this.customTopicMappings.get(this.toMessageTypeMapKey(messageType))); } @Override @@ -141,7 +158,7 @@ public class DefaultTopicResolver implements TopicResolver, BeanFactoryAware { return Resolved.failed("Topic must be specified when the message is null"); } // Check for custom topic mapping - String topic = this.customTopicMappings.get(messageType); + String topic = this.customTopicMappings.get(this.toMessageTypeMapKey(messageType)); // If no custom topic mapping found, look for @PulsarMessage (if enabled) if (this.usePulsarMessageAnnotations && topic == null) { @@ -174,6 +191,19 @@ public class DefaultTopicResolver implements TopicResolver, BeanFactoryAware { .orElseThrow(() -> "Failed to resolve topic expression: %s".formatted(v)); } + private Class fromMessageTypeMapKey(String messageTypeKey) { + try { + return Class.forName(messageTypeKey); + } + catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + private String toMessageTypeMapKey(Class messageType) { + return messageType.getName(); + } + @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) { diff --git a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultSchemaResolverTests.java b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultSchemaResolverTests.java index 02bef1bf..794293b5 100644 --- a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultSchemaResolverTests.java +++ b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultSchemaResolverTests.java @@ -79,6 +79,7 @@ class DefaultSchemaResolverTests { @Nested class CustomSchemaMappingsAPI { + @SuppressWarnings("removal") @Test void noMappingsByDefault() { assertThat(resolver.getCustomSchemaMappings()).asInstanceOf(InstanceOfAssertFactories.MAP).isEmpty(); @@ -88,14 +89,13 @@ class DefaultSchemaResolverTests { void addMappings() { Schema previouslyMappedSchema = resolver.addCustomSchemaMapping(Foo.class, Schema.STRING); assertThat(previouslyMappedSchema).isNull(); - assertThat(resolver.getCustomSchemaMappings()).asInstanceOf(InstanceOfAssertFactories.MAP) - .containsEntry(Foo.class, Schema.STRING); + assertThat(resolver.getCustomSchemaMapping(Foo.class)).hasValue(Schema.STRING); previouslyMappedSchema = resolver.addCustomSchemaMapping(Foo.class, Schema.BOOL); assertThat(previouslyMappedSchema).isEqualTo(Schema.STRING); - assertThat(resolver.getCustomSchemaMappings()).asInstanceOf(InstanceOfAssertFactories.MAP) - .containsEntry(Foo.class, Schema.BOOL); + assertThat(resolver.getCustomSchemaMapping(Foo.class)).hasValue(Schema.BOOL); } + @SuppressWarnings("removal") @Test void removeMappings() { Schema previouslyMappedSchema = resolver.removeCustomMapping(Foo.class); @@ -423,10 +423,11 @@ class DefaultSchemaResolverTests { .endsWith(JsonMsgType.class.getSimpleName()); // verify added to custom mappings - assertThat(resolver.getCustomSchemaMappings().get(JsonMsgType.class)).isSameAs(resolvedSchema); + assertThat(resolver.getCustomSchemaMapping(JsonMsgType.class)) + .hasValueSatisfying((v) -> assertThat(v).isSameAs(resolvedSchema)); // verify subsequent calls skip resolution again - assertThat(resolver.resolveSchema(JsonMsgType.class, false).orElseThrow()).isSameAs(resolvedSchema); + assertThat(resolver.resolveSchema(JsonMsgType.class, false).orElseThrow()).isEqualTo(resolvedSchema); verify(resolver, times(1)).getAnnotatedSchemaType(JsonMsgType.class); } diff --git a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java index 02466039..704204a9 100644 --- a/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java +++ b/spring-pulsar/src/test/java/org/springframework/pulsar/core/DefaultTopicResolverTests.java @@ -166,7 +166,7 @@ class DefaultTopicResolverTests { assertThat(resolver.resolveTopic(null, Baz.class, () -> defaultTopic).value().orElse(null)) .isEqualTo(bazTopic); // verify added to custom mappings - assertThat(resolver.getCustomTopicMappings().get(Baz.class)).isEqualTo(bazTopic); + assertThat(resolver.getCustomTopicMapping(Baz.class)).hasValue(bazTopic); // verify subsequent calls skip resolution again assertThat(resolver.resolveTopic(null, Baz.class, () -> defaultTopic).value().orElse(null)) .isEqualTo(bazTopic); @@ -242,6 +242,7 @@ class DefaultTopicResolverTests { resolver = new DefaultTopicResolver(); } + @SuppressWarnings("removal") @Test void noMappingsByDefault() { resolver = new DefaultTopicResolver(); @@ -254,14 +255,13 @@ class DefaultTopicResolverTests { String topic2 = "bar-topic"; String previouslyMappedTopic = resolver.addCustomTopicMapping(Foo.class, topic1); assertThat(previouslyMappedTopic).isNull(); - assertThat(resolver.getCustomTopicMappings()).asInstanceOf(InstanceOfAssertFactories.MAP) - .containsEntry(Foo.class, topic1); + assertThat(resolver.getCustomTopicMapping(Foo.class)).hasValue(topic1); previouslyMappedTopic = resolver.addCustomTopicMapping(Foo.class, topic2); assertThat(previouslyMappedTopic).isEqualTo(topic1); - assertThat(resolver.getCustomTopicMappings()).asInstanceOf(InstanceOfAssertFactories.MAP) - .containsEntry(Foo.class, topic2); + assertThat(resolver.getCustomTopicMapping(Foo.class)).hasValue(topic2); } + @SuppressWarnings("removal") @Test void removeMappings() { String previouslyMappedTopic = resolver.removeCustomMapping(Foo.class);