From fc8ffa607c51fa5d9b6db426fb436912227c8501 Mon Sep 17 00:00:00 2001 From: onobc Date: Sat, 19 Feb 2022 00:37:33 -0600 Subject: [PATCH] Improve ContextFunctionCatalogAutoConfiguration conditional loading - Allow custom AvroSchemaServiceManager to be used - Make AvroSchemaMessageConverter bean method specifically typed - Make CloudEventsMessageConverter bean method specifically typed - Add tests focusing on the conditional loading aspects of the auto configuration Fixes gh-797 Resolves #814 --- ...ntextFunctionCatalogAutoConfiguration.java | 55 ++++--- .../avro/AvroSchemaServiceManager.java | 3 +- ...oConfigurationConditionalLoadingTests.java | 136 ++++++++++++++++++ 3 files changed, 170 insertions(+), 24 deletions(-) create mode 100644 spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationConditionalLoadingTests.java diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java index 781acd984..62eb8ac9d 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-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. @@ -41,6 +41,7 @@ import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.MessageRoutingCallback; import org.springframework.cloud.function.context.catalog.BeanFactoryAwareFunctionRegistry; import org.springframework.cloud.function.context.converter.avro.AvroSchemaMessageConverter; +import org.springframework.cloud.function.context.converter.avro.AvroSchemaServiceManager; import org.springframework.cloud.function.context.converter.avro.AvroSchemaServiceManagerImpl; import org.springframework.cloud.function.core.FunctionInvocationHelper; import org.springframework.cloud.function.json.GsonMapper; @@ -69,7 +70,6 @@ import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; - /** * @author Dave Syer * @author Mark Fisher @@ -77,6 +77,7 @@ import org.springframework.util.StringUtils; * @author Artem Bilan * @author Anshul Mehra * @author Soby Chacko + * @author Chris Bono */ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(FunctionCatalog.class) @@ -110,8 +111,7 @@ public class ContextFunctionCatalogAutoConfiguration { if (!CollectionUtils.isEmpty(messageConverters)) { for (MessageConverter mc : messageConverters) { if (mc instanceof CompositeMessageConverter) { - List conv = ((CompositeMessageConverter) mc).getConverters().stream() - .collect(Collectors.toList()); + List conv = ((CompositeMessageConverter) mc).getConverters().stream().toList(); mcList.addAll(conv); } else { @@ -121,7 +121,7 @@ public class ContextFunctionCatalogAutoConfiguration { } mcList = mcList.stream() - .filter(c -> isConverterEligible(c)) + .filter(this::isConverterEligible) .collect(Collectors.toList()); mcList.add(new JsonMessageConverter(jsonMapper)); @@ -139,20 +139,6 @@ public class ContextFunctionCatalogAutoConfiguration { return new BeanFactoryAwareFunctionRegistry(conversionService, messageConverter, jsonMapper, functionProperties, functionInvocationHelper); } - @Bean - @ConditionalOnMissingBean - @ConditionalOnClass(name = "org.apache.avro.Schema") - public MessageConverter avroSchemaMessageConverter() { - return new AvroSchemaMessageConverter(new AvroSchemaServiceManagerImpl()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnClass(name = "io.cloudevents.spring.messaging.CloudEventMessageConverter") - public MessageConverter cloudEventMessageConverter() { - return new CloudEventMessageConverter(); - } - @Bean(RoutingFunction.FUNCTION_NAME) RoutingFunction functionRouter(FunctionCatalog functionCatalog, FunctionProperties functionProperties, BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) { @@ -164,10 +150,35 @@ public class ContextFunctionCatalogAutoConfiguration { if (messageConverterName.startsWith("org.springframework.cloud.")) { return true; } - else if (!messageConverterName.startsWith("org.springframework.")) { - return true; + return !messageConverterName.startsWith("org.springframework."); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "io.cloudevents.spring.messaging.CloudEventMessageConverter") + static class CloudEventsMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean + public CloudEventMessageConverter cloudEventMessageConverter() { + return new CloudEventMessageConverter(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "org.apache.avro.Schema") + static class AvroSchemaMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean + public AvroSchemaServiceManager avroSchemaServiceManager() { + return new AvroSchemaServiceManagerImpl(); + } + + @Bean + @ConditionalOnMissingBean + public AvroSchemaMessageConverter avroSchemaMessageConverter(AvroSchemaServiceManager avroSchemaServiceManager) { + return new AvroSchemaMessageConverter(avroSchemaServiceManager); } - return false; } @ComponentScan(basePackages = "${spring.cloud.function.scan.packages:functions}", diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java index 9152df071..eed921215 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java @@ -28,8 +28,7 @@ import org.apache.avro.io.DatumWriter; * Helps to substitute the default implementation of {@link org.apache.avro.Schema} * Generation using Custom Avro schema generator * - * Provide a custom bean definition of {@link AvroSchemaServiceManager} and mark - * it as @Primary to override the default implementation + * Provide a custom bean definition of {@link AvroSchemaServiceManager} to override the default implementation. * * Migrating this interface from the original Spring Cloud Schema Registry project. * diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationConditionalLoadingTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationConditionalLoadingTests.java new file mode 100644 index 000000000..768ef643d --- /dev/null +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationConditionalLoadingTests.java @@ -0,0 +1,136 @@ +/* + * 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.function.context.config; + +import io.cloudevents.spring.messaging.CloudEventMessageConverter; +import org.apache.avro.Schema; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionRegistry; +import org.springframework.cloud.function.context.converter.avro.AvroSchemaMessageConverter; +import org.springframework.cloud.function.context.converter.avro.AvroSchemaServiceManager; +import org.springframework.cloud.function.context.scan.TestFunction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests the conditional loading aspects of the {@link ContextFunctionCatalogAutoConfiguration}. + * + * @author Chris Bono + */ +public class ContextFunctionCatalogAutoConfigurationConditionalLoadingTests { + + protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ContextFunctionCatalogAutoConfiguration.class)); + + @Test + void autoConfigDisabledWhenCustomFunctionCatalogExists() { + contextRunner.withBean(FunctionCatalog.class, () -> mock(FunctionCatalog.class)) + .run((context) -> assertThat(context).doesNotHaveBean(FunctionRegistry.class)); + } + + @Nested + class AvroSchemaMessageConverterConfig { + + @Test + void avroSchemaMessageConverterBeansLoadedWhenAvroOnClasspath() { + contextRunner.run((context) -> assertThat(context).hasSingleBean(AvroSchemaServiceManager.class) + .hasSingleBean(AvroSchemaMessageConverter.class)); + } + + @Test + void avroSchemaMessageConverterBeansNotLoadedWhenAvroNotOnClasspath() { + contextRunner.withClassLoader(new FilteredClassLoader(Schema.class)).run((context) -> + assertThat(context).doesNotHaveBean(AvroSchemaServiceManager.class) + .doesNotHaveBean(AvroSchemaMessageConverter.class)); + } + + @Test + void customAvroSchemaServiceManagerIsRespected() { + AvroSchemaServiceManager customManager = mock(AvroSchemaServiceManager.class); + contextRunner.withBean(AvroSchemaServiceManager.class, () -> customManager) + .run((context) -> assertThat(context).getBean(AvroSchemaServiceManager.class).isSameAs(customManager)); + } + + @Test + void customAvroSchemaMessageConverterIsRespected() { + AvroSchemaMessageConverter customConverter = mock(AvroSchemaMessageConverter.class); + contextRunner.withBean(AvroSchemaMessageConverter.class, () -> customConverter) + .run((context) -> assertThat(context).getBean(AvroSchemaMessageConverter.class).isSameAs(customConverter)); + } + } + + @Nested + class CloudEventsMessageConverterConfig { + + @Test + void cloudEventsMessageConverterBeanLoadedWhenCloudEventsOnClasspath() { + contextRunner.run((context) -> assertThat(context).hasSingleBean(CloudEventMessageConverter.class)); + } + + @Test + void cloudEventsMessageConverterBeanNotLoadedWhenCloudEventsNotOnClasspath() { + contextRunner.withClassLoader(new FilteredClassLoader(CloudEventMessageConverter.class)).run((context) -> + assertThat(context).doesNotHaveBean(CloudEventMessageConverter.class)); + } + + @Test + void customCloudEventsMessageConverterIsRespected() { + CloudEventMessageConverter customConverter = mock(CloudEventMessageConverter.class); + contextRunner.withBean(CloudEventMessageConverter.class, () -> customConverter) + .run((context) -> assertThat(context).getBean(CloudEventMessageConverter.class).isSameAs(customConverter)); + } + } + + @Nested + class PlainFunctionScanConfig { + + @Test + void functionScanConfigEnabledByDefault() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackageName()) + .run((context) -> assertThat(context).hasSingleBean(TestFunction.class)); + } + + @Test + void functionScanConfigEnabledWhenEnabledPropertySetToTrue() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackageName(), + "spring.cloud.function.scan.enabled:true") + .run((context) -> assertThat(context).hasSingleBean(TestFunction.class)); + } + + @Test + void functionScanConfigEnabledWithScanPackagesPointingToNoFunctions() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackageName() + ".faux", + "spring.cloud.function.scan.enabled:true") + .run((context) -> assertThat(context).doesNotHaveBean(TestFunction.class)); + } + + @Test + void functionScanConfigDisabledWhenEnabledPropertySetToFalse() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackageName(), + "spring.cloud.function.scan.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(TestFunction.class)); + } + + } +}