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 { + } }