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 11147d11a..b290deea2 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,7 @@ import java.util.stream.Collectors; 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; import org.springframework.beans.factory.support.AbstractBeanDefinition; @@ -34,12 +34,15 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.log.LogMessage; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -143,6 +146,11 @@ public class RepositoryConfigurationDelegate { configurationSource.getBasePackages().stream().collect(Collectors.joining(", ")))); } + ApplicationStartup startup = getStartup(registry); + StartupStep repoScan = startup.start("spring.data.repository.scanning"); + repoScan.tag("data-module", extension.getModuleName()); + repoScan.tag("packages", configurationSource.getBasePackages().stream().collect(Collectors.joining(", "))); + watch.start(); Collection> configurations = extension @@ -184,6 +192,9 @@ public class RepositoryConfigurationDelegate { watch.stop(); + repoScan.tag("repository.count", Integer.toString(configurations.size())); + repoScan.end(); + if (logger.isInfoEnabled()) { logger.info(LogMessage.format("Finished Spring Data repository scanning in %s ms. Found %s %s repository interfaces.", // watch.getLastTaskTimeMillis(), configurations.size(), extension.getModuleName())); @@ -208,7 +219,6 @@ public class RepositoryConfigurationDelegate { } DefaultListableBeanFactory beanFactory = DefaultListableBeanFactory.class.cast(registry); - AutowireCandidateResolver resolver = beanFactory.getAutowireCandidateResolver(); if (!Arrays.asList(ContextAnnotationAutowireCandidateResolver.class, LazyRepositoryInjectionPointResolver.class) @@ -253,6 +263,23 @@ public class RepositoryConfigurationDelegate { return multipleModulesFound; } + ApplicationStartup getStartup(BeanDefinitionRegistry registry) { + + if(ConfigurableBeanFactory.class.isInstance(registry)) { + return ((ConfigurableBeanFactory)registry).getApplicationStartup(); + } + + if (DefaultListableBeanFactory.class.isInstance(registry)) { + return ((DefaultListableBeanFactory)registry).getBeanProvider(ApplicationStartup.class).getIfAvailable(() -> ApplicationStartup.DEFAULT); + } + + if(GenericApplicationContext.class.isInstance(registry)) { + return ((GenericApplicationContext)registry).getDefaultListableBeanFactory().getApplicationStartup(); + } + + return ApplicationStartup.DEFAULT; + } + /** * Customer {@link ContextAnnotationAutowireCandidateResolver} that also considers all injection points for lazy * repositories lazy. diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java index 1e5027c7a..388791a70 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryComposition.java @@ -364,6 +364,7 @@ public class RepositoryComposition { * Value object representing an ordered list of {@link RepositoryFragment fragments}. * * @author Mark Paluch + * @author Christoph Strobl */ public static class RepositoryFragments implements Streamable> { @@ -550,6 +551,16 @@ public class RepositoryComposition { return null; } + /** + * Returns the number of {@link RepositoryFragment fragments} available. + * + * @return the number of {@link RepositoryFragment fragments}. + * @since 2.5 + */ + public int size() { + return fragments.size(); + } + /* * (non-Javadoc) * @see java.lang.Object#toString() diff --git a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java index 4920ae0eb..4f68cdd64 100644 --- a/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java +++ b/src/main/java/org/springframework/data/repository/core/support/RepositoryFactorySupport.java @@ -35,9 +35,12 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.core.log.LogMessage; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; import org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -272,15 +275,30 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, Assert.notNull(repositoryInterface, "Repository interface must not be null!"); Assert.notNull(fragments, "RepositoryFragments must not be null!"); + ApplicationStartup applicationStartup = getStartup(); + + StartupStep repositoryInit = applicationStartup.start("spring.data.repository.init"); + repositoryInit.tag("repository", () -> repositoryInterface.getName()); + + StartupStep repositoryMetadataStep = applicationStartup.start("spring.data.repository.metadata"); RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface); + repositoryMetadataStep.end(); + + StartupStep repositoryCompositionStep = applicationStartup.start("spring.data.repository.composition"); + repositoryCompositionStep.tag("fragment.count", () -> String.valueOf(fragments.size())); + RepositoryComposition composition = getRepositoryComposition(metadata, fragments); RepositoryInformation information = getRepositoryInformation(metadata, composition); + repositoryCompositionStep.end(); validate(information, composition); + StartupStep repositoryTargetStep = applicationStartup.start("spring.data.repository.target"); Object target = getTargetRepository(information); + repositoryTargetStep.end(); // Create proxy + StartupStep repositoryProxyStep = applicationStartup.start("spring.data.repository.proxy"); ProxyFactory result = new ProxyFactory(); result.setTarget(target); result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class); @@ -291,31 +309,56 @@ public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); - postProcessors.forEach(processor -> processor.postProcess(result, information)); + StartupStep repositoryPostprocessorsStep = applicationStartup.start("spring.data.repository.postprocessors"); + postProcessors.forEach(processor -> { + + StartupStep singlePostProcessor = applicationStartup.start("spring.data.repository.postprocessor"); + singlePostProcessor.tag("type", processor.getClass().getName()); + processor.postProcess(result, information); + singlePostProcessor.end(); + }); + repositoryPostprocessorsStep.end(); if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) { result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); } + StartupStep queryExecutorsStep = applicationStartup.start("spring.data.repository.queryexecutors"); ProjectionFactory projectionFactory = getProjectionFactory(classLoader, beanFactory); Optional queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey, evaluationContextProvider); result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy, namedQueries, queryPostProcessors, methodInvocationListeners)); + queryExecutorsStep.end(); composition = composition.append(RepositoryFragment.implemented(target)); result.addAdvice(new ImplementationMethodExecutionInterceptor(information, composition, methodInvocationListeners)); T repository = (T) result.getProxy(classLoader); + repositoryProxyStep.end(); + repositoryInit.end(); if (logger.isDebugEnabled()) { - logger - .debug(LogMessage.format("Finished creation of repository instance for {}.", repositoryInterface.getName())); + logger.debug(LogMessage.format("Finished creation of repository instance for {}.", + repositoryInterface.getName())); } return repository; } + ApplicationStartup getStartup() { + + try { + + ApplicationStartup applicationStartup = beanFactory != null ? beanFactory.getBean(ApplicationStartup.class) + : ApplicationStartup.DEFAULT; + + return applicationStartup != null ? applicationStartup : ApplicationStartup.DEFAULT; + } catch (NoSuchBeanDefinitionException e) { + return ApplicationStartup.DEFAULT; + } + } + /** * Returns the {@link ProjectionFactory} to be used with the repository instances created. * 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 b221fc430..47881af85 100644 --- a/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java +++ b/src/test/java/org/springframework/data/repository/config/RepositoryConfigurationDelegateUnitTests.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; @@ -35,6 +36,8 @@ import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.FilterType; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.data.repository.config.RepositoryConfigurationDelegate.LazyRepositoryInjectionPointResolver; import org.springframework.data.repository.sample.AddressRepository; @@ -107,6 +110,26 @@ class RepositoryConfigurationDelegateUnitTests { return context.getDefaultListableBeanFactory(); } + @Test // DATACMNS-1832 + void writesRepositoryScanningMetrics() { + + ApplicationStartup startup = Mockito.spy(ApplicationStartup.DEFAULT); + + StandardEnvironment environment = new StandardEnvironment(); + GenericApplicationContext context = new GenericApplicationContext(); + context.setApplicationStartup(startup); + + RepositoryConfigurationSource configSource = new AnnotationRepositoryConfigurationSource( + new StandardAnnotationMetadata(TestConfig.class, true), EnableRepositories.class, context, environment, + context.getDefaultListableBeanFactory()); + + RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configSource, context, environment); + + delegate.registerRepositoriesIn(context, extension); + + Mockito.verify(startup).start("spring.data.repository.scanning"); + } + @EnableRepositories(basePackageClasses = ProductRepository.class) static class TestConfig {} diff --git a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java index e7d3ffba7..65533ae9a 100644 --- a/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java +++ b/src/test/java/org/springframework/data/repository/core/support/DummyRepositoryFactory.java @@ -19,8 +19,13 @@ import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.util.Optional; +import java.util.function.Supplier; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.core.metrics.ApplicationStartup; +import org.springframework.core.metrics.StartupStep; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.core.EntityInformation; @@ -28,9 +33,9 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; /** @@ -38,6 +43,7 @@ import org.springframework.data.repository.query.RepositoryQuery; * cases. * * @author Oliver Gierke + * @author Christoph Strobl */ public class DummyRepositoryFactory extends RepositoryFactorySupport { @@ -45,6 +51,8 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport { public final RepositoryQuery queryTwo = mock(RepositoryQuery.class); public final QueryLookupStrategy strategy = mock(QueryLookupStrategy.class); + private final ApplicationStartup applicationStartup; + @SuppressWarnings("unchecked") private final QuerydslPredicateExecutor querydsl = mock( QuerydslPredicateExecutor.class); private final Object repository; @@ -55,6 +63,16 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport { when(strategy.resolveQuery(Mockito.any(Method.class), Mockito.any(RepositoryMetadata.class), Mockito.any(ProjectionFactory.class), Mockito.any(NamedQueries.class))).thenReturn(queryOne); + + this.applicationStartup = mock(ApplicationStartup.class); + StartupStep startupStep = mock(StartupStep.class); + when(applicationStartup.start(anyString())).thenReturn(startupStep); + when(startupStep.tag(anyString(), anyString())).thenReturn(startupStep); + when(startupStep.tag(anyString(), ArgumentMatchers.> any())).thenReturn(startupStep); + + BeanFactory beanFactory = Mockito.mock(BeanFactory.class); + when(beanFactory.getBean(ApplicationStartup.class)).thenReturn(applicationStartup); + setBeanFactory(beanFactory); } /* @@ -109,10 +127,15 @@ public class DummyRepositoryFactory extends RepositoryFactorySupport { : fragments; } + ApplicationStartup getApplicationStartup() { + return this.applicationStartup; + } + /** * @author Mark Paluch */ public interface MyRepositoryQuery extends RepositoryQuery { } + } diff --git a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java index 353338d32..d41eb974c 100755 --- a/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java +++ b/src/test/java/org/springframework/data/repository/core/support/RepositoryFactorySupportUnitTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -42,6 +43,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.metrics.ApplicationStartup; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -405,6 +407,22 @@ class RepositoryFactorySupportUnitTests { assertThatThrownBy(repository::getFindRouteQuery).isInstanceOf(EmptyResultDataAccessException.class); } + @Test // DATACMNS-1832 + void callsApplicationStartupOnRepositoryInitialization() { + + factory.getRepository(ObjectRepository.class, backingRepo); + + ApplicationStartup startup = factory.getApplicationStartup(); + + InOrder orderedInvocation = Mockito.inOrder(startup); + orderedInvocation.verify(startup).start("spring.data.repository.init"); + orderedInvocation.verify(startup).start("spring.data.repository.metadata"); + orderedInvocation.verify(startup).start("spring.data.repository.composition"); + orderedInvocation.verify(startup).start("spring.data.repository.target"); + orderedInvocation.verify(startup).start("spring.data.repository.proxy"); + orderedInvocation.verify(startup).start("spring.data.repository.postprocessors"); + } + private ConvertingRepository prepareConvertingRepository(final Object expectedValue) { when(factory.queryOne.execute(any(Object[].class))).then(invocation -> {