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)
This commit is contained in:
Dave Syer
2016-12-16 11:17:23 +00:00
parent 7408664aeb
commit c6736f959b
39 changed files with 1404 additions and 84 deletions

View File

@@ -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:

View File

@@ -19,6 +19,7 @@
<reactor.version>3.0.4.RELEASE</reactor.version>
<spring-cloud-stream.version>1.1.0.BUILD-SNAPSHOT</spring-cloud-stream.version>
<wrapper.version>0.0.1.BUILD-SNAPSHOT</wrapper.version>
<spring-boot.version>1.5.0.BUILD-SNAPSHOT</spring-boot.version>
</properties>
<dependencyManagement>
@@ -39,9 +40,12 @@
<modules>
<module>spring-cloud-function-compiler</module>
<module>spring-cloud-function-core</module>
<module>spring-cloud-function-context</module>
<module>spring-cloud-function-stream</module>
<module>spring-cloud-function-task</module>
<module>spring-cloud-function-web</module>
<module>spring-cloud-function-samples</module>
<module>spring-cloud-function-deployer</module>
</modules>
<build>

View File

@@ -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 ${@}

View File

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-function-context</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-function-context</name>
<description>Spring Cloud Function Web Support</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<wrapper.version>0.0.1.BUILD-SNAPSHOT</wrapper.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-core</artifactId>
<version>${spring-cloud-function.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<String, Function<?, ?>> functions;
private final Map<String, Consumer<?>> consumers;
private final Map<String, Supplier<?>> suppliers;
public ApplicationContextFunctionCatalog(Map<String, Function<?, ?>> functions,
Map<String, Consumer<?>> consumers, Map<String, Supplier<?>> suppliers) {
this.functions = functions;
this.consumers = consumers;
this.suppliers = suppliers;
}
@SuppressWarnings("unchecked")
@Override
public <T> Consumer<T> lookupConsumer(String name) {
return (Consumer<T>) consumers.get(name);
}
@SuppressWarnings("unchecked")
@Override
public <T, R> Function<T, R> lookupFunction(String name) {
return (Function<T, R>) functions.get(name);
}
@Override
public <T, R> Function<T, R> composeFunction(String... functionNames) {
Function<T, R> 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 <T> Supplier<T> lookupSupplier(String name) {
return (Supplier<T>) suppliers.get(name);
}
}

View File

@@ -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<String, Function<?, ?>> functions = Collections.emptyMap();
@Autowired(required = false)
private Map<String, Consumer<?>> consumers = Collections.emptyMap();
@Autowired(required = false)
private Map<String, Supplier<?>> suppliers = Collections.emptyMap();
@Bean
public FunctionCatalog functionCatalog() {
return new ApplicationContextFunctionCatalog(functions, consumers, suppliers);
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration

View File

@@ -18,6 +18,10 @@
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-compiler</artifactId>
@@ -46,6 +50,13 @@
<configuration>
<classifier>registrar</classifier>
</configuration>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-launcher</artifactId>
<version>${wrapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

View File

@@ -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();
}
}

View File

@@ -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 {
<T> Consumer<T> lookupConsumer(String name);
<T, R> Function<T, R> lookupFunction(String name);
<T, R> Function<T, R> composeFunction(String... functionNames);
<T> Supplier<T> lookupSupplier(String name);
}

View File

@@ -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);
<T> Consumer<T> lookupConsumer(String name);
<T, R> Function<T, R> lookupFunction(String name);
<T, R> Function<T, R> composeFunction(String... functionNames);
<T> Supplier<T> lookupSupplier(String name);
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.registry.DefaultFunctionRegistryAutoConfiguration

View File

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-function-deployer</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-function-deployer</name>
<description>Spring Cloud Function Web Support</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud-deployer-thin.version>0.0.1.BUILD-SNAPSHOT</spring-cloud-deployer-thin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-starter-web-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-deployer-thin</artifactId>
<version>${spring-cloud-deployer-thin.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-dependencies-web-reactive</artifactId>
<version>0.1.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-parent</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Dalston.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<String, String> 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<URL> child = new ArrayList<>();
List<URL> 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<File> 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;
}
}

View File

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

View File

@@ -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<String> function(@PathVariable String name,
@RequestBody Flux<String> body) {
Function<Object, Object> function;
if (name.contains(",")) {
function = deployer.composeFunction(name.split(","));
}
else {
function = deployer.lookupFunction(name);
}
@SuppressWarnings("unchecked")
Flux<String> result = (Flux<String>) function.apply(body);
return result;
}
@GetMapping("/{name}")
public Flux<String> supplier(@PathVariable String name) {
@SuppressWarnings("unchecked")
Flux<String> result = (Flux<String>) deployer.lookupSupplier(name).get();
return result;
}
}

View File

@@ -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<String, String> deployed = new LinkedHashMap<>();
private Map<String, String> names = new LinkedHashMap<>();
@Autowired
public FunctionAdminController(FunctionExtractingAppDeployer deployer) {
this.deployer = deployer;
}
@PostMapping(path = "/{name}")
public Map<String, Object> push(@PathVariable String name, @RequestParam String path)
throws Exception {
String id = deploy(name, path);
return Collections.singletonMap("id", id);
}
@DeleteMapping(path = "/{name}")
public Map<String, Object> 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<String, Object> deployed() {
Map<String, Object> 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;
}
}

View File

@@ -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<String, Function<?, ?>> functions = new HashMap<>();
private final Map<String, Consumer<?>> consumers = new HashMap<>();
private final Map<String, Supplier<?>> suppliers = new HashMap<>();
public FunctionExtractingAppDeployer() {
this("thin", "slim");
}
public FunctionExtractingAppDeployer(String name, String... profiles) {
super(name, profiles);
}
@SuppressWarnings("unchecked")
@Override
public <T> Consumer<T> lookupConsumer(String name) {
return (Consumer<T>) consumers.get(name);
}
@SuppressWarnings("unchecked")
@Override
public <T, R> Function<T, R> lookupFunction(String name) {
return (Function<T, R>) functions.get(name);
}
@Override
public <T, R> Function<T, R> composeFunction(String... functionNames) {
Function<T, R> 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 <T> Supplier<T> lookupSupplier(String name) {
return (Supplier<T>) 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<String, Function<?, ?>> functions(String id) {
Map<String, Function<?, ?>> map = new HashMap<>();
ThinJarAppWrapper wrapper = getWrapper(id);
if (wrapper == null) {
return map;
}
try {
@SuppressWarnings("unchecked")
Map<String, ? extends Function<?, ?>> result = (Map<String, ? extends Function<?, ?>>) 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<String, Consumer<?>> consumers(String id) {
Map<String, Consumer<?>> map = new HashMap<>();
ThinJarAppWrapper wrapper = getWrapper(id);
if (wrapper == null) {
return map;
}
try {
@SuppressWarnings("unchecked")
Map<String, ? extends Consumer<?>> result = (Map<String, ? extends Consumer<?>>) 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<String, Supplier<?>> suppliers(String id) {
Map<String, Supplier<?>> map = new HashMap<>();
ThinJarAppWrapper wrapper = getWrapper(id);
if (wrapper == null) {
return map;
}
try {
@SuppressWarnings("unchecked")
Map<String, ? extends Supplier<?>> result = (Map<String, ? extends Supplier<?>>) 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 <T> Map<String, ? extends T> getBeans(ThinJarAppWrapper wrapper,
Class<T> 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<String, T> result = (Map<String, T>) 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;
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.deployer.FunctionExtractingAutoConfiguration

View File

@@ -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");
}
}

View File

@@ -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<String> result = (Flux<String>) 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;
}
}

View File

@@ -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

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-function-samples</artifactId>
<name>Spring Cloud Function Samples</name>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-parent</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</parent>
<modules>
<module>spring-cloud-function-sample</module>
</modules>
</project>

View File

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>function-sample</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-cloud-function-sample</name>
<description>Spring Cloud Function Web Support</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
<wrapper.version>0.0.1.BUILD-SNAPSHOT</wrapper.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<version>${spring-cloud-function.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
<version>${spring-cloud-function.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.5.0.BUILD-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-launcher</artifactId>
<version>${wrapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<String>, Flux<String>> uppercase() {
return flux -> flux.map(value -> value.toUpperCase());
}
@Bean
public Supplier<Flux<String>> words() {
return () -> Flux.fromArray(new String[] { "foo", "bar" });
}
@Bean
public Function<Flux<String>, Flux<String>> lowercase() {
return flux -> flux.map(value -> value.toLowerCase());
}
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleApplication.class, args);
}
}

View File

@@ -52,6 +52,13 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-launcher</artifactId>
<version>${wrapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

View File

@@ -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<Object>, Flux<Object>> function = (name.indexOf(',') == -1)
? registry.lookupFunction(name)
: registry.composeFunction(StringUtils.commaDelimitedListToStringArray(name));
: registry.composeFunction(
StringUtils.commaDelimitedListToStringArray(name));
return new StreamListeningFunctionInvoker(function);
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.stream.StreamConfiguration

View File

@@ -43,6 +43,13 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-launcher</artifactId>
<version>${wrapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

View File

@@ -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<Flux<Object>> supplier = registry.lookupSupplier(properties.getSupplier());
public CommandLineRunner commandLineRunner(FunctionCatalog registry) {
final Supplier<Flux<Object>> supplier = registry
.lookupSupplier(properties.getSupplier());
String functionName = properties.getFunction();
Function<Flux<Object>, Flux<Object>> function = (functionName.indexOf(',') == -1)
? registry.lookupFunction(functionName)
: registry.composeFunction(StringUtils.commaDelimitedListToStringArray(functionName));
final Consumer<Object> consumer = registry.lookupConsumer(properties.getConsumer());
: registry.composeFunction(
StringUtils.commaDelimitedListToStringArray(functionName));
final Consumer<Object> 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;

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.task.TaskConfiguration

View File

@@ -58,6 +58,13 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot.experimental</groupId>
<artifactId>spring-boot-thin-launcher</artifactId>
<version>${wrapper.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

View File

@@ -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<String>, Flux<String>> function = (name.indexOf(',') == -1)
? registry.lookupFunction(name)
: registry.composeFunction(
StringUtils.commaDelimitedListToStringArray(name));
FunctionInvokingHandler handler = new FunctionInvokingHandler(function);
RouterFunction<ServerResponse> 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<String>, Flux<String>> function;
private final FunctionCatalog registry;
private FunctionInvokingHandler(Function<Flux<String>, Flux<String>> function) {
this.function = function;
private FunctionInvokingHandler(FunctionCatalog registry) {
this.registry = registry;
}
private Mono<ServerResponse> handleText(ServerRequest request) {
private Mono<ServerResponse> handlePost(ServerRequest request) {
Flux<String> input = request.body(toFlux(String.class));
Publisher<String> output = this.function.apply(input);
String name = request.pathVariable("name");
Function<Flux<String>, Flux<String>> function = (name.indexOf(',') == -1)
? registry.lookupFunction(name)
: registry.composeFunction(
StringUtils.commaDelimitedListToStringArray(name));
Publisher<String> output = function.apply(input);
return ServerResponse.ok().body(fromPublisher(output, String.class));
}
private Mono<ServerResponse> handleGet(ServerRequest request) {
String name = request.pathVariable("name");
Supplier<Flux<String>> function = registry.lookupSupplier(name);
Publisher<String> output = function.get();
return ServerResponse.ok().body(fromPublisher(output, String.class));
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.web.RestConfiguration