From 7d7f62f7030ccdc8bbe8898e03320f5f3352e803 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 17 Nov 2020 14:38:33 +0100 Subject: [PATCH] Add repository initialization metrics. This commit adds support for collecting data repository startup metric (repository scanning, repository initialization) using core framework ApplicationStartup and StartupStep. Collected metrics can be stored with Java Flight Recorder when using a FlightRecorderApplicationStartup. Closes #2247. Original pull request: #2273. --- .../RepositoryConfigurationDelegate.java | 31 +++++++++++- .../core/support/RepositoryComposition.java | 11 +++++ .../support/RepositoryFactorySupport.java | 49 +++++++++++++++++-- ...ositoryConfigurationDelegateUnitTests.java | 23 +++++++++ .../core/support/DummyRepositoryFactory.java | 25 +++++++++- .../RepositoryFactorySupportUnitTests.java | 18 +++++++ 6 files changed, 151 insertions(+), 6 deletions(-) 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 -> {