Skip to content

Commit

Permalink
Improves auto-confiuguration of GemfireTemplates for cache Regions to…
Browse files Browse the repository at this point in the history
… minimize issues when auto-wiring templates into application components.

Resolves gh-55.
  • Loading branch information
jxblum committed Sep 20, 2019
1 parent 6b4827c commit af35414
Show file tree
Hide file tree
Showing 8 changed files with 465 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,30 @@
*/
package org.springframework.geode.boot.autoconfigure;

import java.util.Map;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

import org.apache.geode.cache.GemFireCache;
import org.apache.geode.cache.Region;
import org.apache.geode.internal.concurrent.ConcurrentHashSet;

import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.SingletonBeanRegistry;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
Expand All @@ -38,8 +49,18 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.type.StandardMethodMetadata;
import org.springframework.data.gemfire.GemfireOperations;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.gemfire.GemfireUtils;
import org.springframework.data.gemfire.ResolvableRegionFactoryBean;
import org.springframework.data.gemfire.config.xml.GemfireConstants;
import org.springframework.data.gemfire.util.ArrayUtils;
import org.springframework.data.gemfire.util.CollectionUtils;
import org.springframework.data.gemfire.util.SpringUtils;
import org.springframework.geode.config.annotation.support.TypelessAnnotationConfigSupport;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;

/**
Expand All @@ -48,93 +69,264 @@
* the Spring {@link ConfigurableApplicationContext} in order to perform {@link Region} data access operations.
*
* @author John Blum
* @see org.apache.geode.cache.GemFireCache
* @see org.apache.geode.cache.Region
* @see org.springframework.beans.factory.BeanFactory
* @see org.springframework.beans.factory.config.BeanDefinition
* @see org.springframework.beans.factory.config.BeanFactoryPostProcessor
* @see org.springframework.beans.factory.config.BeanPostProcessor
* @see org.springframework.beans.factory.config.SingletonBeanRegistry
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory
* @see org.springframework.beans.factory.support.BeanDefinitionBuilder
* @see org.springframework.beans.factory.support.BeanDefinitionRegistry
* @see org.springframework.boot.autoconfigure.AutoConfigureAfter
* @see org.springframework.boot.autoconfigure.condition.ConditionalOnBean
* @see org.springframework.boot.autoconfigure.condition.ConditionalOnClass
* @see org.springframework.context.ApplicationContext
* @see org.springframework.context.ConfigurableApplicationContext
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.event.EventListener
* @see org.springframework.data.gemfire.GemfireTemplate
* @see org.springframework.data.gemfire.ResolvableRegionFactoryBean
* @see org.springframework.geode.config.annotation.support.TypelessAnnotationConfigSupport
* @since 1.0.0
*/
@Configuration
@AutoConfigureAfter(ClientCacheAutoConfiguration.class)
@ConditionalOnBean(GemFireCache.class)
@ConditionalOnClass(GemfireTemplate.class)
@SuppressWarnings("unused")
public class RegionTemplateAutoConfiguration {
public class RegionTemplateAutoConfiguration extends TypelessAnnotationConfigSupport {

private static final Set<String> regionTemplateNames = new ConcurrentHashSet<>();
private static final Object NON_BEAN = new Object();

private String toRegionTemplateName(String regionName) {
return StringUtils.uncapitalize(regionName) + "Template";
private static final String TEMPLATE = "Template";

private final Set<String> autoConfiguredRegionTemplateBeanNames = Collections.synchronizedSet(new HashSet<>());
private final Set<String> regionNamesWithTemplates = Collections.synchronizedSet(new HashSet<>());

@Bean
BeanFactoryPostProcessor regionTemplateBeanFactoryPostProcessor() {

return beanFactory -> {

if (beanFactory instanceof BeanDefinitionRegistry) {

BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;

List<String> beanDefinitionNames =
Arrays.asList(ArrayUtils.nullSafeArray(registry.getBeanDefinitionNames(), String.class));

Set<String> userRegionTemplateNames = new HashSet<>();

for (String beanName : beanDefinitionNames) {

String regionTemplateBeanName = toRegionTemplateBeanName(beanName);

if (!beanDefinitionNames.contains(regionTemplateBeanName)) {

BeanDefinition beanDefinition = registry.getBeanDefinition(beanName);

Class<?> resolvedBeanType = resolveBeanClass(beanDefinition, registry).orElse(null);

if (isRegionBeanDefinition(resolvedBeanType)) {
register(newGemfireTemplateBeanDefinition(beanName), regionTemplateBeanName, registry);
}
else if (isGemfireTemplateBeanDefinition(resolvedBeanType)) {
userRegionTemplateNames.add(beanName);
}
else if (isBeanWithGemfireTemplateDependency(beanFactory, beanDefinition)) {
SpringUtils.addDependsOn(beanDefinition, GemfireConstants.DEFAULT_GEMFIRE_CACHE_NAME);
}
}
}

setAutoConfiguredRegionTemplateDependencies(registry, userRegionTemplateNames);
}
};
}

private boolean isBeanWithGemfireTemplateDependency(@NonNull BeanFactory beanFactory,
@NonNull BeanDefinition beanDefinition) {

Predicate<Object> isGemfireTemplate = value -> value instanceof RuntimeBeanReference
? beanFactory.isTypeMatch(((RuntimeBeanReference) value).getBeanName(), GemfireOperations.class)
: value instanceof GemfireOperations;

boolean match = beanDefinition.getConstructorArgumentValues().getGenericArgumentValues().stream()
.map(ConstructorArgumentValues.ValueHolder::getValue)
.anyMatch(isGemfireTemplate);

match |= match || beanDefinition.getPropertyValues().getPropertyValueList().stream()
.map(PropertyValue::getValue)
.anyMatch(isGemfireTemplate);

match |= match || Optional.of(beanDefinition)
.filter(AnnotatedBeanDefinition.class::isInstance)
.map(AnnotatedBeanDefinition.class::cast)
.map(AnnotatedBeanDefinition::getFactoryMethodMetadata)
.filter(StandardMethodMetadata.class::isInstance)
.map(StandardMethodMetadata.class::cast)
.map(StandardMethodMetadata::getIntrospectedMethod)
.map(method -> Arrays.stream(ArrayUtils.nullSafeArray(method.getParameterTypes(), Class.class))
.filter(Objects::nonNull)
.anyMatch(GemfireOperations.class::isAssignableFrom)
).orElse(false);

return match;
}

private boolean isGemfireTemplateBeanDefinition(@Nullable Class<?> beanType) {
return beanType != null && GemfireOperations.class.isAssignableFrom(beanType);
}

private boolean isRegionBeanDefinition(@Nullable Class<?> beanType) {
return beanType != null && ResolvableRegionFactoryBean.class.isAssignableFrom(beanType);
}

private BeanDefinition newGemfireTemplateBeanDefinition(String regionBeanName) {

BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(GemfireTemplate.class);

builder.addConstructorArgReference(regionBeanName);

return builder.getBeanDefinition();
}

// Register BeanDefinition with bean name in BeanDefinitionRegistry
private boolean register(BeanDefinition beanDefinition, String beanName, BeanDefinitionRegistry registry) {

if (this.autoConfiguredRegionTemplateBeanNames.add(beanName)) {
registry.registerBeanDefinition(beanName, beanDefinition);
return true;
}

return false;
}

private void setAutoConfiguredRegionTemplateDependencies(BeanDefinitionRegistry registry,
Set<String> dependencyBeanNames) {

String[] dependencyBeanNamesArray = dependencyBeanNames.toArray(new String[0]);

this.autoConfiguredRegionTemplateBeanNames.stream()
.map(registry::getBeanDefinition)
.forEach(beanDefinition -> SpringUtils.addDependsOn(beanDefinition, dependencyBeanNamesArray));
}

// Required by @EnableClusterDefinedRegions & Native-Defined Regions (e.g. Regions defined in "cache.xml").
@Bean
BeanPostProcessor regionTemplateBeanPostProcessor(ConfigurableApplicationContext applicationContext) {

handlePrematureCacheCreation(applicationContext);

return new BeanPostProcessor() {

/**
* User-defined {@link GemfireTemplate} beans should be post processed before
* auto-configured {@link GemfireTemplate} beans!
*
* @see RegionTemplateAutoConfiguration#setAutoConfiguredRegionTemplateDependencies(BeanDefinitionRegistry, Set)
*/
@Nullable @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {

if (bean instanceof GemfireTemplate) {
if (autoConfiguredRegionTemplateBeanNames.contains(beanName)) {
if (regionNamesWithTemplates.contains(((GemfireTemplate) bean).getRegion().getName())) {
// Returning NO_BEAN means an existing, user-defined GemfireTemplate bean already exists
// for the target Region and the auto-configured GemfireTemplate bean is not required.
bean = NON_BEAN;
}
}
else {
regionNamesWithTemplates.add(((GemfireTemplate) bean).getRegion().getName());
}
}

return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

if (bean instanceof Region) {
if (bean instanceof GemFireCache) {

String regionTemplateName = toRegionTemplateName(beanName);
GemFireCache cache = (GemFireCache) bean;

registerRegionTemplateBean(regionTemplateName, bean);
registerRegionTemplatesForCacheRegions(applicationContext, cache);
}

return bean;
}

@SuppressWarnings("all")
private void registerRegionTemplateBean(String regionTemplateName, Object bean) {

Optional.ofNullable(applicationContext)
.filter(it -> bean instanceof Region)
.filter(it -> !it.containsBean(regionTemplateName))
.filter(it -> isGemfireTemplateWithRegionNotPresent(it, (Region) bean))
.map(ConfigurableApplicationContext::getBeanFactory)
.filter(SingletonBeanRegistry.class::isInstance)
.map(SingletonBeanRegistry.class::cast)
.ifPresent(beanFactory -> {
beanFactory.registerSingleton(regionTemplateName, new GemfireTemplate((Region) bean));
regionTemplateNames.add(regionTemplateName);
});
}
};
}

// TODO: Remove this logic when DATAGEODE-231 is resolved!
private void handlePrematureCacheCreation(ConfigurableApplicationContext applicationContext) {

Optional.ofNullable(GemfireUtils.resolveGemFireCache())
.ifPresent(cache -> registerRegionTemplatesForCacheRegions(applicationContext, cache));
}

// Required by @EnableCachingDefinedRegions
@EventListener({ ContextRefreshedEvent.class })
public void registerRemainingRegionTemplatesOnContextRefresh(ContextRefreshedEvent event) {
public void regionTemplateContextRefreshedEventListener(ContextRefreshedEvent event) {

this.regionNamesWithTemplates.clear();

ApplicationContext applicationContext = event.getApplicationContext();

if (applicationContext instanceof ConfigurableApplicationContext) {

ConfigurableListableBeanFactory beanFactory =
((ConfigurableApplicationContext) applicationContext).getBeanFactory();

Optional.ofNullable(applicationContext.getBean(GemFireCache.class))
.map(GemFireCache::rootRegions)
.ifPresent(rootRegions -> rootRegions.stream()
.filter(Objects::nonNull)
.filter(region -> !regionTemplateNames.contains(toRegionTemplateName(region.getName())))
.filter(region -> !applicationContext.containsBean(toRegionTemplateName(region.getName())))
.filter(region -> isGemfireTemplateWithRegionNotPresent(applicationContext, region))
.forEach(region -> beanFactory.registerSingleton(toRegionTemplateName(region.getName()),
new GemfireTemplate(region))));
ConfigurableApplicationContext configurableApplicationContext =
(ConfigurableApplicationContext) applicationContext;

GemFireCache cache = configurableApplicationContext.getBean(GemFireCache.class);

registerRegionTemplatesForCacheRegions(configurableApplicationContext, cache);
}
}

private void registerRegionTemplatesForCacheRegions(@NonNull ConfigurableApplicationContext applicationContext,
@NonNull GemFireCache cache) {

for (Region region : CollectionUtils.nullSafeSet(cache.rootRegions())) {

String regionTemplateBeanName = toRegionTemplateBeanName(region.getName());

registerRegionTemplateBean(applicationContext, region, regionTemplateBeanName);
}
}

private boolean isGemfireTemplateWithRegionNotPresent(ApplicationContext applicationContext, Region region) {
private void registerRegionTemplateBean(@NonNull ConfigurableApplicationContext applicationContext,
@NonNull Region region, String regionTemplateBeanName) {

Map<String, GemfireTemplate> gemfireTemplateBeans =
applicationContext.getBeansOfType(GemfireTemplate.class, false, false);
Optional.of(applicationContext)
.filter(it -> isNotBean(it, regionTemplateBeanName))
.map(ConfigurableApplicationContext::getBeanFactory)
.ifPresent(beanFactory -> register(newGemfireTemplate(region), regionTemplateBeanName, beanFactory));
}

private boolean isNotBean(@NonNull ApplicationContext applicationContext, @Nullable String beanName) {
return !(StringUtils.hasText(beanName) && applicationContext.containsBean(beanName));
}

@SuppressWarnings("unchecked")
private GemfireTemplate newGemfireTemplate(@NonNull Region region) {
return new GemfireTemplate(region);
}

// Register Singleton Object with bean name in BeanDefinitionRegistry
private void register(Object singletonObject, String beanName, ConfigurableBeanFactory beanFactory) {

if (this.autoConfiguredRegionTemplateBeanNames.add(beanName)) {
beanFactory.registerSingleton(beanName, singletonObject);
}
}

return CollectionUtils.nullSafeMap(gemfireTemplateBeans).values().stream()
.map(GemfireTemplate::getRegion)
.noneMatch(templateRegion -> templateRegion.equals(region));
private String toRegionTemplateBeanName(@NonNull String regionName) {
return StringUtils.uncapitalize(regionName) + TEMPLATE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ public void setup() {
.map(Region::getName)
.sorted()
.collect(Collectors.toList())).containsExactly("BooksByAuthor", "BooksByYear", "CachedBooks");

assertThat(this.booksByAuthor).isNotNull();
assertThat(this.booksByTitle).isNotNull();
assertThat(this.booksByYear).isNotNull();
}

@Test
Expand Down Expand Up @@ -134,5 +138,10 @@ static class TestApplicationConfiguration {
LibraryService libraryService() {
return new LibraryService();
}

//@Bean("TestBean")
Object testBean(@Qualifier("booksByAuthor") GemfireTemplate booksByAuthorTemplate) {
return "TEST";
}
}
}
Loading

0 comments on commit af35414

Please sign in to comment.