GH-195: Introduce ComponentCustomizer (#273)

Fixes #195 

* Introduce a `ComponentCustomizer` contract to be exposed in
the end-user configuration for particular bean type
* introduce a `@CustomizationAware` annotation to mark a component 
as customizable
* Introduce a `ComponentCustomizationBeanPostProcessor` as an auto-configured
bean to apply `ComponentCustomizer` to a matching `@CustomizationAware` (if any)
* Use `@CustomizationAware` in the `FileSupplierConfiguration`
and demonstrate `ComponentCustomizer` feature in its tests
* Move `config-common` to functions parent to make the mentioned auto-configuration
available for all the functions
* Remove some other duplicated dependencies from the affected poms
This commit is contained in:
Artem Bilan
2022-05-24 21:55:11 -04:00
committed by GitHub
parent 7aa166e54c
commit 89cd8812c3
23 changed files with 249 additions and 189 deletions

View File

@@ -74,11 +74,6 @@
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-ip</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2022-2022 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.cloud.fn.common.config;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
/**
* An auto-configuration to expose customizer strategy beans to accept user-provided component customizers.
*
* @author Artem Bilan
*
* @since 1.2.1
*
* @see ComponentCustomizerBeanPostProcessor
*/
public class ComponentCustomizationAutoConfiguration {
@Bean
@ConditionalOnBean(ComponentCustomizer.class)
static BeanPostProcessor componentCustomizerBeanPostProcessor() {
return new ComponentCustomizerBeanPostProcessor();
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2022-2022 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.cloud.fn.common.config;
/**
* The customizer contract to apply to beans in the application context which are marked
* with the {@link CustomizationAware} annotation and their type is matching to generic
* type of the instance of this interface.
* <p>
* The bean for {@link ComponentCustomizer} has to be declared as a {@code static} bean
* method to avoid early bean initialization syndrome when not all bean post processors
* are configured into the application context yet. Its dependencies have to be {@code static}
* as well, or all of related beans can be declared in the dedicated {@code @Configuration} class.
*
* @param <T> the target component (bean) type in the application context to customize.
*
* @author Artem Bilan
*
* @since 1.2.1
*/
@FunctionalInterface
public interface ComponentCustomizer<T> {
void customize(T component, String beanName);
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2022-2022 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.cloud.fn.common.config;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.core.log.LogMessage;
import org.springframework.core.type.MethodMetadata;
/**
* The {@link MergedBeanDefinitionPostProcessor} to apply a {@link ComponentCustomizer} for a bean
* in process if this bean is marked with the {@link CustomizationAware} annotation and
* a customizer for a type of this bean is present in the application context.
*
* @author Artem Bilan
*
* @since 1.2.1
*/
class ComponentCustomizerBeanPostProcessor implements MergedBeanDefinitionPostProcessor, BeanFactoryAware {
private static final Log logger = LogFactory.getLog(ComponentCustomizerBeanPostProcessor.class);
private final Set<String> customizationAwareBeanNames = new HashSet<>();
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
if (beanDefinition instanceof AnnotatedBeanDefinition) {
AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
MethodMetadata factoryMethodMetadata = annotatedBeanDefinition.getFactoryMethodMetadata();
if (factoryMethodMetadata != null &&
factoryMethodMetadata.isAnnotated(CustomizationAware.class.getName())) {
this.customizationAwareBeanNames.add(beanName);
}
}
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (this.customizationAwareBeanNames.remove(beanName)) {
ResolvableType integrationComponentCustomizerType =
ResolvableType.forClassWithGenerics(ComponentCustomizer.class,
AopProxyUtils.ultimateTargetClass(bean));
ComponentCustomizer<Object> componentCustomizer =
this.beanFactory.<ComponentCustomizer<Object>>getBeanProvider(integrationComponentCustomizerType)
.getIfAvailable();
if (componentCustomizer != null) {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.format("Use '%s' for '%s' customization...", componentCustomizer, beanName));
}
componentCustomizer.customize(bean, beanName);
}
}
return bean;
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2022-2022 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.cloud.fn.common.config;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The annotation to mark a {@code @Bean} method for end user-provided customization
* via {@link ComponentCustomizer} with this bean type.
*
* @author Artem Bilan
*
* @since 1.2.1
*/
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomizationAware {
}

View File

@@ -1,2 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.fn.common.config.SpelExpressionConverterConfiguration
org.springframework.cloud.fn.common.config.SpelExpressionConverterConfiguration,\
org.springframework.cloud.fn.common.config.ComponentCustomizationAutoConfiguration

View File

@@ -28,12 +28,6 @@
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-ip</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -21,11 +21,6 @@
<artifactId>payload-converter-function</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -35,20 +30,6 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -22,34 +22,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>payload-converter-function</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
@@ -68,10 +49,5 @@
<version>${test-containers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -26,11 +26,6 @@
<artifactId>geode-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.geode</groupId>
<artifactId>spring-geode-starter-test</artifactId>

View File

@@ -27,12 +27,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>

View File

@@ -25,12 +25,6 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -14,13 +14,6 @@
<description>ZeroMQ consumer</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Integration dependencies -->
<dependency>
<groupId>org.springframework.integration</groupId>

View File

@@ -15,22 +15,6 @@
<description>Spring Native Function for Aggregator</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<!--MongoDb-->
<dependency>
<groupId>org.springframework.integration</groupId>
@@ -99,29 +83,6 @@
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -15,45 +15,11 @@
<description>Spring Native Function for applying filter SpEL expressions</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>payload-converter-function</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -19,13 +19,6 @@
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>

View File

@@ -19,7 +19,11 @@
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${revision}</version>
</dependency>
<!-- Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.fn.common.config.CustomizationAware;
import org.springframework.cloud.fn.common.file.FileConsumerProperties;
import org.springframework.cloud.fn.common.file.FileReadingMode;
import org.springframework.cloud.fn.common.file.FileUtils;
@@ -92,6 +93,7 @@ public class FileSupplierConfiguration {
}
@Bean
@CustomizationAware
public FileInboundChannelAdapterSpec fileMessageSource(FileListFilter<File> fileListFilter) {
return Files.inboundAdapter(this.fileSupplierProperties.getDirectory())
.filter(fileListFilter);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2020-2020 the original author or authors.
* Copyright 2020-2022 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.
@@ -17,6 +17,7 @@
package org.springframework.cloud.fn.supplier.file;
import java.nio.file.Path;
import java.util.Date;
import java.util.function.Supplier;
import org.junit.jupiter.api.AfterAll;
@@ -27,6 +28,10 @@ import reactor.core.publisher.Flux;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.fn.common.config.ComponentCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.file.dsl.FileInboundChannelAdapterSpec;
import org.springframework.messaging.Message;
import org.springframework.test.annotation.DirtiesContext;
@@ -57,5 +62,19 @@ public class AbstractFileSupplierTests {
@SpringBootApplication
static class FileSupplierTestApplication {
@Bean
static ComponentCustomizer<FileInboundChannelAdapterSpec> fileInboundChannelAdapterSpecCustomizer() {
return (adapterSpec, beanName) -> adapterSpec.watchEvents(FileReadingMessageSource.WatchEventType.DELETE);
}
@Bean
static ComponentCustomizer<Date> fakeCustomizer() {
return (date, beanName) -> {
throw new RuntimeException("Must not happen");
};
}
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.integration.file.FileHeaders;
import org.springframework.integration.file.FileReadingMessageSource;
import org.springframework.integration.jdbc.metadata.JdbcMetadataStore;
import org.springframework.integration.metadata.ConcurrentMetadataStore;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.messaging.Message;
import org.springframework.test.context.TestPropertySource;
@@ -104,6 +105,14 @@ public class DefaultFileSupplierTests extends AbstractFileSupplierTests {
assertThat(metadataStoreContent.get(0)).startsWith("local-file-system-metadata-");
assertThat(metadataStoreContent.get(0)).endsWith("first.file");
assertThat(metadataStoreContent.get(1)).endsWith("test.file");
/* See AbstractFileSupplierTests.FileSupplierTestApplication.fileInboundChannelAdapterSpecCustomizer() -
through the ComponentCustomizer and @CustomizationAware on the FileSupplierConfiguration.fileMessageSource()
the provided customization is populated down to the bean under testing.
*/
assertThat(TestUtils.getPropertyValue(this.fileMessageSource, "watchEvents",
FileReadingMessageSource.WatchEventType[].class))
.isEqualTo(new FileReadingMessageSource.WatchEventType[]{ FileReadingMessageSource.WatchEventType.DELETE });
}
}

View File

@@ -31,12 +31,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
@@ -48,11 +42,6 @@
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.geode</groupId>
<artifactId>spring-geode-starter-logging</artifactId>

View File

@@ -32,12 +32,6 @@
<version>${spring-cloud-function.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>splitter-function</artifactId>

View File

@@ -14,12 +14,6 @@
<description>sftp supplier</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
@@ -30,11 +24,6 @@
<artifactId>spring-integration-sftp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>config-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud.fn</groupId>
<artifactId>file-common</artifactId>