diff --git a/pom.xml b/pom.xml index 9be85dfb76..4f0971f07f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 2.5.0-SNAPSHOT + 2.5.0-DATACMNS-1832 Spring Data Core 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 11147d11a1..28f1501e44 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 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.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; @@ -54,6 +57,7 @@ * @author Oliver Gierke * @author Jens Schauder * @author Mark Paluch + * @author Christoph Strobl */ public class RepositoryConfigurationDelegate { @@ -143,6 +147,12 @@ public List registerRepositoriesIn(BeanDefinitionRegist configurationSource.getBasePackages().stream().collect(Collectors.joining(", ")))); } + ApplicationStartup startup = getStartup(registry); + StartupStep repoScan = startup.start("spring.data.repository.scanning"); + repoScan.tag("dataModule", extension.getModuleName()); + repoScan.tag("basePackages", + () -> configurationSource.getBasePackages().stream().collect(Collectors.joining(", "))); + watch.start(); Collection> configurations = extension @@ -184,6 +194,9 @@ public List registerRepositoriesIn(BeanDefinitionRegist 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 +221,6 @@ private static void potentiallyLazifyRepositories(Map getArgumentConverter() { * 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 @@ private static Method findMethod(InvokedMethod invokedMethod, MethodLookup looku 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 4920ae0eb7..96768c5796 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.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,54 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra Assert.notNull(repositoryInterface, "Repository interface must not be null!"); Assert.notNull(fragments, "RepositoryFragments must not be null!"); + ApplicationStartup applicationStartup = getStartup(); + + StartupStep repositoryInit = onEvent(applicationStartup, "spring.data.repository.init", repositoryInterface); + + repositoryBaseClass.ifPresent(it -> repositoryInit.tag("baseClass", it.getName())); + + StartupStep repositoryMetadataStep = onEvent(applicationStartup, "spring.data.repository.metadata", + repositoryInterface); RepositoryMetadata metadata = getRepositoryMetadata(repositoryInterface); + repositoryMetadataStep.end(); + + StartupStep repositoryCompositionStep = onEvent(applicationStartup, "spring.data.repository.composition", + repositoryInterface); + repositoryCompositionStep.tag("fragment.count", String.valueOf(fragments.size())); + RepositoryComposition composition = getRepositoryComposition(metadata, fragments); RepositoryInformation information = getRepositoryInformation(metadata, composition); + repositoryCompositionStep.tag("fragments", () -> { + + StringBuilder fragmentsTag = new StringBuilder(); + + for (RepositoryFragment fragment : composition.getFragments()) { + + if (fragmentsTag.length() > 0) { + fragmentsTag.append(";"); + } + + fragmentsTag.append(fragment.getSignatureContributor().getName()); + fragmentsTag.append(fragment.getImplementation().map(it -> ":" + it.getClass().getName()).orElse("")); + } + + return fragmentsTag.toString(); + }); + + repositoryCompositionStep.end(); + validate(information, composition); + StartupStep repositoryTargetStep = onEvent(applicationStartup, "spring.data.repository.target", + repositoryInterface); Object target = getTargetRepository(information); + repositoryTargetStep.tag("target", target.getClass().getName()); + repositoryTargetStep.end(); + // Create proxy + StartupStep repositoryProxyStep = onEvent(applicationStartup, "spring.data.repository.proxy", repositoryInterface); ProxyFactory result = new ProxyFactory(); result.setTarget(target); result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class); @@ -291,7 +333,19 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra result.addAdvisor(ExposeInvocationInterceptor.ADVISOR); - postProcessors.forEach(processor -> processor.postProcess(result, information)); + if (!postProcessors.isEmpty()) { + StartupStep repositoryPostprocessorsStep = onEvent(applicationStartup, "spring.data.repository.postprocessors", + repositoryInterface); + postProcessors.forEach(processor -> { + + StartupStep singlePostProcessor = onEvent(applicationStartup, "spring.data.repository.postprocessor", + repositoryInterface); + singlePostProcessor.tag("type", processor.getClass().getName()); + processor.postProcess(result, information); + singlePostProcessor.end(); + }); + repositoryPostprocessorsStep.end(); + } if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) { result.addAdvice(new DefaultMethodInvokingMethodInterceptor()); @@ -303,14 +357,17 @@ public T getRepository(Class repositoryInterface, RepositoryFragments fra result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy, namedQueries, queryPostProcessors, methodInvocationListeners)); - composition = composition.append(RepositoryFragment.implemented(target)); - result.addAdvice(new ImplementationMethodExecutionInterceptor(information, composition, methodInvocationListeners)); + RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target)); + result.addAdvice( + new ImplementationMethodExecutionInterceptor(information, compositionToUse, 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; @@ -497,6 +554,25 @@ protected final R getTargetRepositoryViaReflection(Class baseClass, Objec baseClass, Arrays.stream(constructorArguments).map(Object::getClass).collect(Collectors.toList())))); } + private 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; + } + } + + private StartupStep onEvent(ApplicationStartup applicationStartup, String name, Class repositoryInterface) { + + StartupStep step = applicationStartup.start(name); + return step.tag("repository", repositoryInterface.getName()); + } + /** * Method interceptor that calls methods on the {@link RepositoryComposition}. * 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 b221fc430b..47881af851 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 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.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 @@ private static ListableBeanFactory assertLazyRepositoryBeanSetup(Class config 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 e7d3ffba74..65533ae9a7 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 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.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 @@ * 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 DummyRepositoryFactory(Object repository) { 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 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata : 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 353338d32e..3eeb54316d 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.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.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,21 @@ void considersNullabilityForKotlinInterfaceProperties() { 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"); + } + private ConvertingRepository prepareConvertingRepository(final Object expectedValue) { when(factory.queryOne.execute(any(Object[].class))).then(invocation -> {