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

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