DATACMNS-1368 - Add support for deferred repository initialization.

Both the XML and annotation based configuration sources now support a bootstrap mode configuration property that allow to configure whether repositories are eagerly initialized unless declared as lazy, initialized in a deferred way (just before the application context finishs bootstrapping) or entirely lazy (upon first usage, i.e. a method invocation).

We now register a custom AutowireCandidateResolver that will consider all injection points of lazy repositories lazy too. This will prevent them to accidentally trigger downstream infrastructure initialization.
This commit is contained in:
Oliver Gierke
2018-08-09 08:15:11 +02:00
parent 60bbc59054
commit 5da6cf20a2
17 changed files with 728 additions and 6 deletions

View File

@@ -65,6 +65,7 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
private static final String REPOSITORY_FACTORY_BEAN_CLASS = "repositoryFactoryBeanClass";
private static final String REPOSITORY_BASE_CLASS = "repositoryBaseClass";
private static final String CONSIDER_NESTED_REPOSITORIES = "considerNestedRepositories";
private static final String BOOTSTRAP_MODE = "bootstrapMode";
private final AnnotationMetadata configMetadata;
private final AnnotationMetadata enableAnnotationMetadata;
@@ -341,6 +342,20 @@ public class AnnotationRepositoryConfigurationSource extends RepositoryConfigura
return hasExplicitFilters;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationSource#getBootstrapMode()
*/
@Override
public BootstrapMode getBootstrapMode() {
try {
return attributes.getEnum(BOOTSTRAP_MODE);
} catch (IllegalArgumentException o_O) {
return BootstrapMode.DEFAULT;
}
}
/**
* Safely reads the {@code pattern} attribute from the given {@link AnnotationAttributes} and returns an empty list if
* the attribute is not present.

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2018 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
*
* http://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.repository.config;
/**
* Enumeration to define in which way repositories are bootstrapped.
*
* @author Oliver Gierke
* @see RepositoryConfigurationSource#getBootstrapMode()
* @since 2.1
* @soundtrack Dave Matthews Band - She (Come Tomorrow)
*/
public enum BootstrapMode {
/**
* Repository proxies are instantiated eagerly, just like any other Spring bean, except explicitly marked as lazy.
* Thus, injection into repository clients will trigger initialization.
*/
DEFAULT,
/**
* Repository bean definitions are considered lazy and clients will get repository proxies injected that will
* initialize on first access. Repository initialization is triggered on application context bootstrap completion.
*/
DEFERRED,
/**
* Repository bean definitions are considered lazy, lazily inject and only initialized on first use, i.e. the
* application might have fully started without the repositories initialized.
*/
LAZY;
}

View File

@@ -170,7 +170,7 @@ public class DefaultRepositoryConfiguration<T extends RepositoryConfigurationSou
*/
@Override
public boolean isLazyInit() {
return definition.isLazyInit();
return definition.isLazyInit() || !configurationSource.getBootstrapMode().equals(BootstrapMode.DEFAULT);
}
/*

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2018 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
*
* http://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.repository.config;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.Ordered;
import org.springframework.data.repository.Repository;
/**
* {@link ApplicationListener} to trigger the initialization of Spring Data repositories right before the application
* context is started.
*
* @author Oliver Gierke
* @since 2.1
* @soundtrack Dave Matthews Band - Here On Out (Come Tomorrow)
*/
@Slf4j
@RequiredArgsConstructor
class DeferredRepositoryInitializationListener implements ApplicationListener<ContextRefreshedEvent>, Ordered {
private final @NonNull ListableBeanFactory beanFactory;
/*
* (non-Javadoc)
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
*/
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
LOG.debug("Triggering deferred initialization of Spring Data repositories…");
beanFactory.getBeansOfType(Repository.class);
}
/*
* (non-Javadoc)
* @see org.springframework.core.Ordered#getOrder()
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}

View File

@@ -101,6 +101,7 @@ class RepositoryBeanDefinitionBuilder {
builder.addConstructorArgValue(configuration.getRepositoryInterface());
builder.addPropertyValue("queryLookupStrategyKey", configuration.getQueryLookupStrategyKey());
builder.addPropertyValue("lazyInit", configuration.isLazyInit());
builder.setLazyInit(configuration.isLazyInit());
configuration.getRepositoryBaseClassName()//
.ifPresent(it -> builder.addPropertyValue("repositoryBaseClass", it));

View File

@@ -15,18 +15,26 @@
*/
package org.springframework.data.repository.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.AutowireCandidateResolver;
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.core.env.Environment;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.env.StandardEnvironment;
@@ -49,6 +57,7 @@ public class RepositoryConfigurationDelegate {
private static final String REPOSITORY_REGISTRATION = "Spring Data {} - Registering repository: {} - Interface: {} - Factory: {}";
private static final String MULTIPLE_MODULES = "Multiple Spring Data modules found, entering strict repository configuration mode!";
private static final String NON_DEFAULT_AUTOWIRE_CANDIDATE_RESOLVER = "Non-default AutowireCandidateResolver ({}) detected. Skipping the registration of LazyRepositoryInjectionPointResolver. Lazy repository injection will not be working!";
static final String FACTORY_BEAN_OBJECT_TYPE = "factoryBeanObjectType";
@@ -122,9 +131,13 @@ public class RepositoryConfigurationDelegate {
configurationSource.getBasePackages().stream().collect(Collectors.joining(", ")));
}
Map<String, RepositoryConfiguration<?>> configurations = new HashMap<>();
for (RepositoryConfiguration<? extends RepositoryConfigurationSource> configuration : extension
.getRepositoryConfigurations(configurationSource, resourceLoader, inMultiStoreMode)) {
configurations.put(configuration.getRepositoryInterface(), configuration);
BeanDefinitionBuilder definitionBuilder = builder.build(configuration);
extension.postProcess(definitionBuilder, configurationSource);
@@ -149,6 +162,8 @@ public class RepositoryConfigurationDelegate {
definitions.add(new BeanComponentDefinition(beanDefinition, beanName));
}
potentiallyLazifyRepositories(configurations, registry, configurationSource.getBootstrapMode());
if (LOG.isDebugEnabled()) {
LOG.debug("Finished repository scanning.");
}
@@ -156,6 +171,45 @@ public class RepositoryConfigurationDelegate {
return definitions;
}
/**
* Registers a {@link LazyRepositoryInjectionPointResolver} over the default
* {@link ContextAnnotationAutowireCandidateResolver} to make injection points of lazy repositories lazy, too. Will
* augment the {@link LazyRepositoryInjectionPointResolver}'s configuration if there already is one configured.
*
* @param configurations must not be {@literal null}.
* @param registry must not be {@literal null}.
*/
private static void potentiallyLazifyRepositories(Map<String, RepositoryConfiguration<?>> configurations,
BeanDefinitionRegistry registry, BootstrapMode mode) {
if (!DefaultListableBeanFactory.class.isInstance(registry) || mode.equals(BootstrapMode.DEFAULT)) {
return;
}
DefaultListableBeanFactory beanFactory = DefaultListableBeanFactory.class.cast(registry);
AutowireCandidateResolver resolver = beanFactory.getAutowireCandidateResolver();
if (!Arrays.asList(ContextAnnotationAutowireCandidateResolver.class, LazyRepositoryInjectionPointResolver.class)
.contains(resolver.getClass())) {
LOG.warn(NON_DEFAULT_AUTOWIRE_CANDIDATE_RESOLVER, resolver.getClass().getName());
return;
}
AutowireCandidateResolver newResolver = LazyRepositoryInjectionPointResolver.class.isInstance(resolver) //
? LazyRepositoryInjectionPointResolver.class.cast(resolver).withAdditionalConfigurations(configurations) //
: new LazyRepositoryInjectionPointResolver(configurations);
beanFactory.setAutowireCandidateResolver(newResolver);
if (mode.equals(BootstrapMode.DEFERRED)) {
beanFactory.registerSingleton(DeferredRepositoryInitializationListener.class.getName(),
new DeferredRepositoryInitializationListener(beanFactory));
}
}
/**
* Scans {@code repository.support} packages for implementations of {@link RepositoryFactorySupport}. Finding more
* than a single type is considered a multi-store configuration scenario which will trigger stricter repository
@@ -174,4 +228,58 @@ public class RepositoryConfigurationDelegate {
return multipleModulesFound;
}
/**
* Customer {@link ContextAnnotationAutowireCandidateResolver} that also considers all injection points for lazy
* repositories lazy.
*
* @author Oliver Gierke
* @since 2.1
*/
@Slf4j
@RequiredArgsConstructor
static class LazyRepositoryInjectionPointResolver extends ContextAnnotationAutowireCandidateResolver {
private final Map<String, RepositoryConfiguration<?>> configurations;
/**
* Returns a new {@link LazyRepositoryInjectionPointResolver} that will have its configurations augmented with the
* given ones.
*
* @param configurations must not be {@literal null}.
* @return
*/
LazyRepositoryInjectionPointResolver withAdditionalConfigurations(
Map<String, RepositoryConfiguration<?>> configurations) {
Map<String, RepositoryConfiguration<?>> map = new HashMap<>(this.configurations);
map.putAll(configurations);
return new LazyRepositoryInjectionPointResolver(map);
}
/*
* (non-Javadoc)
* @see org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver#isLazy(org.springframework.beans.factory.config.DependencyDescriptor)
*/
@Override
protected boolean isLazy(DependencyDescriptor descriptor) {
Class<?> type = descriptor.getDependencyType();
RepositoryConfiguration<?> configuration = configurations.get(type.getName());
if (configuration == null) {
return super.isLazy(descriptor);
}
boolean lazyInit = configuration.isLazyInit();
if (lazyInit) {
LOG.debug("Creating lazy injection proxy for {}…", configuration.getRepositoryInterface());
}
return lazyInit;
}
}
}

View File

@@ -22,6 +22,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Supplier;
import javax.annotation.Nullable;
@@ -193,13 +194,17 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit
* {@link BeanDefinitionRegistry}. For {@link BeanDefinition}s to be registered once-and-only-once for all
* configuration elements (annotation or XML), prefer calling
* {@link #registerIfNotAlreadyRegistered(AbstractBeanDefinition, BeanDefinitionRegistry, String, Object)} with a
* dedicated bean name to avoid the bead definition being registered multiple times. *
* dedicated bean name to avoid the bead definition being registered multiple times.
*
* @param registry must not be {@literal null}.
* @param bean must not be {@literal null}.
* @param source must not be {@literal null}.
* @return the bean name generated for the given {@link BeanDefinition}
* @deprecated since 2.1, use
* {@link #registerWithSourceAndGeneratedBeanName(AbstractBeanDefinition, BeanDefinitionRegistry, Object)}
* instead.
*/
@Deprecated
public static String registerWithSourceAndGeneratedBeanName(BeanDefinitionRegistry registry,
AbstractBeanDefinition bean, Object source) {
@@ -211,6 +216,29 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit
return beanName;
}
/**
* Sets the given source on the given {@link AbstractBeanDefinition} and registers it inside the given
* {@link BeanDefinitionRegistry}. For {@link BeanDefinition}s to be registered once-and-only-once for all
* configuration elements (annotation or XML), prefer calling
* {@link #registerIfNotAlreadyRegistered(AbstractBeanDefinition, BeanDefinitionRegistry, String, Object)} with a
* dedicated bean name to avoid the bead definition being registered multiple times.
*
* @param bean must not be {@literal null}.
* @param registry must not be {@literal null}.
* @param source must not be {@literal null}.
* @return the bean name generated for the given {@link BeanDefinition}
*/
public static String registerWithSourceAndGeneratedBeanName(AbstractBeanDefinition bean,
BeanDefinitionRegistry registry, Object source) {
bean.setSource(source);
String beanName = generateBeanName(bean, registry);
registry.registerBeanDefinition(beanName, bean);
return beanName;
}
/**
* Registers the given {@link AbstractBeanDefinition} with the given registry with the given bean name unless the
* registry already contains a bean with that name.
@@ -219,18 +247,62 @@ public abstract class RepositoryConfigurationExtensionSupport implements Reposit
* @param registry must not be {@literal null}.
* @param beanName must not be {@literal null} or empty.
* @param source must not be {@literal null}.
* @deprecated since 2.1, prefer
* {@link #registerIfNotAlreadyRegistered(Supplier, BeanDefinitionRegistry, String, Object)}
*/
@Deprecated
public static void registerIfNotAlreadyRegistered(AbstractBeanDefinition bean, BeanDefinitionRegistry registry,
String beanName, Object source) {
registerIfNotAlreadyRegistered(() -> bean, registry, beanName, source);
}
/**
* Registers the {@link AbstractBeanDefinition} produced by the given {@link Supplier} with the given registry with
* the given bean name unless the registry already contains a bean with that name.
*
* @param supplier must not be {@literal null}.
* @param registry must not be {@literal null}.
* @param beanName must not be {@literal null} or empty.
* @param source must not be {@literal null}.
* @since 2.1
*/
public static void registerIfNotAlreadyRegistered(Supplier<AbstractBeanDefinition> supplier,
BeanDefinitionRegistry registry, String beanName, Object source) {
if (registry.containsBeanDefinition(beanName)) {
return;
}
AbstractBeanDefinition bean = supplier.get();
bean.setSource(source);
registry.registerBeanDefinition(beanName, bean);
}
/**
* Registers the {@link AbstractBeanDefinition} produced by the given {@link Supplier} as lazy bean definition with
* the given registry with the given bean name unless the registry already contains a bean with that name.
*
* @param supplier must not be {@literal null}.
* @param registry must not be {@literal null}.
* @param beanName must not be {@literal null} or empty.
* @param source must not be {@literal null}.
* @since 2.1
*/
public static void registerLazyIfNotAlreadyRegistered(Supplier<AbstractBeanDefinition> supplier,
BeanDefinitionRegistry registry, String beanName, Object source) {
if (registry.containsBeanDefinition(beanName)) {
return;
}
AbstractBeanDefinition definition = supplier.get();
definition.setSource(source);
definition.setLazyInit(true);
registry.registerBeanDefinition(beanName, definition);
}
/**
* Returns whether the given {@link BeanDefinitionRegistry} already contains a bean of the given type assuming the
* bean name has been auto-generated.

View File

@@ -137,4 +137,12 @@ public interface RepositoryConfigurationSource {
* @since 2.1
*/
ImplementationDetectionConfiguration toImplementationDetectionConfiguration(MetadataReaderFactory factory);
/**
* Defines the repository {@link BootstrapMode} to be used.
*
* @return
* @since 2.1
*/
BootstrapMode getBootstrapMode();
}

View File

@@ -16,6 +16,7 @@
package org.springframework.data.repository.config;
import java.util.Collection;
import java.util.Locale;
import java.util.Optional;
import org.springframework.beans.factory.xml.ParserContext;
@@ -50,6 +51,7 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou
private static final String REPOSITORY_FACTORY_BEAN_CLASS_NAME = "factory-class";
private static final String REPOSITORY_BASE_CLASS_NAME = "base-class";
private static final String CONSIDER_NESTED_REPOSITORIES = "consider-nested-repositories";
private static final String BOOTSTRAP_MODE = "bootstrap-mode";
private final Element element;
private final ParserContext context;
@@ -211,4 +213,18 @@ public class XmlRepositoryConfigurationSource extends RepositoryConfigurationSou
public boolean usesExplicitFilters() {
return !(this.includeFilters.isEmpty() && this.excludeFilters.isEmpty());
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.config.RepositoryConfigurationSource#getBootstrapMode()
*/
@Override
public BootstrapMode getBootstrapMode() {
String attribute = element.getAttribute(BOOTSTRAP_MODE);
return StringUtils.hasText(attribute) //
? BootstrapMode.valueOf(attribute.toUpperCase(Locale.US)) //
: BootstrapMode.DEFAULT;
}
}