From c6736f959b4e2cc87ff1558705ee85a5998ab11d Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 16 Dec 2016 11:17:23 +0000 Subject: [PATCH] Add a sample app with just beans that are Functions Make it deployable via its maven coordinates in spring-cloud-function-deployer (it is deployed by default on start up right now, but that's just a demo) --- README.adoc | 5 +- pom.xml | 4 + scripts/web.sh | 15 +- spring-cloud-function-context/.jdk8 | 0 spring-cloud-function-context/pom.xml | 58 ++++++ .../ApplicationContextFunctionCatalog.java | 65 ++++++ ...ntextFunctionCatalogAutoConfiguration.java | 51 +++++ .../main/resources/META-INF/spring.factories | 2 + spring-cloud-function-core/pom.xml | 11 + ...aultFunctionRegistryAutoConfiguration.java | 33 +++ .../function/registry/FunctionCatalog.java | 35 ++++ .../function/registry/FunctionRegistry.java | 13 +- .../main/resources/META-INF/spring.factories | 2 + spring-cloud-function-deployer/.jdk8 | 0 spring-cloud-function-deployer/pom.xml | 96 +++++++++ .../function/deployer/ApplicationRunner.java | 189 ++++++++++++++++++ .../deployer/DeployedFunctionApplication.java | 26 +++ .../deployer/DeployedFunctionController.java | 66 ++++++ .../deployer/FunctionAdminController.java | 145 ++++++++++++++ .../FunctionExtractingAppDeployer.java | 183 +++++++++++++++++ .../FunctionExtractingAutoConfiguration.java | 38 ++++ .../main/resources/META-INF/spring.factories | 2 + ...ExtractingAppDeployerIntegrationTests.java | 57 ++++++ .../FunctionExtractingAppDeployerTests.java | 96 +++++++++ .../thin-slim.properties | 13 ++ spring-cloud-function-samples/pom.xml | 19 ++ .../spring-cloud-function-sample/.jdk8 | 0 .../spring-cloud-function-sample/pom.xml | 77 +++++++ .../java/com/example/SampleApplication.java | 49 +++++ spring-cloud-function-stream/pom.xml | 7 + .../function/stream/StreamConfiguration.java | 16 +- .../main/resources/META-INF/spring.factories | 2 + spring-cloud-function-task/pom.xml | 7 + .../function/task/TaskConfiguration.java | 24 +-- .../main/resources/META-INF/spring.factories | 2 + spring-cloud-function-web/pom.xml | 7 + .../cloud/function/web/RestConfiguration.java | 56 +++--- .../web/WebConfigurationProperties.java | 15 +- .../main/resources/META-INF/spring.factories | 2 + 39 files changed, 1404 insertions(+), 84 deletions(-) create mode 100644 spring-cloud-function-context/.jdk8 create mode 100644 spring-cloud-function-context/pom.xml create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ApplicationContextFunctionCatalog.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java create mode 100644 spring-cloud-function-context/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/DefaultFunctionRegistryAutoConfiguration.java create mode 100644 spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionCatalog.java create mode 100644 spring-cloud-function-core/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-function-deployer/.jdk8 create mode 100644 spring-cloud-function-deployer/pom.xml create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionApplication.java create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionController.java create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionAdminController.java create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployer.java create mode 100644 spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAutoConfiguration.java create mode 100644 spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerIntegrationTests.java create mode 100644 spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerTests.java create mode 100644 spring-cloud-function-deployer/thin-slim.properties create mode 100644 spring-cloud-function-samples/pom.xml create mode 100644 spring-cloud-function-samples/spring-cloud-function-sample/.jdk8 create mode 100644 spring-cloud-function-samples/spring-cloud-function-sample/pom.xml create mode 100644 spring-cloud-function-samples/spring-cloud-function-sample/src/main/java/com/example/SampleApplication.java create mode 100644 spring-cloud-function-stream/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-function-task/src/main/resources/META-INF/spring.factories create mode 100644 spring-cloud-function-web/src/main/resources/META-INF/spring.factories diff --git a/README.adoc b/README.adoc index 73425850e..7780b0b2f 100644 --- a/README.adoc +++ b/README.adoc @@ -13,7 +13,8 @@ == Run a REST Microservice using that Function: ---- -./web.sh -p /words -f uppercase +./web.sh +curl -H "Content-Type=text/plain" localhost:8080/uppercase -d foo ---- == Compose Functions: @@ -23,7 +24,7 @@ ---- ./registerFunction.sh -n pluralize -f "f->f.map(s->s+\"S\")" -./web.sh -p /words -f uppercase,pluralize +curl -H "Content-Type=text/plain" localhost:8080/uppercase,pluralize -d foo ---- == Run a Task Microservice using a Supplier, Function, and Consumer: diff --git a/pom.xml b/pom.xml index bb6ea0e29..cb83c05de 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,7 @@ 3.0.4.RELEASE 1.1.0.BUILD-SNAPSHOT 0.0.1.BUILD-SNAPSHOT + 1.5.0.BUILD-SNAPSHOT @@ -39,9 +40,12 @@ spring-cloud-function-compiler spring-cloud-function-core + spring-cloud-function-context spring-cloud-function-stream spring-cloud-function-task spring-cloud-function-web + spring-cloud-function-samples + spring-cloud-function-deployer diff --git a/scripts/web.sh b/scripts/web.sh index 6a2ad038e..4a1ed9d14 100755 --- a/scripts/web.sh +++ b/scripts/web.sh @@ -1,17 +1,4 @@ #!/bin/bash -while getopts ":p:f:" opt; do - case $opt in - p) - WEBPATH=$OPTARG - ;; - f) - FUNC=$OPTARG - ;; - esac -done - -java -noverify -XX:TieredStopAtLevel=1 -Xss256K -Xms16M -Xmx256M -XX:MaxMetaspaceSize=128M -jar ../spring-cloud-function-web/target/spring-cloud-function-web-1.0.0.BUILD-SNAPSHOT.jar\ - --web.path=$WEBPATH\ - --function.name=$FUNC +java -jar ../spring-cloud-function-web/target/spring-cloud-function-web-1.0.0.BUILD-SNAPSHOT.jar ${@} diff --git a/spring-cloud-function-context/.jdk8 b/spring-cloud-function-context/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-function-context/pom.xml b/spring-cloud-function-context/pom.xml new file mode 100644 index 000000000..d182403c7 --- /dev/null +++ b/spring-cloud-function-context/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + spring-cloud-function-context + jar + spring-cloud-function-context + Spring Cloud Function Web Support + + + org.springframework.cloud + spring-cloud-function-parent + 1.0.0.BUILD-SNAPSHOT + + + + 1.8 + 1.0.0.BUILD-SNAPSHOT + 0.0.1.BUILD-SNAPSHOT + + + + + org.springframework.cloud + spring-cloud-function-core + ${spring-cloud-function.version} + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.0.0.BUILD-SNAPSHOT + pom + import + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.0 + + + + + diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ApplicationContextFunctionCatalog.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ApplicationContextFunctionCatalog.java new file mode 100644 index 000000000..230ef7dfb --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ApplicationContextFunctionCatalog.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016 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 + * + * http://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; + +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.cloud.function.registry.FunctionCatalog; + +public class ApplicationContextFunctionCatalog implements FunctionCatalog { + + private final Map> functions; + private final Map> consumers; + private final Map> suppliers; + + public ApplicationContextFunctionCatalog(Map> functions, + Map> consumers, Map> suppliers) { + this.functions = functions; + this.consumers = consumers; + this.suppliers = suppliers; + } + + @SuppressWarnings("unchecked") + @Override + public Consumer lookupConsumer(String name) { + return (Consumer) consumers.get(name); + } + + @SuppressWarnings("unchecked") + @Override + public Function lookupFunction(String name) { + return (Function) functions.get(name); + } + + @Override + public Function composeFunction(String... functionNames) { + Function function = this.lookupFunction(functionNames[0]); + for (int i = 1; i < functionNames.length; i++) { + function = function.andThen(this.lookupFunction(functionNames[i])); + } + return function; + } + + @SuppressWarnings("unchecked") + @Override + public Supplier lookupSupplier(String name) { + return (Supplier) suppliers.get(name); + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java new file mode 100644 index 000000000..ac1911c97 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016 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 + * + * http://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; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.function.registry.DefaultFunctionRegistryAutoConfiguration; +import org.springframework.cloud.function.registry.FunctionCatalog; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass(ApplicationContextFunctionCatalog.class) +@ConditionalOnMissingBean(FunctionCatalog.class) +@AutoConfigureBefore(DefaultFunctionRegistryAutoConfiguration.class) +public class ContextFunctionCatalogAutoConfiguration { + + @Autowired(required = false) + private Map> functions = Collections.emptyMap(); + @Autowired(required = false) + private Map> consumers = Collections.emptyMap(); + @Autowired(required = false) + private Map> suppliers = Collections.emptyMap(); + + @Bean + public FunctionCatalog functionCatalog() { + return new ApplicationContextFunctionCatalog(functions, consumers, suppliers); + } + +} diff --git a/spring-cloud-function-context/src/main/resources/META-INF/spring.factories b/spring-cloud-function-context/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..d00ef6707 --- /dev/null +++ b/spring-cloud-function-context/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-function-core/pom.xml b/spring-cloud-function-core/pom.xml index 8b882d82e..cbf02fd29 100644 --- a/spring-cloud-function-core/pom.xml +++ b/spring-cloud-function-core/pom.xml @@ -18,6 +18,10 @@ + + org.springframework.boot + spring-boot-starter + org.springframework.cloud spring-cloud-function-compiler @@ -46,6 +50,13 @@ registrar + + + org.springframework.boot.experimental + spring-boot-thin-launcher + ${wrapper.version} + + diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/DefaultFunctionRegistryAutoConfiguration.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/DefaultFunctionRegistryAutoConfiguration.java new file mode 100644 index 000000000..2ff70c9d9 --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/DefaultFunctionRegistryAutoConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016 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 + * + * http://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.registry; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnClass(FileSystemFunctionRegistry.class) +@ConditionalOnMissingBean(FunctionCatalog.class) +public class DefaultFunctionRegistryAutoConfiguration { + + @Bean + public FunctionRegistry functionRegistry() { + return new FileSystemFunctionRegistry(); + } + +} diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionCatalog.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionCatalog.java new file mode 100644 index 000000000..f1a48519e --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionCatalog.java @@ -0,0 +1,35 @@ +/* + * Copyright 2016 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 + * + * http://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.registry; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * @author Dave Syer + */ +public interface FunctionCatalog { + + Consumer lookupConsumer(String name); + + Function lookupFunction(String name); + + Function composeFunction(String... functionNames); + + Supplier lookupSupplier(String name); +} diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java index 8fc8bc685..e58e7639f 100644 --- a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/registry/FunctionRegistry.java @@ -16,14 +16,10 @@ package org.springframework.cloud.function.registry; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - /** * @author Mark Fisher */ -public interface FunctionRegistry { +public interface FunctionRegistry extends FunctionCatalog { void registerConsumer(String name, String consumer); @@ -31,11 +27,4 @@ public interface FunctionRegistry { void registerSupplier(String name, String supplier); - Consumer lookupConsumer(String name); - - Function lookupFunction(String name); - - Function composeFunction(String... functionNames); - - Supplier lookupSupplier(String name); } diff --git a/spring-cloud-function-core/src/main/resources/META-INF/spring.factories b/spring-cloud-function-core/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..106bdaa49 --- /dev/null +++ b/spring-cloud-function-core/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.function.registry.DefaultFunctionRegistryAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-function-deployer/.jdk8 b/spring-cloud-function-deployer/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml new file mode 100644 index 000000000..f7853a206 --- /dev/null +++ b/spring-cloud-function-deployer/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + spring-cloud-function-deployer + jar + spring-cloud-function-deployer + Spring Cloud Function Web Support + + + org.springframework.cloud + spring-cloud-function-parent + 1.0.0.BUILD-SNAPSHOT + + + + 1.8 + 0.0.1.BUILD-SNAPSHOT + + + + + org.springframework.cloud + spring-cloud-function-core + ${project.version} + + + org.springframework.boot.experimental + spring-boot-starter-web-reactive + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.cloud + spring-cloud-deployer-thin + ${spring-cloud-deployer-thin.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.0.0.BUILD-SNAPSHOT + pom + import + + + org.springframework.boot.experimental + spring-boot-dependencies-web-reactive + 0.1.0.BUILD-SNAPSHOT + pom + import + + + org.springframework.cloud + spring-cloud-function-parent + ${project.version} + pom + import + + + org.springframework.cloud + spring-cloud-dependencies + Dalston.BUILD-SNAPSHOT + pom + import + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.0 + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java new file mode 100644 index 000000000..b120a3e7c --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java @@ -0,0 +1,189 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import java.io.File; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.PreDestroy; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.graph.Dependency; +import org.eclipse.aether.resolution.ArtifactResolutionException; + +import org.springframework.boot.Banner.Mode; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.cli.compiler.RepositoryConfigurationFactory; +import org.springframework.boot.cli.compiler.grape.DependencyResolutionContext; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.thin.AetherEngine; +import org.springframework.boot.loader.thin.ArchiveUtils; +import org.springframework.cloud.deployer.thin.ContextRunner; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import reactor.core.publisher.Flux; + +/** + * @author Dave Syer + * + */ +// NOT a @Component (to prevent it from being scanned by the "main" application). +public class ApplicationRunner implements CommandLineRunner { + + private static final String DEFAULT_REACTOR_VERSION = "3.0.3.RELEASE"; + + private static Log logger = LogFactory.getLog(ApplicationRunner.class); + + public static void main(String[] args) { + new ApplicationRunner().start(args); + } + + public ConfigurableApplicationContext start(String... args) { + return new SpringApplicationBuilder(ApplicationRunner.class).web(false) + .contextClass(AnnotationConfigApplicationContext.class) + .bannerMode(Mode.OFF).properties("spring.main.applicationContextClass=" + + AnnotationConfigApplicationContext.class.getName()) + .run(args); + } + + private Object app; + + @Override + public void run(String... args) { + ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + try { + ClassLoader classLoader = createClassLoader(); + ClassUtils.overrideThreadContextClassLoader(classLoader); + Class cls = classLoader.loadClass(ContextRunner.class.getName()); + this.app = cls.newInstance(); + runContext(DeployedFunctionApplication.class.getName(), + Collections.emptyMap(), args); + } + catch (Exception e) { + logger.error("Cannot deploy", e); + } + finally { + ClassUtils.overrideThreadContextClassLoader(contextLoader); + } + } + + @PreDestroy + public void close() { + closeContext(); + } + + private void runContext(String mainClass, Map properties, + String... args) { + Method method = ReflectionUtils.findMethod(this.app.getClass(), "run", + String.class, Map.class, String[].class); + ReflectionUtils.invokeMethod(method, this.app, mainClass, properties, args); + } + + private void closeContext() { + Method method = ReflectionUtils.findMethod(this.app.getClass(), "close"); + ReflectionUtils.invokeMethod(method, this.app); + } + + private ClassLoader createClassLoader() { + ClassLoader base = getClass().getClassLoader(); + if (!(base instanceof URLClassLoader)) { + throw new IllegalStateException("Need a URL class loader, found: " + base); + } + @SuppressWarnings("resource") + URLClassLoader urlClassLoader = (URLClassLoader) base; + URL[] urls = urlClassLoader.getURLs(); + List child = new ArrayList<>(); + List parent = new ArrayList<>(); + for (URL url : urls) { + child.add(url); + } + String reactor = getReactorCoordinates(); + DependencyResolutionContext context = new DependencyResolutionContext(); + AetherEngine engine = AetherEngine.create( + RepositoryConfigurationFactory.createDefaultRepositoryConfiguration(), + context); + try { + List resolved = engine.resolve(Arrays + .asList(new Dependency(new DefaultArtifact(reactor), "runtime"))); + for (File archive : resolved) { + try { + URL url = archive.toURI().toURL(); + parent.add(url); + child.remove(url); + } + catch (MalformedURLException e) { + throw new IllegalStateException("Cannot locate jar for: " + archive); + } + } + } + catch (ArtifactResolutionException e) { + throw new IllegalStateException("Cannot resolve archive for " + reactor, e); + } + logger.info("Parent: " + parent); + logger.info("Child: " + child); + if (!parent.isEmpty()) { + base = new URLClassLoader(parent.toArray(new URL[0]), base.getParent()); + } + return new URLClassLoader(child.toArray(new URL[0]), base); + } + + private String getReactorCoordinates() { + Package pkg = Flux.class.getPackage(); + String version = null; + version = (pkg != null ? pkg.getImplementationVersion() + : DEFAULT_REACTOR_VERSION); + if (version == null) { + Archive archive = ArchiveUtils.getArchive(Flux.class); + try { + String path = archive.getUrl().toString(); + if (path.endsWith("!/")) { + path = path.substring(0, path.length() - 2); + } + path = StringUtils.getFilename(path); + if (path.startsWith("reactor-core-")) { + path = path.substring("reactor-core-".length()); + } + if (path.endsWith(".jar")) { + path = path.substring(0, path.length() - ".jar".length()); + } + version = path; + } + catch (MalformedURLException e) { + // ignore + } + } + if (version == null) { + version = DEFAULT_REACTOR_VERSION; + } + return "io.projectreactor:reactor-core:" + version; + } + +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionApplication.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionApplication.java new file mode 100644 index 000000000..e31b19aa2 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionApplication.java @@ -0,0 +1,26 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Dave Syer + * + */ +@SpringBootApplication +public class DeployedFunctionApplication { +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionController.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionController.java new file mode 100644 index 000000000..dc2cf1c42 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/DeployedFunctionController.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import java.util.function.Function; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import reactor.core.publisher.Flux; + +/** + * @author Dave Syer + * + */ +@RestController +public class DeployedFunctionController { + + private final FunctionExtractingAppDeployer deployer; + + @Autowired + public DeployedFunctionController(FunctionExtractingAppDeployer deployer) { + this.deployer = deployer; + } + + @PostMapping(path = "/{name}", consumes = MediaType.TEXT_PLAIN_VALUE) + public Flux function(@PathVariable String name, + @RequestBody Flux body) { + Function function; + if (name.contains(",")) { + function = deployer.composeFunction(name.split(",")); + } + else { + function = deployer.lookupFunction(name); + } + @SuppressWarnings("unchecked") + Flux result = (Flux) function.apply(body); + return result; + } + + @GetMapping("/{name}") + public Flux supplier(@PathVariable String name) { + @SuppressWarnings("unchecked") + Flux result = (Flux) deployer.lookupSupplier(name).get(); + return result; + } + +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionAdminController.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionAdminController.java new file mode 100644 index 000000000..903127b86 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionAdminController.java @@ -0,0 +1,145 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.loader.thin.ArchiveUtils; +import org.springframework.cloud.deployer.spi.core.AppDefinition; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Dave Syer + * + */ +@RestController +@RequestMapping("/admin") +public class FunctionAdminController implements CommandLineRunner { + + private final FunctionExtractingAppDeployer deployer; + + private Map deployed = new LinkedHashMap<>(); + + private Map names = new LinkedHashMap<>(); + + @Autowired + public FunctionAdminController(FunctionExtractingAppDeployer deployer) { + this.deployer = deployer; + } + + @PostMapping(path = "/{name}") + public Map push(@PathVariable String name, @RequestParam String path) + throws Exception { + String id = deploy(name, path); + return Collections.singletonMap("id", id); + } + + @DeleteMapping(path = "/{name}") + public Map undeploy(@PathVariable String name, + @RequestParam String path) throws Exception { + String id = names.get(name); + if (id == null) { + throw new IllegalStateException("No such app"); + } + deployer.undeploy(id); + names.remove(name); + deployed.remove(id); + return Collections.singletonMap("id", id); + } + + @GetMapping({ "", "/" }) + public Map deployed() { + Map result = new LinkedHashMap<>(); + for (String name : names.keySet()) { + result.put(name, new DeployedArtifact(name, names.get(name), + deployed.get(names.get(name)))); + } + return result; + } + + @Override + public void run(String... args) throws Exception { + deploy("sample", "maven://com.example:function-sample:1.0.0.BUILD-SNAPSHOT"); + } + + private String deploy(String name, String path, String... args) throws Exception { + Resource resource = new FileSystemResource( + ArchiveUtils.getArchiveRoot(ArchiveUtils.getArchive(path))); + AppDefinition definition = new AppDefinition(resource.getFilename(), + Collections.emptyMap()); + AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, + Collections.emptyMap(), Arrays.asList(args)); + String deployed = deployer.deploy(request); + this.deployed.put(deployed, path); + this.names.put(deployed, name); + return deployed; + } +} + +class DeployedArtifact { + + private String name; + private String id; + private String path; + + public DeployedArtifact() { + } + + public DeployedArtifact(String name, String id, String path) { + this.name = name; + this.id = id; + this.path = path; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployer.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployer.java new file mode 100644 index 000000000..34933441f --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployer.java @@ -0,0 +1,183 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.cloud.deployer.thin.ThinJarAppDeployer; +import org.springframework.cloud.deployer.thin.ThinJarAppWrapper; +import org.springframework.cloud.function.registry.FunctionCatalog; +import org.springframework.util.MethodInvoker; +import org.springframework.util.ReflectionUtils; + +public class FunctionExtractingAppDeployer extends ThinJarAppDeployer + implements FunctionCatalog { + + private static final Log logger = LogFactory + .getLog(FunctionExtractingAppDeployer.class); + + private final Map> functions = new HashMap<>(); + private final Map> consumers = new HashMap<>(); + private final Map> suppliers = new HashMap<>(); + + public FunctionExtractingAppDeployer() { + this("thin", "slim"); + } + + public FunctionExtractingAppDeployer(String name, String... profiles) { + super(name, profiles); + } + + @SuppressWarnings("unchecked") + @Override + public Consumer lookupConsumer(String name) { + return (Consumer) consumers.get(name); + } + + @SuppressWarnings("unchecked") + @Override + public Function lookupFunction(String name) { + return (Function) functions.get(name); + } + + @Override + public Function composeFunction(String... functionNames) { + Function function = this.lookupFunction(functionNames[0]); + for (int i = 1; i < functionNames.length; i++) { + function = function.andThen(this.lookupFunction(functionNames[i])); + } + return function; + } + + @SuppressWarnings("unchecked") + @Override + public Supplier lookupSupplier(String name) { + return (Supplier) suppliers.get(name); + } + + @Override + public String deploy(AppDeploymentRequest request) { + String id = super.deploy(request); + functions.putAll(functions(id)); + suppliers.putAll(suppliers(id)); + consumers.putAll(consumers(id)); + return id; + } + + @Override + public void undeploy(String id) { + super.undeploy(id); + for (String name : functions(id).keySet()) { + functions.remove(name); + } + for (String name : suppliers(id).keySet()) { + suppliers.remove(name); + } + for (String name : consumers(id).keySet()) { + consumers.remove(name); + } + } + + private Map> functions(String id) { + Map> map = new HashMap<>(); + ThinJarAppWrapper wrapper = getWrapper(id); + if (wrapper == null) { + return map; + } + try { + @SuppressWarnings("unchecked") + Map> result = (Map>) getBeans( + wrapper, Function.class); + map.putAll(result); + } + catch (Exception e) { + throw new IllegalStateException("Cannot extract functions", e); + } + logger.info("Loaded functions: " + map.keySet()); + return map; + } + + private Map> consumers(String id) { + Map> map = new HashMap<>(); + ThinJarAppWrapper wrapper = getWrapper(id); + if (wrapper == null) { + return map; + } + try { + @SuppressWarnings("unchecked") + Map> result = (Map>) getBeans( + wrapper, Consumer.class); + map.putAll(result); + } + catch (Exception e) { + throw new IllegalStateException("Cannot extract consumers", e); + } + logger.info("Loaded consumers: " + map.keySet()); + return map; + } + + private Map> suppliers(String id) { + Map> map = new HashMap<>(); + ThinJarAppWrapper wrapper = getWrapper(id); + if (wrapper == null) { + return map; + } + try { + @SuppressWarnings("unchecked") + Map> result = (Map>) getBeans( + wrapper, Supplier.class); + map.putAll(result); + } + catch (Exception e) { + throw new IllegalStateException("Cannot extract suppliers", e); + } + logger.info("Loaded suppliers: " + map.keySet()); + return map; + } + + private Map getBeans(ThinJarAppWrapper wrapper, + Class type) throws IllegalAccessException, ClassNotFoundException, + NoSuchMethodException, InvocationTargetException { + Object app = findContext(wrapper); + MethodInvoker invoker = new MethodInvoker(); + invoker.setTargetObject(app); + invoker.setTargetMethod("getBeansOfType"); + invoker.setArguments(new Object[] { type }); + invoker.prepare(); + @SuppressWarnings("unchecked") + Map result = (Map) invoker.invoke(); + return result; + } + + private Object findContext(ThinJarAppWrapper wrapper) { + Object app = wrapper.getApp(); + Field field = ReflectionUtils.findField(app.getClass(), "context"); + ReflectionUtils.makeAccessible(field); + app = ReflectionUtils.getField(field, app); + return app; + } + +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAutoConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAutoConfiguration.java new file mode 100644 index 000000000..6a2ff4f5c --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionExtractingAutoConfiguration.java @@ -0,0 +1,38 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.function.registry.DefaultFunctionRegistryAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnClass(FunctionExtractingAppDeployer.class) +@AutoConfigureBefore(DefaultFunctionRegistryAutoConfiguration.class) +public class FunctionExtractingAutoConfiguration { + + @Bean + public FunctionExtractingAppDeployer functionCatalog() { + return new FunctionExtractingAppDeployer(); + } + +} diff --git a/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories b/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..f63ca8973 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.function.deployer.FunctionExtractingAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerIntegrationTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerIntegrationTests.java new file mode 100644 index 000000000..f502dc6d8 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.SocketUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +public class FunctionExtractingAppDeployerIntegrationTests { + + private static ConfigurableApplicationContext context; + private static int port; + + @BeforeClass + public static void open() { + port = SocketUtils.findAvailableTcpPort(); + context = new ApplicationRunner().start("--server.port=" + port); + } + + @AfterClass + public static void close() { + if (context != null) { + context.close(); + } + } + + @Test + public void test() { + assertThat(new TestRestTemplate() + .getForObject("http://localhost:" + port + "/words", String.class)) + .isEqualTo("foobar"); + } + +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerTests.java new file mode 100644 index 000000000..d681dfd2b --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionExtractingAppDeployerTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2017 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 + * + * http://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.deployer; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.boot.loader.thin.ArchiveUtils; +import org.springframework.boot.loader.tools.LogbackInitializer; +import org.springframework.cloud.deployer.spi.app.DeploymentState; +import org.springframework.cloud.deployer.spi.core.AppDefinition; +import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import reactor.core.publisher.Flux; + +/** + * @author Dave Syer + * + */ +public class FunctionExtractingAppDeployerTests { + + private static String id; + + static { + LogbackInitializer.initialize(); + } + + private static FunctionExtractingAppDeployer deployer = new FunctionExtractingAppDeployer(); + + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Before + public void init() throws Exception { + if (id == null) { + id = deploy("maven://com.example:function-sample:1.0.0.BUILD-SNAPSHOT"); + // "--debug"); + } + assertThat(deployer.status(id).getState()).isEqualTo(DeploymentState.deployed); + } + + @Test + public void deployAndExtractFunctions() throws Exception { + // This one can only work if you change the boot classpath to contain reactor-core + // and reactive-streams + expected.expect(ClassCastException.class); + @SuppressWarnings("unchecked") + Flux result = (Flux) deployer.lookupFunction("uppercase") + .apply(Flux.just("foo")); + assertThat(result.blockFirst()).isEqualTo("FOO"); + } + + @Test + public void deployAndExtractConsumers() throws Exception { + assertThat(deployer.lookupConsumer("sink")).isNull(); + } + + @Test + public void deployAndExtractSuppliers() throws Exception { + assertThat(deployer.lookupSupplier("words")).isNotNull(); + } + + private String deploy(String jarName, String... args) throws Exception { + Resource resource = new FileSystemResource( + ArchiveUtils.getArchiveRoot(ArchiveUtils.getArchive(jarName))); + AppDefinition definition = new AppDefinition(resource.getFilename(), + Collections.emptyMap()); + AppDeploymentRequest request = new AppDeploymentRequest(definition, resource, + Collections.emptyMap(), Arrays.asList(args)); + String deployed = deployer.deploy(request); + return deployed; + } + +} diff --git a/spring-cloud-function-deployer/thin-slim.properties b/spring-cloud-function-deployer/thin-slim.properties new file mode 100644 index 000000000..734ae153e --- /dev/null +++ b/spring-cloud-function-deployer/thin-slim.properties @@ -0,0 +1,13 @@ +exclusions.spring-web-reactive: org.springframework:spring-web-reactive +exclusions.spring-web: org.springframework:spring-web +exclusions.reator-netty: io.projectreactor.ipc:reactor-netty +exclusions.spring-cloud-stream: org.springframework.cloud:spring-cloud-stream +exclusions.spring-cloud-stream-reactive: org.springframework.cloud:spring-cloud-stream-reactive +exclusions.spring-cloud-stream-binder-rabbit: org.springframework.cloud:spring-cloud-stream-binder-rabbit +exclusions.spring-cloud-stream-binder-kafka: org.springframework.cloud:spring-cloud-stream-binder-kafka +exclusions.spring-boot-starter-web: org.springframework.boot:spring-boot-starter-web +exclusions.spring-boot-starter-stream: org.springframework.boot:spring-boot-starter-stream +exclusions.spring-boot-starter-actuator: org.springframework.boot:spring-boot-starter-actuator +dependencies.spring-boot-starter: org.springframework.boot:spring-boot-starter +dependencies.spring-cloud-function-context: org.springframework.cloud:spring-cloud-function-context:1.0.0.BUILD-SNAPSHOT + diff --git a/spring-cloud-function-samples/pom.xml b/spring-cloud-function-samples/pom.xml new file mode 100644 index 000000000..c89b15c20 --- /dev/null +++ b/spring-cloud-function-samples/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + spring-cloud-function-samples + Spring Cloud Function Samples + pom + + + org.springframework.cloud + spring-cloud-function-parent + 1.0.0.BUILD-SNAPSHOT + + + + spring-cloud-function-sample + + + diff --git a/spring-cloud-function-samples/spring-cloud-function-sample/.jdk8 b/spring-cloud-function-samples/spring-cloud-function-sample/.jdk8 new file mode 100644 index 000000000..e69de29bb diff --git a/spring-cloud-function-samples/spring-cloud-function-sample/pom.xml b/spring-cloud-function-samples/spring-cloud-function-sample/pom.xml new file mode 100644 index 000000000..90f38a29c --- /dev/null +++ b/spring-cloud-function-samples/spring-cloud-function-sample/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + com.example + function-sample + 1.0.0.BUILD-SNAPSHOT + jar + spring-cloud-function-sample + Spring Cloud Function Web Support + + + org.springframework.boot + spring-boot-starter-parent + 2.0.0.BUILD-SNAPSHOT + + + + 1.8 + 1.0.0.BUILD-SNAPSHOT + 0.0.1.BUILD-SNAPSHOT + + + + + org.springframework.cloud + spring-cloud-function-web + ${spring-cloud-function.version} + + + org.springframework.cloud + spring-cloud-function-context + ${spring-cloud-function.version} + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + org.springframework.boot + spring-boot-dependencies + 2.0.0.BUILD-SNAPSHOT + pom + import + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.0.0 + + + org.springframework.boot + spring-boot-maven-plugin + 1.5.0.BUILD-SNAPSHOT + + + org.springframework.boot.experimental + spring-boot-thin-launcher + ${wrapper.version} + + + + + + + diff --git a/spring-cloud-function-samples/spring-cloud-function-sample/src/main/java/com/example/SampleApplication.java b/spring-cloud-function-samples/spring-cloud-function-sample/src/main/java/com/example/SampleApplication.java new file mode 100644 index 000000000..a340236dd --- /dev/null +++ b/spring-cloud-function-samples/spring-cloud-function-sample/src/main/java/com/example/SampleApplication.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2016 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 +* +* http://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 com.example; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import reactor.core.publisher.Flux; + +@SpringBootApplication +public class SampleApplication { + + @Bean + public Function, Flux> uppercase() { + return flux -> flux.map(value -> value.toUpperCase()); + } + + @Bean + public Supplier> words() { + return () -> Flux.fromArray(new String[] { "foo", "bar" }); + } + + @Bean + public Function, Flux> lowercase() { + return flux -> flux.map(value -> value.toLowerCase()); + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(SampleApplication.class, args); + } + +} diff --git a/spring-cloud-function-stream/pom.xml b/spring-cloud-function-stream/pom.xml index 96af96f04..5183121bd 100644 --- a/spring-cloud-function-stream/pom.xml +++ b/spring-cloud-function-stream/pom.xml @@ -52,6 +52,13 @@ org.springframework.boot spring-boot-maven-plugin + + + org.springframework.boot.experimental + spring-boot-thin-launcher + ${wrapper.version} + + diff --git a/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java index f8beaf81a..bf1486bb9 100644 --- a/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java +++ b/spring-cloud-function-stream/src/main/java/org/springframework/cloud/function/stream/StreamConfiguration.java @@ -19,12 +19,13 @@ package org.springframework.cloud.function.stream; import java.util.function.Function; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.function.invoker.AbstractFunctionInvoker; -import org.springframework.cloud.function.registry.FileSystemFunctionRegistry; -import org.springframework.cloud.function.registry.FunctionRegistry; +import org.springframework.cloud.function.registry.FunctionCatalog; import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.binder.Binder; import org.springframework.cloud.stream.messaging.Processor; import org.springframework.context.annotation.Bean; import org.springframework.util.StringUtils; @@ -36,23 +37,20 @@ import reactor.core.publisher.Flux; */ @EnableBinding(Processor.class) @EnableConfigurationProperties(FunctionConfigurationProperties.class) +@ConditionalOnClass({ Binder.class, AbstractFunctionInvoker.class }) public class StreamConfiguration { @Autowired private FunctionConfigurationProperties properties; - @Bean - public FunctionRegistry registry() { - return new FileSystemFunctionRegistry(); - } - @Bean @ConditionalOnProperty("spring.cloud.stream.bindings.input.destination") - public AbstractFunctionInvoker invoker(FunctionRegistry registry) { + public AbstractFunctionInvoker invoker(FunctionCatalog registry) { String name = properties.getName(); Function, Flux> function = (name.indexOf(',') == -1) ? registry.lookupFunction(name) - : registry.composeFunction(StringUtils.commaDelimitedListToStringArray(name)); + : registry.composeFunction( + StringUtils.commaDelimitedListToStringArray(name)); return new StreamListeningFunctionInvoker(function); } diff --git a/spring-cloud-function-stream/src/main/resources/META-INF/spring.factories b/spring-cloud-function-stream/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..bdf658b18 --- /dev/null +++ b/spring-cloud-function-stream/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.function.stream.StreamConfiguration \ No newline at end of file diff --git a/spring-cloud-function-task/pom.xml b/spring-cloud-function-task/pom.xml index 4756b732f..1c27ab1f6 100644 --- a/spring-cloud-function-task/pom.xml +++ b/spring-cloud-function-task/pom.xml @@ -43,6 +43,13 @@ org.springframework.boot spring-boot-maven-plugin + + + org.springframework.boot.experimental + spring-boot-thin-launcher + ${wrapper.version} + + diff --git a/spring-cloud-function-task/src/main/java/org/springframework/cloud/function/task/TaskConfiguration.java b/spring-cloud-function-task/src/main/java/org/springframework/cloud/function/task/TaskConfiguration.java index 5c196d90b..f4e3cae29 100644 --- a/spring-cloud-function-task/src/main/java/org/springframework/cloud/function/task/TaskConfiguration.java +++ b/spring-cloud-function-task/src/main/java/org/springframework/cloud/function/task/TaskConfiguration.java @@ -24,9 +24,9 @@ import java.util.function.Supplier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.function.registry.FileSystemFunctionRegistry; -import org.springframework.cloud.function.registry.FunctionRegistry; +import org.springframework.cloud.function.registry.FunctionCatalog; import org.springframework.cloud.task.configuration.EnableTask; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -40,24 +40,23 @@ import reactor.core.publisher.Flux; @Configuration @EnableTask @EnableConfigurationProperties(LambdaConfigurationProperties.class) +@ConditionalOnClass({ EnableTask.class }) public class TaskConfiguration { @Autowired private LambdaConfigurationProperties properties; @Bean - public FunctionRegistry registry() { - return new FileSystemFunctionRegistry(); - } - - @Bean - public CommandLineRunner commandLineRunner(FunctionRegistry registry) { - final Supplier> supplier = registry.lookupSupplier(properties.getSupplier()); + public CommandLineRunner commandLineRunner(FunctionCatalog registry) { + final Supplier> supplier = registry + .lookupSupplier(properties.getSupplier()); String functionName = properties.getFunction(); Function, Flux> function = (functionName.indexOf(',') == -1) ? registry.lookupFunction(functionName) - : registry.composeFunction(StringUtils.commaDelimitedListToStringArray(functionName)); - final Consumer consumer = registry.lookupConsumer(properties.getConsumer()); + : registry.composeFunction( + StringUtils.commaDelimitedListToStringArray(functionName)); + final Consumer consumer = registry + .lookupConsumer(properties.getConsumer()); final CountDownLatch latch = new CountDownLatch(1); final AtomicBoolean status = new AtomicBoolean(); CommandLineRunner runner = new CommandLineRunner() { @@ -81,7 +80,8 @@ public class TaskConfiguration { private final boolean value; - private CompletionConsumer(CountDownLatch latch, AtomicBoolean status, boolean value) { + private CompletionConsumer(CountDownLatch latch, AtomicBoolean status, + boolean value) { this.latch = latch; this.status = status; this.value = value; diff --git a/spring-cloud-function-task/src/main/resources/META-INF/spring.factories b/spring-cloud-function-task/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..92a351e91 --- /dev/null +++ b/spring-cloud-function-task/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.function.task.TaskConfiguration \ No newline at end of file diff --git a/spring-cloud-function-web/pom.xml b/spring-cloud-function-web/pom.xml index 9a66e2e00..8c99b110b 100644 --- a/spring-cloud-function-web/pom.xml +++ b/spring-cloud-function-web/pom.xml @@ -58,6 +58,13 @@ org.springframework.boot spring-boot-maven-plugin + + + org.springframework.boot.experimental + spring-boot-thin-launcher + ${wrapper.version} + + diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RestConfiguration.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RestConfiguration.java index 35a197f61..6e7f6d9b7 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RestConfiguration.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/RestConfiguration.java @@ -17,15 +17,16 @@ package org.springframework.cloud.function.web; import java.util.function.Function; +import java.util.function.Supplier; import org.reactivestreams.Publisher; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cloud.function.registry.FileSystemFunctionRegistry; -import org.springframework.cloud.function.registry.FunctionRegistry; +import org.springframework.cloud.function.registry.FunctionCatalog; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; @@ -50,33 +51,22 @@ import reactor.ipc.netty.http.server.HttpServer; * @author Mark Fisher */ @Configuration -@EnableConfigurationProperties({ FunctionConfigurationProperties.class, - WebConfigurationProperties.class }) +@EnableConfigurationProperties({ WebConfigurationProperties.class }) +@ConditionalOnClass({ HttpHandler.class, NettyContext.class }) public class RestConfiguration { - @Autowired - private FunctionConfigurationProperties functionProperties; - @Autowired private WebConfigurationProperties webProperties; @Bean - public FunctionRegistry registry() { - return new FileSystemFunctionRegistry(); - } - - @Bean - public HttpHandler httpHandler(FunctionRegistry registry) { - String name = functionProperties.getName(); - Function, Flux> function = (name.indexOf(',') == -1) - ? registry.lookupFunction(name) - : registry.composeFunction( - StringUtils.commaDelimitedListToStringArray(name)); - FunctionInvokingHandler handler = new FunctionInvokingHandler(function); - RouterFunction route = RouterFunctions.route( - RequestPredicates.POST(webProperties.getPath()) + public HttpHandler httpHandler(FunctionCatalog registry) { + FunctionInvokingHandler handler = new FunctionInvokingHandler(registry); + RouterFunction route = RouterFunctions + .route(RequestPredicates.POST(webProperties.getPath() + "/{name}") .and(RequestPredicates.contentType(MediaType.TEXT_PLAIN)), - handler::handleText); + handler::handlePost) + .andRoute(RequestPredicates.GET(webProperties.getPath() + "/{name}"), + handler::handleGet); return RouterFunctions.toHttpHandler(route); } @@ -87,15 +77,27 @@ public class RestConfiguration { private static class FunctionInvokingHandler { - private final Function, Flux> function; + private final FunctionCatalog registry; - private FunctionInvokingHandler(Function, Flux> function) { - this.function = function; + private FunctionInvokingHandler(FunctionCatalog registry) { + this.registry = registry; } - private Mono handleText(ServerRequest request) { + private Mono handlePost(ServerRequest request) { Flux input = request.body(toFlux(String.class)); - Publisher output = this.function.apply(input); + String name = request.pathVariable("name"); + Function, Flux> function = (name.indexOf(',') == -1) + ? registry.lookupFunction(name) + : registry.composeFunction( + StringUtils.commaDelimitedListToStringArray(name)); + Publisher output = function.apply(input); + return ServerResponse.ok().body(fromPublisher(output, String.class)); + } + + private Mono handleGet(ServerRequest request) { + String name = request.pathVariable("name"); + Supplier> function = registry.lookupSupplier(name); + Publisher output = function.get(); return ServerResponse.ok().body(fromPublisher(output, String.class)); } } diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/WebConfigurationProperties.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/WebConfigurationProperties.java index 9227949e6..26df4392d 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/WebConfigurationProperties.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/WebConfigurationProperties.java @@ -1,11 +1,11 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-2017 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 * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://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, @@ -13,20 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.cloud.function.web; import org.springframework.boot.context.properties.ConfigurationProperties; -/** - * @author Mark Fisher - */ -@ConfigurationProperties(prefix = "web") +@ConfigurationProperties("server") public class WebConfigurationProperties { private int port = 8080; - private String path; + private String path = ""; public int getPort() { return port; @@ -41,6 +37,9 @@ public class WebConfigurationProperties { } public void setPath(String path) { + while (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } this.path = path; } } diff --git a/spring-cloud-function-web/src/main/resources/META-INF/spring.factories b/spring-cloud-function-web/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..cfc3c41bf --- /dev/null +++ b/spring-cloud-function-web/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.function.web.RestConfiguration \ No newline at end of file