GH-714 Add support for component scanning to function deployer

We already support it for standard deployment, so this fix extends such support for functions deploye via function deployer feature

Resolves #714
This commit is contained in:
Oleg Zhurakousky
2021-07-16 12:34:35 +02:00
parent 2f8cd6c0c0
commit fb0bac0a96
7 changed files with 141 additions and 12 deletions

View File

@@ -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 To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate
autoconfiguration and supporting classes. 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 == 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]. 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]. 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 <<Function Component Scan>>. 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 ==== 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 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

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>function.example</groupId>
<artifactId>simplestjarcs</artifactId>
<version>1.0.0.RELEASE</version>
<description>Showcases compoinent scanning capabilities</description>
<packaging>jar</packaging>
<name>simplestjarcs</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package functions;
import java.util.function.Function;
public class UpperCaseFunction implements Function<String, String> {
@Override
public String apply(String value) {
System.out.println("Uppercasing " + value);
return value.toUpperCase();
}
}

View File

@@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference; 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.Log;
import org.apache.commons.logging.LogFactory; 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.SpringApplication;
import org.springframework.boot.loader.JarLauncher; import org.springframework.boot.loader.JarLauncher;
import org.springframework.boot.loader.LaunchedURLClassLoader; 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.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; 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.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -70,12 +80,15 @@ class FunctionArchiveDeployer extends JarLauncher {
} }
@SuppressWarnings({ "unchecked", "rawtypes" }) @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(); ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
try { try {
ClassLoader cl = createClassLoader(discoverClassPathAcrhives().iterator()); ClassLoader cl = createClassLoader(discoverClassPathAcrhives().iterator());
Thread.currentThread().setContextClassLoader(cl); Thread.currentThread().setContextClassLoader(cl);
evalContext.setTypeLocator(new StandardTypeLocator(Thread.currentThread().getContextClassLoader())); evalContext.setTypeLocator(new StandardTypeLocator(Thread.currentThread().getContextClassLoader()));
if (this.isBootApplicationWithMain()) { if (this.isBootApplicationWithMain()) {
@@ -98,11 +111,31 @@ class FunctionArchiveDeployer extends JarLauncher {
} }
String[] functionClassNames = discoverFunctionClassName(functionProperties); String[] functionClassNames = discoverFunctionClassName(functionProperties);
for (String functionClassName : functionClassNames) {
if (!StringUtils.isEmpty(functionClassName)) { if (functionClassNames == null) {
FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(functionClassName); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner((BeanDefinitionRegistry) applicationContext, false);
if (registration != null) { scanner.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*")));
functionRegistry.register(registration); Set<BeanDefinition> 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) { private String[] discoverFunctionClassName(FunctionDeployerProperties functionProperties) {
try { try {
return StringUtils.hasText(functionProperties.getFunctionClass()) if (StringUtils.hasText(functionProperties.getFunctionClass())) {
? functionProperties.getFunctionClass().split(";") return functionProperties.getFunctionClass().split(";");
: new String[] {this.getArchive().getManifest().getMainAttributes().getValue("Function-Class")}; }
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) { catch (Exception e) {
throw new IllegalStateException("Failed to discover function class name", e); throw new IllegalStateException("Failed to discover function class name", e);

View File

@@ -37,6 +37,7 @@ import org.springframework.cloud.deployer.resource.maven.MavenProperties;
import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader; import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader;
import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle; import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -68,7 +69,7 @@ public class FunctionDeployerConfiguration {
@Bean @Bean
SmartLifecycle functionArchiveUnDeployer(FunctionDeployerProperties functionProperties, 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); ApplicationArguments updatedArguments = this.updateArguments(arguments);
@@ -103,7 +104,7 @@ public class FunctionDeployerConfiguration {
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info("Deploying archive: " + functionProperties.getLocation()); logger.info("Deploying archive: " + functionProperties.getLocation());
} }
deployer.deploy(functionRegistry, functionProperties, updatedArguments.getSourceArgs()); deployer.deploy(functionRegistry, functionProperties, updatedArguments.getSourceArgs(), applicationContext);
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
logger.info("Successfully deployed archive: " + functionProperties.getLocation()); logger.info("Successfully deployed archive: " + functionProperties.getLocation());
} }
@@ -180,4 +181,5 @@ public class FunctionDeployerConfiguration {
} }
} }
} }
} }

View File

@@ -123,6 +123,25 @@ public class FunctionDeployerTests {
assertThat(results.get(1)).isEqualTo("STACY"); 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<String, String> function = catalog.lookup("upperCaseFunction");
assertThat(function.apply("bob")).isEqualTo("BOB");
assertThat(function.apply("stacy")).isEqualTo("STACY");
Function<Flux<String>, Flux<String>> functionAsFlux = catalog.lookup("upperCaseFunction");
List<String> results = functionAsFlux.apply(Flux.just("bob", "stacy")).collectList().block();
assertThat(results.get(0)).isEqualTo("BOB");
assertThat(results.get(1)).isEqualTo("STACY");
}
@Test @Test
public void testWithSimplestJarExploaded() throws Exception { public void testWithSimplestJarExploaded() throws Exception {
String[] args = new String[] { String[] args = new String[] {

View File

@@ -33,6 +33,14 @@
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-aws</artifactId> <artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>com.amazonaws</groupId>