diff --git a/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/pom.xml b/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/pom.xml
new file mode 100644
index 000000000..d564e7154
--- /dev/null
+++ b/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+ function.example
+ bootapp-with-javax
+ 0.0.1.BUILD-SNAPSHOT
+ jar
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.0.BUILD-SNAPSHOT
+
+
+
+
+ 1.8
+ 3.0.0.BUILD-SNAPSHOT
+ 1.0.17.RELEASE
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ com.sun.mail
+ javax.mail
+ 1.6.2
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ exec
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ unpack
+ package
+
+ unpack
+
+
+
+
+ ${project.groupId}
+ ${project.artifactId}
+ ${project.version}
+ exec
+
+
+
+
+
+
+
+
+
diff --git a/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/src/main/java/function/example/SimpleFunctionAppApplication.java b/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/src/main/java/function/example/SimpleFunctionAppApplication.java
new file mode 100644
index 000000000..ed8f694ef
--- /dev/null
+++ b/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/src/main/java/function/example/SimpleFunctionAppApplication.java
@@ -0,0 +1,57 @@
+package function.example;
+
+import java.util.function.Function;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+
+@SpringBootApplication
+public class SimpleFunctionAppApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(SimpleFunctionAppApplication.class, args);
+ }
+
+ @Bean
+ public Function uppercase() {
+ System.out.println("==> CREATING 'uppercase' FUNCTION bean");
+ return new UpperCaseFunction();
+ }
+
+ @Bean
+ public Function uppercasePerson() {
+ System.out.println("==> CREATING 'uppercasePerson' FUNCTION bean");
+ return person -> {
+ Person p = new Person();
+ p.setId(person.getId());
+ p.setName(person.getName().toUpperCase());
+ return p;
+ };
+ }
+
+
+
+ public static class Person {
+ private String name;
+
+ private int id;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ }
+}
diff --git a/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/src/main/java/function/example/UpperCaseFunction.java b/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/src/main/java/function/example/UpperCaseFunction.java
new file mode 100644
index 000000000..735ad65fb
--- /dev/null
+++ b/spring-cloud-function-deployer-new/src/it/bootapp-with-javax/src/main/java/function/example/UpperCaseFunction.java
@@ -0,0 +1,23 @@
+package function.example;
+
+import java.util.function.Function;
+
+import javax.mail.Address;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+
+public class UpperCaseFunction implements Function {
+
+ @Override
+ public String apply(String value) {
+ System.out.println("Uppercasing " + value);
+ try {
+ Address address = new InternetAddress(value);
+ }
+ catch (AddressException e) {
+ throw new IllegalStateException("Failed to create and address: ", e);
+ }
+ return value.toUpperCase();
+ }
+
+}
diff --git a/spring-cloud-function-deployer-new/src/it/bootjarnostart/pom.xml b/spring-cloud-function-deployer-new/src/it/bootjarnostart/pom.xml
index cc4a82d04..c65e89fb8 100644
--- a/spring-cloud-function-deployer-new/src/it/bootjarnostart/pom.xml
+++ b/spring-cloud-function-deployer-new/src/it/bootjarnostart/pom.xml
@@ -1,7 +1,7 @@
+ 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">
4.0.0
function.example
@@ -13,7 +13,7 @@
org.springframework.boot
spring-boot-starter-parent
2.2.0.BUILD-SNAPSHOT
-
+
@@ -39,6 +39,17 @@
exec
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ function.example.UpperCaseFunction
+
+
+
+
org.apache.maven.plugins
maven-dependency-plugin
diff --git a/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java b/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java
index 09cabfb27..0fc5f21c1 100644
--- a/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java
+++ b/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java
@@ -92,8 +92,9 @@ class FunctionArchiveDeployer extends JarLauncher {
}
}
- if (!StringUtils.isEmpty(functionProperties.getFunctionClass())) {
- FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(functionProperties.getFunctionClass());
+ String functionClassName = discoverFunctionClassName(functionProperties);
+ if (!StringUtils.isEmpty(functionClassName)) {
+ FunctionRegistration registration = this.discovereAndLoadFunctionFromClassName(functionClassName);
if (registration != null) {
functionRegistry.register(registration);
}
@@ -125,20 +126,28 @@ class FunctionArchiveDeployer extends JarLauncher {
.copyToByteArray(DeployerContextUtils.class.getClassLoader().getResourceAsStream(classAsPath));
/*
* While LaunchedURLClassLoader is completely disconnected with the current
- * class loader, this will ensure that classes in org.reactivestreams.* and reactor.*
- * are shared across two class loaders since they are effectively used as transport.
+ * class loader, this will ensure that certain classes (e.g., org.reactivestreams.* see #shouldLoadViaDeployerLoader() )
+ * are shared across two class loaders.
*/
this.archiveLoader = new LaunchedURLClassLoader(urls, null) {
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
Class> clazz = null;
if (shouldLoadViaDeployerLoader(name)) {
- clazz = getClass().getClassLoader().loadClass(name);
+ try {
+ clazz = getClass().getClassLoader().loadClass(name);
+ }
+ catch (Exception e) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Class '" + name + "' is not available in the current class loader. Loading it from the deployed archive.");
+ }
+ clazz = super.loadClass(name, false);
+ }
}
else if (name.equals(DeployerContextUtils.class.getName())) {
/*
* This will ensure that `DeployerContextUtils` is available to
- * foreign class loader even in cases where foreign JAR does not
+ * foreign class loader for cases where foreign JAR does not
* have SCF dependencies.
*/
try {
@@ -164,6 +173,17 @@ class FunctionArchiveDeployer extends JarLauncher {
|| name.startsWith("com.sun");
}
+ private String discoverFunctionClassName(FunctionProperties functionProperties) {
+ try {
+ return StringUtils.hasText(functionProperties.getFunctionClass())
+ ? functionProperties.getFunctionClass()
+ : this.getArchive().getManifest().getMainAttributes().getValue("Function-Class");
+ }
+ catch (Exception e) {
+ throw new IllegalStateException("Failed to discover function class name", e);
+ }
+ }
+
private boolean isBootApplicationWithMain() {
try {
return StringUtils.hasText(this.getArchive().getManifest().getMainAttributes().getValue("Start-Class"));
diff --git a/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java b/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java
index 280d27575..0741a5358 100644
--- a/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java
+++ b/spring-cloud-function-deployer-new/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java
@@ -23,13 +23,13 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.ApplicationArguments;
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
/**
*
@@ -38,9 +38,9 @@ import org.springframework.context.annotation.Bean;
* @since 3.0
*
*/
-@EnableAutoConfiguration
+@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(FunctionProperties.class)
-public final class FunctionDeployerConfiguration {
+public class FunctionDeployerConfiguration {
private static Log logger = LogFactory.getLog(FunctionDeployerConfiguration.class);
diff --git a/spring-cloud-function-deployer-new/src/main/resources/META-INF/spring.factories b/spring-cloud-function-deployer-new/src/main/resources/META-INF/spring.factories
new file mode 100644
index 000000000..84cb28fe3
--- /dev/null
+++ b/spring-cloud-function-deployer-new/src/main/resources/META-INF/spring.factories
@@ -0,0 +1 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.function.deployer.FunctionDeployerConfiguration
\ No newline at end of file
diff --git a/spring-cloud-function-deployer-new/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java b/spring-cloud-function-deployer-new/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java
index 53f79d3ec..7a80a34e3 100644
--- a/spring-cloud-function-deployer-new/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java
+++ b/spring-cloud-function-deployer-new/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java
@@ -27,6 +27,7 @@ import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.context.ApplicationContext;
import org.springframework.messaging.Message;
@@ -51,7 +52,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootjar/target/bootjar-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-class=function.example.UpperCaseFunction" };
- ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function function = catalog.lookup("upperCaseFunction");
@@ -75,7 +76,32 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootjarnostart/target/bootjarnostart-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-class=function.example.UpperCaseFunction" };
- ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
+ 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");
+ }
+
+ /*
+ * Target function `class UpperCaseFunction implements Function`
+ * No Main/Start class present, no Spring configuration
+ *
+ * Function class is discovered via 'Function-Class` manifest entry
+ */
+ @Test
+ public void testNoMainAndNoStartClassAndNoSpringConfigurationDiscoverClassFromManifest() throws Exception {
+ String[] args = new String[] {
+ "--spring.cloud.function.location=target/it/bootjarnostart/target/bootjarnostart-0.0.1.BUILD-SNAPSHOT-exec.jar" };
+
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function function = catalog.lookup("upperCaseFunction");
@@ -99,7 +125,7 @@ public class FunctionDeployerTests {
String[] args = new String[] {
"--spring.cloud.function.location=target/it/bootapp/target/bootapp-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=uppercase" };
- ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function, Message> function = catalog.lookup("uppercase", "application/json");
@@ -108,6 +134,25 @@ public class FunctionDeployerTests {
assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"BOB\"");
}
+ /*
+ * Same as above but:
+ * Given that Java 11 does not include 'javax' packages, this test simply validates that
+ * the delegation will be made to archive loader where it is available
+ */
+ @Test
+ public void testWithMainAndStartClassAndSpringConfigurationJavax() throws Exception {
+ String[] args = new String[] {
+ "--spring.cloud.function.location=target/it/bootapp-with-javax/target/bootapp-with-javax-0.0.1.BUILD-SNAPSHOT-exec.jar",
+ "--spring.cloud.function.function-name=uppercase" };
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
+ FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
+ Function, Message> function = catalog.lookup("uppercase", "application/json");
+
+ Message result = function
+ .apply(MessageBuilder.withPayload("\"foo@bar.com\"".getBytes(StandardCharsets.UTF_8)).build());
+ assertThat(new String(result.getPayload(), StandardCharsets.UTF_8)).isEqualTo("\"FOO@BAR.COM\"");
+ }
+
/*
* Target function:
*
@@ -120,7 +165,7 @@ public class FunctionDeployerTests {
String[] args = new String[] {
"--spring.cloud.function.location=target/it/bootapp-with-scf/target/bootapp-with-scf-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=uppercase" };
- ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function, Message> function = catalog.lookup("uppercase", "application/json");
@@ -140,7 +185,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootapp/target/bootapp-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=uppercasePerson" };
- ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function, Message> function = catalog.lookup("uppercasePerson", "application/json");
@@ -160,7 +205,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootapp-multi/target/bootapp-multi-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-name=fn"
};
- ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function>, Flux>>, Tuple2>, Flux>>> function =
@@ -202,7 +247,7 @@ public class FunctionDeployerTests {
"--spring.cloud.function.location=target/it/bootjar-multi/target/bootjar-multi-0.0.1.BUILD-SNAPSHOT-exec.jar",
"--spring.cloud.function.function-class=function.example.Repeater"
};
- ApplicationContext context = SpringApplication.run(FunctionDeployerConfiguration.class, args);
+ ApplicationContext context = SpringApplication.run(DeployerApplication.class, args);
FunctionCatalog catalog = context.getBean(FunctionCatalog.class);
Function>, Flux>>, Tuple2>, Flux>>> function =
@@ -233,4 +278,7 @@ public class FunctionDeployerTests {
assertThat(result2.get(1)).isEqualTo("2");
}
+ @SpringBootApplication(proxyBeanMethods = false)
+ private static class DeployerApplication {
+ }
}