From 3b5208c213a411b7e762410ed435d4011c50f9a1 Mon Sep 17 00:00:00 2001 From: John Blum Date: Mon, 22 Mar 2021 19:52:27 -0700 Subject: [PATCH] Add (experimental) DependencyOf annotation type and associated DependencyOfBeanFactoryPostProcessor class. The @DependencyOf annotation is the inverse of Spring's @DependsOn annotation enabling a bean to declare itself as a dependency for another bean in the Spring ApplicationContext (container). --- .../context/annotation/DependencyOf.java | 104 ++++++++++++++++++ .../DependencyOfBeanFactoryPostProcessor.java | 92 ++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOf.java create mode 100644 spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOfBeanFactoryPostProcessor.java diff --git a/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOf.java b/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOf.java new file mode 100644 index 0000000..e83695a --- /dev/null +++ b/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOf.java @@ -0,0 +1,104 @@ +/* + * Copyright 2019 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.data.gemfire.tests.extensions.spring.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.annotation.AliasFor; + +/** + * The {@link DependencyOf} annotation is the inverse of Spring's {@link DependsOn} annotation allowing a bean + * to declare itself as a dependency of another bean in the Spring container. + * + * For example, with Spring's {@link DependsOn} annotation, a bean A can say that it {@literal depends on} bean B. + * However, with the {@link DependencyOf} annotation, a bean B can say it is a required dependency for bean A, + * or rather that bean A depends on bean B. + * + * Therefore, the following bean definitions for A & B are equivalent: + * + * + * + * @Configuration + * public class ConfigurationOne { + * + * @Bean + * @DependsOn("b") + * public A a() { + * return new A(); + * } + * + * @Bean + * public B b() { + * return new B(); + * } + * } + * + * + * + * And... + * + * + * @Configuration + * public class ConfigurationTwo { + * + * @Bean + * public A a() { + * return new A(); + * } + * + * @Bean + * @DependencyOf("a") + * public B b() { + * return new B(); + * } + * } + * + * + * One advantage of this approach is that bean A does not need to know all the beans it is possibly dependent on, + * especially at runtime when additional collaborators or dependencies maybe added dynamically to the classpath, + * Therefore, additional dependencies of A can be added to the configuration automatically, over time without + * having to go back and modify the bean definition for A. + * + * This feature is experimental. + * + * @author John Blum + * @see java.lang.annotation.Documented + * @see java.lang.annotation.Inherited + * @see java.lang.annotation.Retention + * @see java.lang.annotation.Target + * @see org.springframework.context.annotation.DependsOn + * @since 0.0.23 + */ +@Documented +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.ANNOTATION_TYPE, ElementType.TYPE }) +@SuppressWarnings("unused") +public @interface DependencyOf { + + @AliasFor("value") + String[] beanNames() default {}; + + @AliasFor("beanNames") + String[] value() default {}; + +} diff --git a/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOfBeanFactoryPostProcessor.java b/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOfBeanFactoryPostProcessor.java new file mode 100644 index 0000000..9c71655 --- /dev/null +++ b/spring-data-geode-test/src/main/java/org/springframework/data/gemfire/tests/extensions/spring/context/annotation/DependencyOfBeanFactoryPostProcessor.java @@ -0,0 +1,92 @@ +/* + * Copyright 2019 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.data.gemfire.tests.extensions.spring.context.annotation; + +import java.lang.annotation.Annotation; +import java.util.Optional; + +import org.apache.shiro.util.StringUtils; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.SpringUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * Spring {@link BeanFactoryPostProcessor} implementation used to post process {@link BeanDefinition BeanDefinitions} + * annotated with {@link DependencyOf} annotations. + * + * @author John Blum + * @see org.springframework.beans.factory.config.BeanDefinition + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor + * @see org.springframework.beans.factory.config.ConfigurableListableBeanFactory + * @see org.springframework.data.gemfire.tests.extensions.spring.context.annotation.DependencyOf + * @since 0.0.23 + */ +public class DependencyOfBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + + protected static final Class DEPENDENCY_OF_TYPE = DependencyOf.class; + + protected static final String VALUE_ATTRIBUTE_NAME = "value"; + + /** + * @inheritDoc + */ + @Override + public void postProcessBeanFactory(@NonNull ConfigurableListableBeanFactory beanFactory) throws BeansException { + + String[] dependencyOfAnnotatedBeanNames = + ArrayUtils.nullSafeArray(beanFactory.getBeanNamesForAnnotation(DEPENDENCY_OF_TYPE), String.class); + + for (String beanName : dependencyOfAnnotatedBeanNames) { + + Annotation dependencyOf = beanFactory.findAnnotationOnBean(beanName, DEPENDENCY_OF_TYPE); + + Optional.ofNullable(dependencyOf) + .map(this::getAnnotationAttributes) + .map(this::getValueAttribute) + .ifPresent(dependentBeanNames -> { + for (String dependentBeanName : dependentBeanNames) { + Optional.ofNullable(dependentBeanName) + .filter(StringUtils::hasText) + .map(beanFactory::getBeanDefinition) + .ifPresent(dependentBeanDefinition -> + SpringUtils.addDependsOn(dependentBeanDefinition, beanName)); + } + }); + } + } + + private @Nullable AnnotationAttributes getAnnotationAttributes(@NonNull Annotation annotation) { + + return annotation != null + ? AnnotationAttributes.fromMap(AnnotationUtils.getAnnotationAttributes(annotation)) + : null; + } + + private @Nullable String[] getValueAttribute(@NonNull AnnotationAttributes annotationAttributes) { + + return annotationAttributes != null + ? annotationAttributes.getStringArray(VALUE_ATTRIBUTE_NAME) + : null; + } +}