diff --git a/docs/src/main/asciidoc/spring-cloud-function.adoc b/docs/src/main/asciidoc/spring-cloud-function.adoc index c1bbad7f4..e175e9a06 100644 --- a/docs/src/main/asciidoc/spring-cloud-function.adoc +++ b/docs/src/main/asciidoc/spring-cloud-function.adoc @@ -542,6 +542,11 @@ same rules for signature transformation outlined in "Java 8 function support" se To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate autoconfiguration and supporting classes. +=== Function Component Scan + +Spring Cloud Function will scan for implementations of `Function`, `Consumer` and `Supplier` in a package called `functions` if it exists. Using this +feature you can write functions that have no dependencies on Spring - not even the `@Component` annotation is needed. If you want to use a different +package, you can set `spring.cloud.function.scan.packages`. You can also use `spring.cloud.function.scan.enabled=false` to switch off the scan completely. == Standalone Web Applications @@ -747,6 +752,13 @@ Here we are identifying two functions to deploy, which we can now access in func For more details please reference the complete sample available https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-deployer/src/it/simplestjar[here]. You can also find a corresponding test in https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java#L70[FunctionDeployerTests]. +*** Component Scanning *** + +Since version 3.1.4 you can simplify your configuration thru component scanning feature described in <>. If you place your functional class in +package named `functions`, you can omit `spring.cloud.function.function-class` property as framework will auto-discover functional classes loading them in function catalog. +Keep in mind the naming convention to follow when doing function lookup. For example function class `functions.UpperCaseFunction` will be available in `FunctionCatalog` +under the name `upperCaseFunction`. + ==== Spring Boot JAR This packaging option implies there is a dependency on Spring Boot and that the JAR was generated as Spring Boot JAR. That said, given that the deployed JAR diff --git a/spring-cloud-function-deployer/src/it/simplestjarcs/pom.xml b/spring-cloud-function-deployer/src/it/simplestjarcs/pom.xml new file mode 100644 index 000000000..503556277 --- /dev/null +++ b/spring-cloud-function-deployer/src/it/simplestjarcs/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + function.example + simplestjarcs + 1.0.0.RELEASE + Showcases compoinent scanning capabilities + jar + + simplestjarcs + + + UTF-8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + diff --git a/spring-cloud-function-deployer/src/it/simplestjarcs/src/main/java/functions/UpperCaseFunction.java b/spring-cloud-function-deployer/src/it/simplestjarcs/src/main/java/functions/UpperCaseFunction.java new file mode 100644 index 000000000..07f535dc1 --- /dev/null +++ b/spring-cloud-function-deployer/src/it/simplestjarcs/src/main/java/functions/UpperCaseFunction.java @@ -0,0 +1,13 @@ +package functions; + +import java.util.function.Function; + +public class UpperCaseFunction implements Function { + + @Override + public String apply(String value) { + System.out.println("Uppercasing " + value); + return value.toUpperCase(); + } + +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java index f085e08a4..dd69a401e 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2019 the original author or authors. + * Copyright 2019-2021 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. @@ -27,11 +27,18 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.SpringApplication; import org.springframework.boot.loader.JarLauncher; import org.springframework.boot.loader.LaunchedURLClassLoader; @@ -40,6 +47,9 @@ import org.springframework.boot.loader.jar.JarFile; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; +import org.springframework.core.type.filter.RegexPatternTypeFilter; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -70,12 +80,15 @@ class FunctionArchiveDeployer extends JarLauncher { } @SuppressWarnings({ "unchecked", "rawtypes" }) - void deploy(FunctionRegistry functionRegistry, FunctionDeployerProperties functionProperties, String[] args) { + void deploy(FunctionRegistry functionRegistry, FunctionDeployerProperties functionProperties, String[] args, ApplicationContext applicationContext) { ClassLoader currentLoader = Thread.currentThread().getContextClassLoader(); try { ClassLoader cl = createClassLoader(discoverClassPathAcrhives().iterator()); + Thread.currentThread().setContextClassLoader(cl); + + evalContext.setTypeLocator(new StandardTypeLocator(Thread.currentThread().getContextClassLoader())); if (this.isBootApplicationWithMain()) { @@ -98,11 +111,31 @@ class FunctionArchiveDeployer extends JarLauncher { } String[] functionClassNames = discoverFunctionClassName(functionProperties); - for (String functionClassName : functionClassNames) { - if (!StringUtils.isEmpty(functionClassName)) { - FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(functionClassName); - if (registration != null) { - functionRegistry.register(registration); + + if (functionClassNames == null) { + ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner((BeanDefinitionRegistry) applicationContext, false); + scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*"))); + Set findCandidateComponents = scanner.findCandidateComponents("functions"); + + ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); + for (BeanDefinition beanDefinition : findCandidateComponents) { + String className = beanDefinition.getBeanClassName(); + Class functionClass = currentClassLoader.loadClass(className); + if (Function.class.isAssignableFrom(functionClass) || Supplier.class.isAssignableFrom(functionClass) || Consumer.class.isAssignableFrom(functionClass)) { + FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(className); + if (registration != null) { + functionRegistry.register(registration); + } + } + } + } + else { + for (String functionClassName : functionClassNames) { + if (StringUtils.hasText(functionClassName)) { + FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(functionClassName); + if (registration != null) { + functionRegistry.register(registration); + } } } } @@ -184,9 +217,18 @@ class FunctionArchiveDeployer extends JarLauncher { private String[] discoverFunctionClassName(FunctionDeployerProperties functionProperties) { try { - return StringUtils.hasText(functionProperties.getFunctionClass()) - ? functionProperties.getFunctionClass().split(";") - : new String[] {this.getArchive().getManifest().getMainAttributes().getValue("Function-Class")}; + if (StringUtils.hasText(functionProperties.getFunctionClass())) { + return functionProperties.getFunctionClass().split(";"); + } + else if (StringUtils.hasText(this.getArchive().getManifest().getMainAttributes().getValue("Function-Class"))) { + return new String[] {this.getArchive().getManifest().getMainAttributes().getValue("Function-Class")}; + } + else { + return null; + } +// return StringUtils.hasText(functionProperties.getFunctionClass()) +// ? functionProperties.getFunctionClass().split(";") +// : new String[] {this.getArchive().getManifest().getMainAttributes().getValue("Function-Class")}; } catch (Exception e) { throw new IllegalStateException("Failed to discover function class name", e); diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java index 831d10aa4..033a1fc6b 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.cloud.deployer.resource.maven.MavenProperties; import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader; import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionRegistry; +import org.springframework.context.ApplicationContext; import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -68,7 +69,7 @@ public class FunctionDeployerConfiguration { @Bean SmartLifecycle functionArchiveUnDeployer(FunctionDeployerProperties functionProperties, - FunctionRegistry functionRegistry, ApplicationArguments arguments, @Nullable MavenProperties mavenProperties) { + FunctionRegistry functionRegistry, ApplicationArguments arguments, @Nullable MavenProperties mavenProperties, ApplicationContext applicationContext) { ApplicationArguments updatedArguments = this.updateArguments(arguments); @@ -103,7 +104,7 @@ public class FunctionDeployerConfiguration { if (logger.isInfoEnabled()) { logger.info("Deploying archive: " + functionProperties.getLocation()); } - deployer.deploy(functionRegistry, functionProperties, updatedArguments.getSourceArgs()); + deployer.deploy(functionRegistry, functionProperties, updatedArguments.getSourceArgs(), applicationContext); if (logger.isInfoEnabled()) { logger.info("Successfully deployed archive: " + functionProperties.getLocation()); } @@ -180,4 +181,5 @@ public class FunctionDeployerConfiguration { } } } + } diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java index 369c6dfb5..2bc6e341e 100644 --- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java @@ -123,6 +123,25 @@ public class FunctionDeployerTests { assertThat(results.get(1)).isEqualTo("STACY"); } + @Test + public void testWithSimplestJarComponentScanning() throws Exception { + String[] args = new String[] { + "--spring.cloud.function.location=target/it/simplestjarcs/target/simplestjarcs-1.0.0.RELEASE.jar"}; + + ApplicationContext context = SpringApplication.run(DeployerApplication.class, args); + FunctionCatalog catalog = context.getBean(FunctionCatalog.class); + Function function = catalog.lookup("upperCaseFunction"); + + assertThat(function.apply("bob")).isEqualTo("BOB"); + assertThat(function.apply("stacy")).isEqualTo("STACY"); + + Function, Flux> functionAsFlux = catalog.lookup("upperCaseFunction"); + + List results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block(); + assertThat(results.get(0)).isEqualTo("BOB"); + assertThat(results.get(1)).isEqualTo("STACY"); + } + @Test public void testWithSimplestJarExploaded() throws Exception { String[] args = new String[] { diff --git a/spring-cloud-function-samples/function-sample-aws/pom.xml b/spring-cloud-function-samples/function-sample-aws/pom.xml index 4cb79b9a0..e4383c2dc 100644 --- a/spring-cloud-function-samples/function-sample-aws/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws/pom.xml @@ -33,6 +33,14 @@ org.springframework.cloud spring-cloud-function-adapter-aws + + org.springframework.cloud + spring-cloud-function-web + + + org.springframework.boot + spring-boot-starter-web + com.amazonaws