From 884dfb66b496def7ef35dfe12f9f348980025e2d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 2 Oct 2025 11:39:28 +0200 Subject: [PATCH 1/3] Explore reduced configuration extension. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do not register a MappingContext through the extension, instead expose a MappingContext through KeyValueAdapter so implementations can bring their own mapping context. They can also decide where to obtain the mapping context from and whether to expose it as bean at all. This is useful for a minimal configuration and removes the need to detect whether there is already a mapping context available. Detection of mapping context (or even converters) can go either into the adapter or somewhere else as detecting bean registrations depends on configuration ordering. Any later bean registrations might be not visible to our extension and so we had always the drawback of requiring any customizations being colocated within the config class that uses the Enable…Repositories annotation. --- .../data/keyvalue/core/KeyValueAdapter.java | 11 +++++++++ .../data/keyvalue/core/KeyValueTemplate.java | 22 ++++------------- ...ValueRepositoryConfigurationExtension.java | 24 +------------------ .../data/map/MapKeyValueAdapter.java | 15 ++++++++++++ .../core/KeyValueTemplateUnitTests.java | 2 ++ ...onfigurationExtensionIntegrationTests.java | 3 ++- 6 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java b/src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java index e4848309..d63b1bdd 100644 --- a/src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java +++ b/src/main/java/org/springframework/data/keyvalue/core/KeyValueAdapter.java @@ -20,7 +20,10 @@ import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.CloseableIterator; /** @@ -32,6 +35,14 @@ */ public interface KeyValueAdapter extends DisposableBean { + /** + * Expose the {@link MappingContext} to obtain mapping metadata. + * + * @return the used mapping context. + * @since 4.0 + */ + MappingContext, ? extends KeyValuePersistentProperty> getMappingContext(); + /** * Add object with given id to keyspace. * diff --git a/src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java b/src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java index 3b33b821..04c415a5 100644 --- a/src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java +++ b/src/main/java/org/springframework/data/keyvalue/core/KeyValueTemplate.java @@ -70,38 +70,24 @@ public class KeyValueTemplate implements KeyValueOperations, ApplicationEventPub * @param adapter must not be {@literal null}. */ public KeyValueTemplate(KeyValueAdapter adapter) { - this(adapter, new KeyValueMappingContext<>()); + this(adapter, DefaultIdentifierGenerator.INSTANCE); } /** - * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} and {@link MappingContext}. + * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} and {@link IdentifierGenerator}. * * @param adapter must not be {@literal null}. - * @param mappingContext must not be {@literal null}. - */ - public KeyValueTemplate(KeyValueAdapter adapter, - MappingContext, ? extends KeyValuePersistentProperty> mappingContext) { - this(adapter, mappingContext, DefaultIdentifierGenerator.INSTANCE); - } - - /** - * Create new {@link KeyValueTemplate} using the given {@link KeyValueAdapter} and {@link MappingContext}. - * - * @param adapter must not be {@literal null}. - * @param mappingContext must not be {@literal null}. * @param identifierGenerator must not be {@literal null}. - * @since 2.4 + * @since 4.0 */ public KeyValueTemplate(KeyValueAdapter adapter, - MappingContext, ? extends KeyValuePersistentProperty> mappingContext, IdentifierGenerator identifierGenerator) { Assert.notNull(adapter, "Adapter must not be null"); - Assert.notNull(mappingContext, "MappingContext must not be null"); Assert.notNull(identifierGenerator, "IdentifierGenerator must not be null"); this.adapter = adapter; - this.mappingContext = mappingContext; + this.mappingContext = adapter.getMappingContext(); this.identifierGenerator = identifierGenerator; } diff --git a/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java index f276451b..71733950 100644 --- a/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/keyvalue/repository/config/KeyValueRepositoryConfigurationExtension.java @@ -21,6 +21,7 @@ import java.util.Optional; import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -28,7 +29,6 @@ import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery; import org.springframework.data.keyvalue.repository.query.SpelQueryCreator; @@ -47,7 +47,6 @@ */ public abstract class KeyValueRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { - protected static final String MAPPING_CONTEXT_BEAN_NAME = "keyValueMappingContext"; protected static final String KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE = "keyValueTemplateRef"; @Override @@ -78,7 +77,6 @@ public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfi builder.addPropertyReference("keyValueOperations", attributes.getString(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE)); builder.addPropertyValue("queryCreator", getQueryCreatorType(config)); builder.addPropertyValue("queryType", getQueryType(config)); - builder.addPropertyReference("mappingContext", getMappingContextBeanRef()); } /** @@ -128,15 +126,6 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf super.registerBeansForRoot(registry, configurationSource); - registerIfNotAlreadyRegistered(() -> { - - RootBeanDefinition mappingContext = new RootBeanDefinition(KeyValueMappingContext.class); - mappingContext.setSource(configurationSource.getSource()); - - return mappingContext; - - }, registry, getMappingContextBeanRef(), configurationSource); - Optional keyValueTemplateName = configurationSource.getAttribute(KEY_VALUE_TEMPLATE_BEAN_REF_ATTRIBUTE); // No custom template reference configured and no matching bean definition found @@ -174,15 +163,4 @@ public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConf */ protected abstract String getDefaultKeyValueTemplateRef(); - /** - * Returns the {@link org.springframework.data.mapping.context.MappingContext} bean name to potentially register a - * default mapping context bean if no bean is registered with the returned name. Defaults to - * {@link MAPPING_CONTEXT_BEAN_NAME}. - * - * @return the {@link org.springframework.data.mapping.context.MappingContext} bean name. Never {@literal null}. - * @since 2.0 - */ - protected String getMappingContextBeanRef() { - return MAPPING_CONTEXT_BEAN_NAME; - } } diff --git a/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java b/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java index 3018c104..e285a4c8 100644 --- a/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/map/MapKeyValueAdapter.java @@ -28,6 +28,9 @@ import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.QueryEngine; import org.springframework.data.keyvalue.core.SortAccessor; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -38,12 +41,14 @@ * @author Christoph Strobl * @author Derek Cochran * @author Marcel Overdijk + * @author Mark Paluch */ public class MapKeyValueAdapter extends AbstractKeyValueAdapter { @SuppressWarnings("rawtypes") // private final Class keySpaceMapType; private final Map> store; + private KeyValueMappingContext, ? extends KeyValuePersistentProperty> mappingContext = new KeyValueMappingContext<>(); /** * Create new {@link MapKeyValueAdapter} using {@link ConcurrentHashMap} as backing store type. @@ -143,6 +148,16 @@ private MapKeyValueAdapter(Map> store, Class, ? extends KeyValuePersistentProperty> getMappingContext() { + return mappingContext; + } + + public void setMappingContext( + KeyValueMappingContext, ? extends KeyValuePersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + @Override public @Nullable Object put(Object id, Object item, String keyspace) { diff --git a/src/test/java/org/springframework/data/keyvalue/core/KeyValueTemplateUnitTests.java b/src/test/java/org/springframework/data/keyvalue/core/KeyValueTemplateUnitTests.java index 22774b0b..cfe4b095 100644 --- a/src/test/java/org/springframework/data/keyvalue/core/KeyValueTemplateUnitTests.java +++ b/src/test/java/org/springframework/data/keyvalue/core/KeyValueTemplateUnitTests.java @@ -47,6 +47,7 @@ import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeGetEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeInsertEvent; import org.springframework.data.keyvalue.core.event.KeyValueEvent.BeforeUpdateEvent; +import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.keyvalue.core.query.KeyValueQuery; /** @@ -73,6 +74,7 @@ class KeyValueTemplateUnitTests { @BeforeEach void setUp() { + when(adapterMock.getMappingContext()).thenReturn(new KeyValueMappingContext()); this.template = new KeyValueTemplate(adapterMock); this.template.setApplicationEventPublisher(publisherMock); } diff --git a/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java b/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java index d4b5fabd..adffa321 100644 --- a/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java +++ b/src/test/java/org/springframework/data/map/repository/config/MapRepositoriesConfigurationExtensionIntegrationTests.java @@ -44,6 +44,7 @@ import org.springframework.data.keyvalue.core.QueryEngineFactory; import org.springframework.data.keyvalue.core.SortAccessor; import org.springframework.data.keyvalue.core.SpelQueryEngine; +import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; import org.springframework.data.keyvalue.core.query.KeyValueQuery; import org.springframework.data.keyvalue.repository.KeyValueRepository; import org.springframework.data.keyvalue.repository.query.PredicateQueryCreator; @@ -220,7 +221,7 @@ public KeyValueOperations mapKeyValueTemplate() { public KeyValueAdapter keyValueAdapter() { KeyValueAdapter mock = mock(KeyValueAdapter.class); - + when(mock.getMappingContext()).thenReturn(new KeyValueMappingContext()); when(mock.get(any(), anyString(), any())).thenThrow(new IllegalStateException("Mock")); return mock; From a7fab8a9c9b4fa3ca558890dc2b9b543076490f0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 2 Oct 2025 11:40:08 +0200 Subject: [PATCH 2/3] Polishing. --- .../config/EnableMapRepositories.java | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java b/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java index 482e180d..acf468f6 100644 --- a/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java +++ b/src/main/java/org/springframework/data/map/repository/config/EnableMapRepositories.java @@ -60,13 +60,25 @@ /** * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: - * {@code @EnableJpaRepositories("org.my.pkg")} instead of {@code @EnableJpaRepositories(basePackages="org.my.pkg")}. + * {@code @EnableMapRepositories("org.my.pkg")} instead of {@code @EnableMapRepositories(basePackages="org.my.pkg")}. */ String[] value() default {}; /** - * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this - * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + * Base packages to scan for annotated components. + *

+ * {@link #value} is an alias for (and mutually exclusive with) this attribute. + *

+ * Supports {@code ${…}} placeholders which are resolved against the {@link org.springframework.core.env.Environment + * Environment} as well as Ant-style package patterns — for example, {@code "org.example.**"}. + *

+ * Multiple packages or patterns may be specified, either separately or within a single {@code String} — for + * example, {@code {"org.example.config", "org.example.service.**"}} or + * {@code "org.example.config, org.example.service.**"}. + *

+ * Use {@link #basePackageClasses} for a type-safe alternative to String-based package names. + * + * @see org.springframework.context.ConfigurableApplicationContext#CONFIG_LOCATION_DELIMITERS */ String[] basePackages() default {}; From c3b6a2d17d0f82447cc48a6b45c5936f03ae164a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 2 Oct 2025 11:40:31 +0200 Subject: [PATCH 3/3] Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 4cc6241b..1958decf 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-keyvalue - 4.0.0-SNAPSHOT + 4.0.0-GH-363-SNAPSHOT Spring Data KeyValue