diff --git a/pom.xml b/pom.xml index 2f40cd66af..1f4fef420a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 3.0.0-SNAPSHOT + 3.0.0-GH-2634-SNAPSHOT Spring Data Core Core Spring concepts underpinning every Spring Data module. diff --git a/src/main/java/org/springframework/data/domain/ManagedTypes.java b/src/main/java/org/springframework/data/domain/ManagedTypes.java new file mode 100644 index 0000000000..c144eb3f85 --- /dev/null +++ b/src/main/java/org/springframework/data/domain/ManagedTypes.java @@ -0,0 +1,129 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.data.util.Lazy; + +/** + * Types managed by a Spring Data implementation. Used to predefine a set of know entities that might need processing + * during the Spring container, Spring Data Repository initialization phase. + * + * @author Christoph Strobl + * @author John Blum + * @see java.lang.FunctionalInterface + * @since 3.0 + */ +@FunctionalInterface +public interface ManagedTypes { + + /** + * Factory method used to construct a new instance of {@link ManagedTypes} containing no {@link Class types}. + * + * @return an empty {@link ManagedTypes} instance. + * @see java.util.Collections#emptySet() + * @see #fromIterable(Iterable) + */ + static ManagedTypes empty() { + return fromIterable(Collections.emptySet()); + } + + /** + * Factory method used to construct {@link ManagedTypes} from the given, required {@link Iterable} of {@link Class + * types}. + * + * @param types {@link Iterable} of {@link Class types} used to initialize the {@link ManagedTypes}; must not be + * {@literal null}. + * @return new instance of {@link ManagedTypes} initialized the given, required {@link Iterable} of {@link Class + * types}. + * @see java.lang.Iterable + * @see #fromStream(Stream) + * @see #fromSupplier(Supplier) + */ + static ManagedTypes fromIterable(Iterable> types) { + return types::forEach; + } + + /** + * Factory method used to construct {@link ManagedTypes} from the given, required {@link Stream} of {@link Class + * types}. + * + * @param types {@link Stream} of {@link Class types} used to initialize the {@link ManagedTypes}; must not be + * {@literal null}. + * @return new instance of {@link ManagedTypes} initialized the given, required {@link Stream} of {@link Class types}. + * @see java.util.stream.Stream + * @see #fromIterable(Iterable) + * @see #fromSupplier(Supplier) + */ + static ManagedTypes fromStream(Stream> types) { + return types::forEach; + } + + /** + * Factory method used to construct {@link ManagedTypes} from the given, required {@link Supplier} of an + * {@link Iterable} of {@link Class types}. + * + * @param dataProvider {@link Supplier} of an {@link Iterable} of {@link Class types} used to lazily initialize the + * {@link ManagedTypes}; must not be {@literal null}. + * @return new instance of {@link ManagedTypes} initialized the given, required {@link Supplier} of an + * {@link Iterable} of {@link Class types}. + * @see java.util.function.Supplier + * @see java.lang.Iterable + * @see #fromIterable(Iterable) + * @see #fromStream(Stream) + */ + static ManagedTypes fromSupplier(Supplier>> dataProvider) { + + return new ManagedTypes() { + + final Lazy>> lazyProvider = Lazy.of(dataProvider); + + @Override + public void forEach(Consumer> action) { + lazyProvider.get().forEach(action); + } + }; + } + + /** + * Applies the given {@link Consumer action} to each of the {@link Class types} contained in this {@link ManagedTypes} + * instance. + * + * @param action {@link Consumer} defining the action to perform on the {@link Class types} contained in this + * {@link ManagedTypes} instance; must not be {@literal null}. + * @see java.util.function.Consumer + */ + void forEach(Consumer> action); + + /** + * Returns all the {@link ManagedTypes} in a {@link List}. + * + * @return these {@link ManagedTypes} in a {@link List}; never {@literal null}. + * @see java.util.List + */ + default List> toList() { + + List> list = new ArrayList<>(100); + forEach(list::add); + return list; + } +} diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index 7570a8f031..93a1902ff0 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -42,6 +42,7 @@ import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.core.KotlinDetector; import org.springframework.core.NativeDetector; +import org.springframework.data.domain.ManagedTypes; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; @@ -101,7 +102,8 @@ public abstract class AbstractMappingContext> initialEntitySet = new HashSet<>(); + private ManagedTypes managedTypes = ManagedTypes.empty(); + private boolean strict = false; private SimpleTypeHolder simpleTypeHolder = SimpleTypeHolder.DEFAULT; @@ -141,9 +143,21 @@ public void setApplicationContext(ApplicationContext applicationContext) throws * Sets the {@link Set} of types to populate the context initially. * * @param initialEntitySet + * @see #setManagedTypes(ManagedTypes) + * */ public void setInitialEntitySet(Set> initialEntitySet) { - this.initialEntitySet = initialEntitySet; + setManagedTypes(ManagedTypes.fromIterable(initialEntitySet)); + } + + /** + * Sets the types to populate the context initially. + * + * @param managedTypes must not be {@literal null}. Use {@link ManagedTypes#empty()} instead; + * @since 3.0 + */ + public void setManagedTypes(ManagedTypes managedTypes) { + this.managedTypes = managedTypes; } /** @@ -467,7 +481,7 @@ public void afterPropertiesSet() { * context. */ public void initialize() { - initialEntitySet.forEach(this::addPersistentEntity); + managedTypes.forEach(this::addPersistentEntity); } /** diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java index 0b133286eb..2c7061003a 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryBeanDefinitionBuilder.java @@ -170,7 +170,7 @@ private String registerRepositoryFragments(RepositoryConfiguration configurat fragmentsBuilder.addConstructorArgValue(fragmentBeanNames); String fragmentsBeanName = BeanDefinitionReaderUtils - .uniqueBeanName(String.format("%s.%s.fragments", extension.getModuleName().toLowerCase(Locale.ROOT), + .uniqueBeanName(String.format("%s.%s.fragments", extension.getModulePrefix(), ClassUtils.getShortName(configuration.getRepositoryInterface())), registry); registry.registerBeanDefinition(fragmentsBeanName, fragmentsBuilder.getBeanDefinition()); return fragmentsBeanName; diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java index 561862cd4a..ee964cb4e3 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationDelegate.java @@ -25,7 +25,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.parsing.BeanComponentDefinition; diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java index 07b29ce47b..ccbd8f4bc2 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtension.java @@ -15,18 +15,22 @@ */ package org.springframework.data.repository.config; +import java.lang.annotation.Annotation; import java.util.Collection; +import java.util.Collections; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; /** * SPI to implement store specific extension to the repository bean definition registration process. * * @see RepositoryConfigurationExtensionSupport * @author Oliver Gierke + * @author Christoph Strobl */ public interface RepositoryConfigurationExtension { @@ -35,7 +39,18 @@ public interface RepositoryConfigurationExtension { * * @return */ - String getModuleName(); + default String getModuleName() { + return StringUtils.capitalize(getModulePrefix()); + } + + /** + * Returns the prefix of the module to be used to create the default location for Spring Data named queries and module + * specific bean definitions. + * + * @return must not be {@literal null}. + * @since 3.0 + */ + String getModulePrefix(); /** * Returns all {@link RepositoryConfiguration}s obtained through the given {@link RepositoryConfigurationSource}. diff --git a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java index 567bbc8db5..ade7871fff 100644 --- a/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java +++ b/src/main/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupport.java @@ -60,11 +60,6 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit private boolean noMultiStoreSupport = false; - @Override - public String getModuleName() { - return StringUtils.capitalize(getModulePrefix()); - } - public Collection> getRepositoryConfigurations( T configSource, ResourceLoader loader) { return getRepositoryConfigurations(configSource, loader, false); @@ -109,13 +104,6 @@ public String getDefaultNamedQueryLocation() { public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) {} - /** - * Returns the prefix of the module to be used to create the default location for Spring Data named queries. - * - * @return must not be {@literal null}. - */ - protected abstract String getModulePrefix(); - public void postProcess(BeanDefinitionBuilder builder, RepositoryConfigurationSource source) {} public void postProcess(BeanDefinitionBuilder builder, AnnotationRepositoryConfigurationSource config) {} diff --git a/src/test/java/org/springframework/data/classloadersupport/HidingClassLoader.java b/src/test/java/org/springframework/data/classloadersupport/HidingClassLoader.java index d79072c792..215c92f579 100644 --- a/src/test/java/org/springframework/data/classloadersupport/HidingClassLoader.java +++ b/src/test/java/org/springframework/data/classloadersupport/HidingClassLoader.java @@ -34,6 +34,7 @@ * * @author Jens Schauder * @author Oliver Gierke + * @author Christoph Strobl */ public class HidingClassLoader extends ShadowingClassLoader { @@ -61,11 +62,21 @@ public static HidingClassLoader hide(Class... packages) { .collect(Collectors.toList())); } + public static HidingClassLoader hideTypes(Class... types) { + + Assert.notNull(types, "Types must not be null!"); + + return new HidingClassLoader(Arrays.stream(types)// + .map(it -> it.getName())// + .collect(Collectors.toList())); + } + @Override public Class loadClass(String name) throws ClassNotFoundException { - checkIfHidden(name); - return super.loadClass(name); + Class loaded = super.loadClass(name); + checkIfHidden(loaded); + return loaded; } @Override @@ -76,13 +87,14 @@ protected boolean isEligibleForShadowing(String className) { @Override protected Class findClass(String name) throws ClassNotFoundException { - checkIfHidden(name); - return super.findClass(name); + Class loaded = super.findClass(name); + checkIfHidden(loaded); + return loaded; } - private void checkIfHidden(String name) throws ClassNotFoundException { + private void checkIfHidden(Class type) throws ClassNotFoundException { - if (hidden.stream().anyMatch(it -> name.startsWith(it))) { + if (hidden.stream().anyMatch(it -> type.getName().startsWith(it))) { throw new ClassNotFoundException(); } } diff --git a/src/test/java/org/springframework/data/domain/ManagedTypesUnitTests.java b/src/test/java/org/springframework/data/domain/ManagedTypesUnitTests.java new file mode 100644 index 0000000000..eb0be6cfc6 --- /dev/null +++ b/src/test/java/org/springframework/data/domain/ManagedTypesUnitTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MockitoExtension.class) +class ManagedTypesUnitTests { + + @Mock Consumer> action; + + @Test // GH-2634 + void emptyNeverCallsAction() { + + ManagedTypes.empty().forEach(action); + verify(action, never()).accept(any()); + } + + @Test // GH-2634 + void supplierBasedManagedTypesAreEvaluatedLazily() { + + Supplier>> typesSupplier = spy(new Supplier>>() { + @Override + public Iterable> get() { + return Collections.singleton(Object.class); + } + }); + + ManagedTypes managedTypes = ManagedTypes.fromSupplier(typesSupplier); + + managedTypes.forEach(action); // 1st invocation + verify(action).accept(any()); + verify(typesSupplier).get(); + + managedTypes.forEach(action); // 2nd invocation + verify(action, times(2)).accept(any()); + verify(typesSupplier, times(1)).get(); + } + + @Test // GH-2634 + void toListOnEmptyReturnsEmptyList() { + assertThat(ManagedTypes.empty().toList()).isEmpty(); + } + + @Test // GH-2634 + void toListContainsEntriesInOrder() { + assertThat(ManagedTypes.fromIterable(Arrays.asList(Object.class, List.class)).toList()).containsExactly(Object.class, + List.class); + } +} diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java index 861e5226b6..52c81e64e7 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryBeanDefinitionRegistrarSupportUnitTests.java @@ -187,7 +187,7 @@ public String getRepositoryFactoryBeanClassName() { } @Override - protected String getModulePrefix() { + public String getModulePrefix() { return "commons"; } } diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java index db57401cd8..3a9895bbf7 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java @@ -24,7 +24,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; - import org.springframework.aop.framework.Advised; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.context.annotation.AnnotationConfigApplicationContext; diff --git a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java index dca0ff7485..b6b9a833bd 100755 --- a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationExtensionSupportUnitTests.java @@ -100,7 +100,7 @@ void doesNotClaimEntityIfNoIdentifyingAnnotationsAreExposed() { static class SampleRepositoryConfigurationExtension extends RepositoryConfigurationExtensionSupport { @Override - protected String getModulePrefix() { + public String getModulePrefix() { return "core"; } @@ -123,7 +123,7 @@ protected Collection> getIdentifyingTypes() { static class NonIdentifyingConfigurationExtension extends RepositoryConfigurationExtensionSupport { @Override - protected String getModulePrefix() { + public String getModulePrefix() { return "non-identifying"; }