diff --git a/spring-geode/src/main/java/org/springframework/geode/context/annotation/RefreshableAnnotationConfigApplicationContext.java b/spring-geode/src/main/java/org/springframework/geode/context/annotation/RefreshableAnnotationConfigApplicationContext.java new file mode 100644 index 00000000..7e345d1f --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/context/annotation/RefreshableAnnotationConfigApplicationContext.java @@ -0,0 +1,273 @@ +/* + * Copyright 2020 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 + * + * https://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.geode.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.context.annotation.AnnotationConfigRegistry; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; +import org.springframework.context.annotation.ScopeMetadataResolver; +import org.springframework.context.support.AbstractRefreshableConfigApplicationContext; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A {@literal refreshable} {@link ApplicationContext} capable of loading {@link Class component classes} common + * in {@link Annotation} based configuration as well as scanning {@link String configuration locations}. + * + * @author John Blum + * @see org.springframework.beans.factory.config.BeanDefinition + * @see org.springframework.beans.factory.support.BeanDefinitionRegistry + * @see org.springframework.beans.factory.support.BeanNameGenerator + * @see org.springframework.beans.factory.support.DefaultListableBeanFactory + * @see org.springframework.context.ApplicationContext + * @see org.springframework.context.annotation.AnnotatedBeanDefinitionReader + * @see org.springframework.context.annotation.AnnotationConfigRegistry + * @see org.springframework.context.annotation.ClassPathBeanDefinitionScanner + * @see org.springframework.context.annotation.ScopeMetadataResolver + * @see org.springframework.context.support.AbstractRefreshableConfigApplicationContext + * @since 1.3.0 + */ +@SuppressWarnings("unused") +public class RefreshableAnnotationConfigApplicationContext extends AbstractRefreshableConfigApplicationContext + implements AnnotationConfigRegistry { + + protected static final boolean USE_DEFAULT_FILTERS = true; + + @Nullable + private BeanNameGenerator beanNameGenerator; + + private volatile DefaultListableBeanFactory beanFactory; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final Set basePackages = new LinkedHashSet<>(); + private final Set> componentClasses = new LinkedHashSet<>(); + + @Nullable + private ScopeMetadataResolver scopeMetadataResolver; + + /** + * Constructs an new instance of the {@link RefreshableAnnotationConfigApplicationContext} + * with default container state an no {@literal parent} {@link ApplicationContext}. + * + * @see #refreshBeanFactory() + */ + public RefreshableAnnotationConfigApplicationContext() { + refreshBeanFactory(); // TODO: Not certain this is a good idea; see comment below on createBeanFactory() method! + } + + /** + * Constructs a new instance of the {@link RefreshableAnnotationConfigApplicationContext} initialized with + * the {@literal parent} {@link ApplicationContext}. + * + * @param parent parent {@link ApplicationContext} to this context. + * @see org.springframework.context.ApplicationContext + * @see #refreshBeanFactory() + */ + public RefreshableAnnotationConfigApplicationContext(@Nullable ApplicationContext parent) { + super(parent); + refreshBeanFactory(); // TODO: Not certain this is a good idea; see comment below on createBeanFactory() method! + } + + /** + * Configures the {@link BeanNameGenerator} strategy used by this {@link ApplicationContext} to generate + * {@link String bean names} for {@link BeanDefinition bean definitions}. + * + * @param beanNameGenerator {@link BeanNameGenerator} used to generate {@link String bean names} + * for {@link BeanDefinition bean definitions}. + * @see org.springframework.beans.factory.support.BeanNameGenerator + */ + public void setBeanNameGenerator(@Nullable BeanNameGenerator beanNameGenerator) { + this.beanNameGenerator = beanNameGenerator; + } + + /** + * Returns the {@link Optional optionally} configured {@link BeanNameGenerator} strategy used by this + * {@link ApplicationContext} to generate {@link String bean names} for {@link BeanDefinition bean definitions}. + * + * @return the {@link BeanNameGenerator} strategy used to generate {@link String bean names} + * for {@link BeanDefinition bean definitions}. + * @see org.springframework.beans.factory.support.BeanNameGenerator + * @see java.util.Optional + */ + protected Optional getBeanNameGenerator() { + return Optional.ofNullable(this.beanNameGenerator); + } + + /** + * Returns the configured {@link Logger} used to log framework {@link String messages} to the application log. + * + * @return the configured {@link Logger}. + * @see org.slf4j.Logger + */ + protected Logger getLogger() { + return this.logger; + } + + /** + * Configures the {@link ScopeMetadataResolver} strategy used by this {@link ApplicationContext} to resolve + * the {@literal scope} for {@link BeanDefinition bean definitions}. + * + * @param scopeMetadataResolver {@link ScopeMetadataResolver} used to resolve the {@literal scope} + * of {@link BeanDefinition bean definitions}. + * @see org.springframework.context.annotation.ScopeMetadataResolver + */ + public void setScopeMetadataResolver(@Nullable ScopeMetadataResolver scopeMetadataResolver) { + this.scopeMetadataResolver = scopeMetadataResolver; + } + + /** + * Returns the {@link Optional optionally} configured {@link ScopeMetadataResolver} strategy used by + * this {@link ApplicationContext} to resolve the {@literal scope} for {@link BeanDefinition bean definitions}. + * + * @return the configured {@link ScopeMetadataResolver} used to resolve the {@literal scope} + * for {@link BeanDefinition bean definitions}. + * @see org.springframework.context.annotation.ScopeMetadataResolver + * @see java.util.Optional + */ + public Optional getScopeMetadataResolver() { + return Optional.ofNullable(this.scopeMetadataResolver); + } + + protected boolean isUsingDefaultFilters() { + return USE_DEFAULT_FILTERS; + } + + @Override + protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException { + + AnnotatedBeanDefinitionReader reader = configure(newAnnotatedBeanDefinitionReader(beanFactory)); + + ClassPathBeanDefinitionScanner scanner = configure(newClassBeanDefinitionScanner(beanFactory)); + + getBeanNameGenerator().ifPresent(beanNameGenerator -> { + reader.setBeanNameGenerator(beanNameGenerator); + scanner.setBeanNameGenerator(beanNameGenerator); + beanFactory.registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, beanNameGenerator); + }); + + getScopeMetadataResolver().ifPresent(scopeMetadataResolver -> { + reader.setScopeMetadataResolver(scopeMetadataResolver); + scanner.setScopeMetadataResolver(scopeMetadataResolver); + }); + + Arrays.stream(ArrayUtils.nullSafeArray(getConfigLocations(), String.class)).forEach(configLocation -> { + try { + Class type = ClassUtils.forName(configLocation, getClassLoader()); + getLogger().trace("Registering [{}]", configLocation); + reader.register(type); + } + catch (ClassNotFoundException cause) { + + getLogger().trace(String.format("Could not load class for config location [%s] - trying package scan.", + configLocation), cause); + + if (scanner.scan(configLocation) == 0) { + getLogger().debug("No component classes found for specified class/package [{}]", configLocation); + } + } + }); + } + + private AnnotatedBeanDefinitionReader configure(AnnotatedBeanDefinitionReader reader) { + + Set> componentClasses = this.componentClasses; + + if (!componentClasses.isEmpty()) { + getLogger().debug("Registering component classes: {}", componentClasses); + reader.register(ClassUtils.toClassArray(componentClasses)); + } + + return reader; + } + + private ClassPathBeanDefinitionScanner configure(ClassPathBeanDefinitionScanner scanner) { + + Set basePackages = this.basePackages; + + if (!basePackages.isEmpty()) { + getLogger().debug("Scanning base packages: {}", basePackages); + scanner.scan(StringUtils.toStringArray(basePackages)); + } + + return scanner; + } + + // TODO: Not entirely certain this is a good idea (yet); However, it does follow the BeanFactory creation pattern + // used by // the GenericApplicationContext (& extensions, e.g. AnnotationConfigApplicationContext) that I am + // trying to // preserve in this RefreshableAnnotationConfigApplicationContext implementation. However, the + // AnnotationConfigApplicationContext implementation is NOT refreshable either, although + // AnnotationConfigWebApplicationContext is, so... hmm! + // This overridden createBeanFactory() method effectively maintains the BeanFactory as a Singleton! + @Override + protected synchronized DefaultListableBeanFactory createBeanFactory() { + + if (this.beanFactory == null) { + this.beanFactory = super.createBeanFactory(); + } + + return this.beanFactory; + } + + protected AnnotatedBeanDefinitionReader newAnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry) { + return new AnnotatedBeanDefinitionReader(registry, getEnvironment()); + } + + protected ClassPathBeanDefinitionScanner newClassBeanDefinitionScanner(BeanDefinitionRegistry registry) { + return new ClassPathBeanDefinitionScanner(registry, isUsingDefaultFilters(), getEnvironment()); + } + + /** + * @inheritDoc + */ + @Override + public void register(Class... componentClasses) { + + Arrays.stream(ArrayUtils.nullSafeArray(componentClasses, Class.class)) + .filter(Objects::nonNull) + .forEach(this.componentClasses::add); + } + + /** + * @inheritDoc + */ + @Override + public void scan(String... basePackages) { + + Arrays.stream(ArrayUtils.nullSafeArray(basePackages, String.class)) + .filter(StringUtils::hasText) + .forEach(this.basePackages::add); + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/context/annotation/RefreshableAnnotationConfigApplicationContextIntegrationTests.java b/spring-geode/src/test/java/org/springframework/geode/context/annotation/RefreshableAnnotationConfigApplicationContextIntegrationTests.java new file mode 100644 index 00000000..985f9e2a --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/context/annotation/RefreshableAnnotationConfigApplicationContextIntegrationTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 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 + * + * https://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.geode.context.annotation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Integration Tests for {@link RefreshableAnnotationConfigApplicationContext}. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.geode.context.annotation.RefreshableAnnotationConfigApplicationContext + * @since 1.3.0 + */ +@SuppressWarnings("unused") +public class RefreshableAnnotationConfigApplicationContextIntegrationTests { + + @Test + public void constructsAndRefreshesRefreshableApplicationContextSuccessfully() { + + try (RefreshableAnnotationConfigApplicationContext applicationContext = + new RefreshableAnnotationConfigApplicationContext()) { + + assertThat(applicationContext).isNotNull(); + + applicationContext.register(TestConfiguration.class); + applicationContext.registerShutdownHook(); + applicationContext.refresh(); + + assertThat(applicationContext.isActive()).isTrue(); + assertThat(applicationContext.isRunning()).isTrue(); + + NamedBean testNameBean = applicationContext.getBean("TestName", NamedBean.class); + + assertThat(testNameBean).isNotNull(); + assertThat(testNameBean.getName()).isEqualTo("TestName"); + + applicationContext.close(); + + assertThat(applicationContext.isActive()).isFalse(); + assertThat(applicationContext.isRunning()).isFalse(); + + applicationContext.refresh(); + + assertThat(applicationContext.isActive()).isTrue(); + assertThat(applicationContext.isRunning()).isTrue(); + + NamedBean refreshedTestNameBean = applicationContext.getBean("TestName", NamedBean.class); + + assertThat(refreshedTestNameBean).isNotNull(); + assertThat(refreshedTestNameBean).isEqualTo(testNameBean); + assertThat(refreshedTestNameBean).isNotSameAs(testNameBean); + } + } + + @Configuration + static class TestConfiguration { + + @Bean("TestName") + NamedBean testNamedBean() { + return new NamedBean(); + } + } + + static class NamedBean implements BeanNameAware { + + private String name; + + public NamedBean() { } + + public NamedBean(String name) { + setBeanName(name); + } + + @Override + public final void setBeanName(String name) { + Assert.hasText(name, "Bean name is required"); + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof NamedBean)) { + return false; + } + + NamedBean that = (NamedBean) obj; + + return this.getName().equals(that.getName()); + } + + @Override + public int hashCode() { + + int hashValue = 17; + + hashValue = 37 * hashValue + ObjectUtils.nullSafeHashCode(getName()); + + return hashValue; + } + + @Override + public String toString() { + return getName(); + } + } +}