Replace deployer with a simpler library
Instead of an app, it is now a library with some utilities (principally ApplicationBootstrap) for launching a Spring Boot application, extracting a function, and registering it in the FunctionRegistry.
This commit is contained in:
@@ -1,56 +1,20 @@
|
||||
Spring Cloud Function Deployer is an app that can deploy functions packaged as jars. Once the app is running it can deploy a basic Spring Cloud Function app from a jar with locally cached dependencies in about 500ms (compared to 1500ms for the same application launched from cold). It can be used in a pool as a "warm" JVM to deploy functions quicker than they could be started from scratch.
|
||||
Spring Cloud Function Deployer is an library for building apps that can deploy functions packaged as jars. It can deploy a basic Spring Cloud Function app from a jar with locally cached dependencies in about 500ms (compared to 1500ms for the same application launched from cold). It can be used in a pool as a "warm" JVM to deploy functions quicker than they could be started from scratch. Example usage:
|
||||
|
||||
The app has a single endpoint called "/admin" that you can use to manage the deployed functions. You GET from it to list the deployed apps, POST to `/{name}` to deploy a named app with a `path` parameter pointing to a jar resource, and then DELETE `/{name}` to remove it. Functions in the apps are exposed as `/{name}/{function}` with the usual conventions for Spring Cloud Function (i.e. the function name is the bean name by default).
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class FunctionApplication {
|
||||
|
||||
== Running the Deployer
|
||||
public static void main(String[] args) throws IOException {
|
||||
new ApplicationBootstrap().run(FunctionApplication.class, args);
|
||||
}
|
||||
|
||||
Run the main class `ApplicationRunner` in this project (from the command line or in the IDE). E.g.
|
||||
|
||||
```
|
||||
$ ./mvnw install -DskipTests
|
||||
$ cd spring-cloud-function-deployer
|
||||
$ ../mvnw spring-boot:run
|
||||
}
|
||||
```
|
||||
|
||||
The app starts empty, so the admin resource shows no deployed apps:
|
||||
|
||||
```
|
||||
$ curl localhost:8080/admin
|
||||
{}
|
||||
```
|
||||
|
||||
Deploy a sample like this:
|
||||
|
||||
```
|
||||
$ curl localhost:8080/admin/pojos -d path=maven://com.example:function-sample-pojo:1.0.0.BUILD-SNAPSHOT
|
||||
{"id":"81c568e36c7909ec1dd841aa7ee6d3e3"}
|
||||
```
|
||||
|
||||
(takes about 500ms, once the local Maven cache is warm). Deploy another one:
|
||||
|
||||
```
|
||||
$ curl localhost:8080/admin/sample -d path=maven://com.example:function-sample:1.0.0.BUILD-SNAPSHOT
|
||||
{"id":"cb2fdb3130f6349f143f4686848ea90f"}
|
||||
```
|
||||
|
||||
Undeploy the first one:
|
||||
|
||||
```
|
||||
$ curl localhost:8080/admin/pojos -X DELETE
|
||||
{"name":"81c568e36c7909ec1dd841aa7ee6d3e3","id":"pojos","path":"maven://com.example:function-sample-pojo:1.0.0.BUILD-SNAPSHOT"}
|
||||
```
|
||||
|
||||
List the deployed apps:
|
||||
|
||||
```
|
||||
$ curl localhost:8080/admin
|
||||
{"sample":{"name":"sample","id":"cb2fdb3130f6349f143f4686848ea90","path":"maven://com.example:function-sample:1.0.0.BUILD-SNAPSHOT"}}
|
||||
```
|
||||
|
||||
Send an event to one of the functions:
|
||||
|
||||
```
|
||||
$ curl -H "Content-Type: text/plain" localhost:8080/sample/uppercase -d foo
|
||||
FOO
|
||||
```
|
||||
There is a main class in the jar that alread looks like this. You can use it like that or you can create your own copy if you want to customize it. The `ApplicationBootstrap` is a utility that replaces `SpringApplication`, creating a class loader hierarchy that works with the function configuration. It needs to be launched with configuration for the `FunctionProperties`:
|
||||
|
||||
| Option | Description |
|
||||
|--------|----------------------|
|
||||
| `function.location` | Mandatory archive location(s) for building the classpath of the function. |
|
||||
| `function.bean` | Mandatory bean class or name (if `function.main` is provided) to create the function. If multi-valued, the function is composed (outputs piped to inputs) |
|
||||
| `function.main` | The main `@SpringBootApplication` to launch (optional). |
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<spring-cloud-deployer-thin.version>1.0.10.RELEASE</spring-cloud-deployer-thin.version>
|
||||
<wrapper.version>1.0.10.RELEASE</wrapper.version>
|
||||
<spring.cloud.deployer.version>1.3.2.RELEASE</spring.cloud.deployer.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -27,38 +27,40 @@
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-stream</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-loader</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-deployer-thin</artifactId>
|
||||
<version>${spring-cloud-deployer-thin.version}</version>
|
||||
<artifactId>spring-cloud-deployer-resource-maven</artifactId>
|
||||
<version>${spring.cloud.deployer.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-deployer-resource-support</artifactId>
|
||||
<version>${spring.cloud.deployer.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-test-support</artifactId>
|
||||
<optional>true</optional>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-aether-provider</artifactId>
|
||||
<version>3.3.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
@@ -72,7 +74,65 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-invoker-plugin</artifactId>
|
||||
<configuration>
|
||||
<localRepositoryPath>${project.build.directory}/local-repo</localRepositoryPath>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>prepare-test</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<cloneProjectsTo>${project.build.directory}/it</cloneProjectsTo>
|
||||
<settingsFile>src/it/settings.xml</settingsFile>
|
||||
<addTestClassPath>true</addTestClassPath>
|
||||
<skipInvocation>${skipTests}</skipInvocation>
|
||||
<streamLogs>true</streamLogs>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<!--This plugin's configuration is used to store Eclipse m2e settings
|
||||
only. It has no influence on the Maven build itself. -->
|
||||
<plugin>
|
||||
<groupId>org.eclipse.m2e</groupId>
|
||||
<artifactId>lifecycle-mapping</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<configuration>
|
||||
<lifecycleMappingMetadata>
|
||||
<pluginExecutions>
|
||||
<pluginExecution>
|
||||
<pluginExecutionFilter>
|
||||
<groupId>
|
||||
org.apache.maven.plugins
|
||||
</groupId>
|
||||
<artifactId>
|
||||
maven-invoker-plugin
|
||||
</artifactId>
|
||||
<versionRange>
|
||||
[1.10,)
|
||||
</versionRange>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
</pluginExecutionFilter>
|
||||
<action>
|
||||
<ignore></ignore>
|
||||
</action>
|
||||
</pluginExecution>
|
||||
</pluginExecutions>
|
||||
</lifecycleMappingMetadata>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
||||
35
spring-cloud-function-deployer/src/it/settings.xml
Normal file
35
spring-cloud-function-deployer/src/it/settings.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<settings>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>it-repo</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>local.central</id>
|
||||
<url>@localRepositoryUrl@</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>local.central</id>
|
||||
<url>@localRepositoryUrl@</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
</profile>
|
||||
</profiles>
|
||||
</settings>
|
||||
107
spring-cloud-function-deployer/src/it/support/pom.xml
Normal file
107
spring-cloud-function-deployer/src/it/support/pom.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?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.M1</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.5.12.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
<spring-cloud-function.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-function.version>
|
||||
<wrapper.version>1.0.10.RELEASE</wrapper.version>
|
||||
<reactor.version>3.2.0.M1</reactor.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<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-starter</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<classifier>exec</classifier>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/libs-snapshot-local</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/release</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/libs-snapshot-local</url>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/libs-milestone-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
<pluginRepository>
|
||||
<id>spring-releases</id>
|
||||
<name>Spring Releases</name>
|
||||
<url>https://repo.spring.io/libs-release-local</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 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 com.example.functions;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DoubleLogger implements Consumer<Integer> {
|
||||
|
||||
@Override
|
||||
public void accept(Integer i) {
|
||||
System.out.println(2 * i);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2015 the original author or authors.
|
||||
* Copyright 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.
|
||||
@@ -14,23 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.deployer;
|
||||
package com.example.functions;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
import org.junit.runners.Suite.SuiteClasses;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* A test suite for probing weird ordering problems in the tests.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
@RunWith(Suite.class)
|
||||
@SuiteClasses({ FunctionAppDeployerTests.class,
|
||||
FunctionExtractingFunctionCatalogTests.class,
|
||||
FunctionExtractingFunctionCatalogIntegrationTests.class })
|
||||
@Ignore
|
||||
public class AdhocTestSuite {
|
||||
public class Emitter implements Supplier<String> {
|
||||
|
||||
private int i = 0;
|
||||
|
||||
private String[] values = {"one", "two", "three", "four"};
|
||||
|
||||
@Override
|
||||
public String get() {
|
||||
return values[i++ % values.length];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 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 com.example.functions;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class FunctionApp {
|
||||
|
||||
@Bean
|
||||
public DoubleLogger myDoubler() {
|
||||
return new DoubleLogger();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Emitter myEmitter() {
|
||||
return new Emitter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LengthCounter myCounter() {
|
||||
return new LengthCounter();
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
SpringApplication.run(FunctionApp.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 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 com.example.functions;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class LengthCounter implements Function<String, Integer> {
|
||||
|
||||
@Override
|
||||
public Integer apply(String string) {
|
||||
return string.length();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 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.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Utility class to launch a Spring Boot application (optionally) in an isolated class
|
||||
* loader. The class loader is created in such a way that it is mostly a copy of the
|
||||
* current class loader (i.e. the one that loaded this class), but has a parent containing
|
||||
* reactor-core (if present). It can then share the reactor dependency with other class
|
||||
* loaders that the app itself creates, without any other classes being shared, other than
|
||||
* the core JDK.
|
||||
*
|
||||
* @author Mark Fisher
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class ApplicationBootstrap {
|
||||
|
||||
private static Log logger = LogFactory.getLog(ApplicationBootstrap.class);
|
||||
private ApplicationRunner runner;
|
||||
private URLClassLoader classLoader;
|
||||
|
||||
/**
|
||||
* Run the provided main class as a Spring Boot application with the provided command
|
||||
* line arguments.
|
||||
*/
|
||||
public void run(Class<?> mainClass, String... args) {
|
||||
if (ApplicationBootstrap.isolated(args)) {
|
||||
runner(mainClass).run(args);
|
||||
}
|
||||
else {
|
||||
SpringApplication.run(mainClass, args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the resources used by this instance, if any. Called automatically on a
|
||||
* runtime shutdown hook.
|
||||
*/
|
||||
public void close() {
|
||||
if (this.runner != null) {
|
||||
this.runner.close();
|
||||
this.runner = null;
|
||||
}
|
||||
if (this.classLoader != null) {
|
||||
try {
|
||||
this.classLoader.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot close ClassLoader", e);
|
||||
}
|
||||
finally {
|
||||
this.classLoader = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ApplicationRunner runner(Class<?> mainClass) {
|
||||
if (this.runner == null) {
|
||||
synchronized (this) {
|
||||
if (this.runner == null) {
|
||||
this.classLoader = createClassLoader();
|
||||
this.runner = new ApplicationRunner(this.classLoader,
|
||||
mainClass.getName());
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::close));
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.runner;
|
||||
}
|
||||
|
||||
private static boolean isolated(String[] args) {
|
||||
for (String arg : args) {
|
||||
if (arg.equals("--function.runner.isolated=false")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private URLClassLoader createClassLoader() {
|
||||
URL[] urls = findClassPath();
|
||||
if (urls.length == 1) {
|
||||
URL[] classpath = extractClasspath(urls[0]);
|
||||
if (classpath != null) {
|
||||
urls = classpath;
|
||||
}
|
||||
}
|
||||
List<URL> child = new ArrayList<>();
|
||||
List<URL> parent = new ArrayList<>();
|
||||
for (URL url : urls) {
|
||||
child.add(url);
|
||||
}
|
||||
for (URL url : urls) {
|
||||
if (isRoot(StringUtils.getFilename(clean(url.toString())))) {
|
||||
parent.add(url);
|
||||
child.remove(url);
|
||||
}
|
||||
}
|
||||
logger.debug("Parent: " + parent);
|
||||
logger.debug("Child: " + child);
|
||||
ClassLoader base = getClass().getClassLoader();
|
||||
if (!parent.isEmpty()) {
|
||||
base = new URLClassLoader(parent.toArray(new URL[0]), base.getParent());
|
||||
}
|
||||
return new URLClassLoader(child.toArray(new URL[0]), base);
|
||||
}
|
||||
|
||||
private URL[] findClassPath() {
|
||||
ClassLoader base = getClass().getClassLoader();
|
||||
if (!(base instanceof URLClassLoader)) {
|
||||
try {
|
||||
// Guess the classpath, based on where we can resolve existing resources
|
||||
List<URL> list = Collections
|
||||
.list(getClass().getClassLoader().getResources("META-INF"));
|
||||
List<URL> result = new ArrayList<>();
|
||||
result.add(
|
||||
getClass().getProtectionDomain().getCodeSource().getLocation());
|
||||
for (URL url : list) {
|
||||
String path = url.toString();
|
||||
path = path.substring(0, path.length() - "/META-INF".length());
|
||||
if (path.endsWith("!")) {
|
||||
path = path + "/";
|
||||
}
|
||||
result.add(new URL(path));
|
||||
}
|
||||
return result.toArray(new URL[result.size()]);
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot find class path", e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@SuppressWarnings("resource")
|
||||
URLClassLoader urlClassLoader = (URLClassLoader) base;
|
||||
return urlClassLoader.getURLs();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRoot(String file) {
|
||||
return file.startsWith("reactor-core") || file.startsWith("reactive-streams");
|
||||
}
|
||||
|
||||
private String clean(String jar) {
|
||||
// This works with fat jars like Spring Boot where the path elements look like
|
||||
// jar:file:...something.jar!/.
|
||||
return jar.endsWith("!/") ? jar.substring(0, jar.length() - 2) : jar;
|
||||
}
|
||||
|
||||
private URL[] extractClasspath(URL url) {
|
||||
// This works for a jar indirection like in surefire and IntelliJ
|
||||
if (url.toString().endsWith(".jar")) {
|
||||
JarFile jar;
|
||||
try {
|
||||
jar = new JarFile(new File(url.toURI()));
|
||||
String path = jar.getManifest().getMainAttributes()
|
||||
.getValue("Class-Path");
|
||||
if (path != null) {
|
||||
List<URL> result = new ArrayList<>();
|
||||
for (String element : path.split(" ")) {
|
||||
result.add(new URL(element));
|
||||
}
|
||||
return result.toArray(new URL[0]);
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
* Copyright 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.
|
||||
@@ -13,72 +13,56 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.deployer;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.eclipse.aether.graph.Dependency;
|
||||
|
||||
import org.springframework.boot.Banner.Mode;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.loader.thin.DependencyResolver;
|
||||
import org.springframework.cloud.deployer.thin.ContextRunner;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.support.LiveBeansView;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.expression.spel.support.StandardTypeLocator;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Driver class for running a Spring Boot application via an isolated classpath.
|
||||
* Initialize an instance of this class with the class loader to be used and the name of
|
||||
* the main class (usually a <code>@SpringBootApplication</code>), and then
|
||||
* {@link #run(String...)} it, cleaning up with a call to {@link #close()}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
// NOT a @Component (to prevent it from being scanned by the "main" application).
|
||||
public class ApplicationRunner implements CommandLineRunner {
|
||||
public class ApplicationRunner {
|
||||
|
||||
private static Log logger = LogFactory.getLog(ApplicationRunner.class);
|
||||
|
||||
public static void main(String[] args) {
|
||||
new ApplicationRunner().start(args);
|
||||
private final ClassLoader classLoader;
|
||||
|
||||
private final String source;
|
||||
|
||||
private StandardEvaluationContext app;
|
||||
|
||||
public ApplicationRunner(ClassLoader classLoader, String source) {
|
||||
this.classLoader = classLoader;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
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;
|
||||
private ClassLoader classLoader;
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
|
||||
try {
|
||||
this.classLoader = createClassLoader();
|
||||
ClassUtils.overrideThreadContextClassLoader(this.classLoader);
|
||||
Class<?> cls = this.classLoader.loadClass(ContextRunner.class.getName());
|
||||
this.app = cls.newInstance();
|
||||
runContext(DeployedFunctionApplication.class.getName(), Collections
|
||||
.singletonMap(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME, "deployer"),
|
||||
this.app = new StandardEvaluationContext(cls.newInstance());
|
||||
this.app.setTypeLocator(new StandardTypeLocator(this.classLoader));
|
||||
runContext(this.source, defaultProperties(UUID.randomUUID().toString()),
|
||||
args);
|
||||
}
|
||||
catch (Exception e) {
|
||||
@@ -93,23 +77,102 @@ public class ApplicationRunner implements CommandLineRunner {
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() throws IOException {
|
||||
closeContext();
|
||||
if (this.classLoader!=null && this.classLoader instanceof Closeable) {
|
||||
((Closeable) this.classLoader).close();
|
||||
private Map<String, String> defaultProperties(String id) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME, "function-invoker-" + id);
|
||||
map.put("spring.jmx.default-domain", "function-invoker-" + id);
|
||||
map.put("spring.jmx.enabled", "false");
|
||||
return map;
|
||||
}
|
||||
|
||||
public Object getBean(String name) {
|
||||
if (this.app != null) {
|
||||
if (containsBeanByName(name)) {
|
||||
return getBeanByName(name);
|
||||
}
|
||||
try {
|
||||
return getBeanByType(name);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// not there
|
||||
}
|
||||
}
|
||||
this.classLoader = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean containsBeanByName(String name) {
|
||||
Expression parsed = new SpelExpressionParser()
|
||||
.parseExpression("context.containsBean(\"" + name + "\")");
|
||||
return parsed.getValue(this.app, Boolean.class);
|
||||
}
|
||||
|
||||
private Object getBeanByName(String name) {
|
||||
Expression parsed = new SpelExpressionParser()
|
||||
.parseExpression("context.getBean(\"" + name + "\")");
|
||||
return parsed.getValue(this.app);
|
||||
}
|
||||
|
||||
private Object getBeanByType(String name) {
|
||||
Expression parsed = new SpelExpressionParser()
|
||||
.parseExpression("context.getBean(T(" + name + "))");
|
||||
return parsed.getValue(this.app);
|
||||
}
|
||||
|
||||
public boolean containsBean(String name) {
|
||||
if (this.app != null) {
|
||||
if (containsBeanByName(name)) {
|
||||
return true;
|
||||
}
|
||||
Expression parsed = new SpelExpressionParser()
|
||||
.parseExpression("context.getBeansOfType(T(" + name + "))");
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> beans = (Map<String, Object>) parsed
|
||||
.getValue(this.app);
|
||||
return !beans.isEmpty();
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Object evaluate(String expression, Object root, Object... attrs) {
|
||||
Expression parsed = new SpelExpressionParser().parseExpression(expression);
|
||||
StandardEvaluationContext context = new StandardEvaluationContext(root);
|
||||
if (attrs.length % 2 != 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Context attributes must be name, value pairs");
|
||||
}
|
||||
for (int i = 0; i < attrs.length / 2; i++) {
|
||||
String name = (String) attrs[2 * i];
|
||||
Object value = attrs[2 * i + 1];
|
||||
context.setVariable(name, value);
|
||||
}
|
||||
return parsed.getValue(context);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
if (this.app == null) {
|
||||
return false;
|
||||
}
|
||||
Expression parsed = new SpelExpressionParser()
|
||||
.parseExpression("context.isRunning()");
|
||||
return parsed.getValue(this.app, Boolean.class);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() {
|
||||
closeContext();
|
||||
}
|
||||
|
||||
private RuntimeException getError() {
|
||||
if (this.app == null) {
|
||||
return null;
|
||||
}
|
||||
Method method = ReflectionUtils.findMethod(this.app.getClass(), "getError");
|
||||
Throwable e;
|
||||
e = (Throwable) ReflectionUtils.invokeMethod(method, this.app);
|
||||
if (e==null) {
|
||||
Expression parsed = new SpelExpressionParser().parseExpression("error");
|
||||
Throwable e = parsed.getValue(this.app, Throwable.class);
|
||||
if (e == null) {
|
||||
return null;
|
||||
}
|
||||
if (e instanceof RuntimeException) {
|
||||
@@ -120,57 +183,21 @@ public class ApplicationRunner implements CommandLineRunner {
|
||||
|
||||
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);
|
||||
Expression parsed = new SpelExpressionParser()
|
||||
.parseExpression("run(#main,#properties,#args)");
|
||||
StandardEvaluationContext context = this.app;
|
||||
context.setVariable("main", mainClass);
|
||||
context.setVariable("properties", properties);
|
||||
context.setVariable("args", args);
|
||||
parsed.getValue(context);
|
||||
}
|
||||
|
||||
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);
|
||||
if (this.app != null) {
|
||||
Expression parsed = new SpelExpressionParser().parseExpression("close()");
|
||||
parsed.getValue(this.app);
|
||||
this.app = null;
|
||||
}
|
||||
@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);
|
||||
}
|
||||
List<File> resolved = resolveParent();
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 List<File> resolveParent() {
|
||||
DependencyResolver resolver = DependencyResolver.instance();
|
||||
List<Dependency> dependencies = resolver
|
||||
.dependencies(new ClassPathResource("core-pom.xml"));
|
||||
List<File> resolved = new ArrayList<>();
|
||||
for (Dependency dependency : dependencies) {
|
||||
resolved.add(resolver.resolve(dependency));
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.management.ManagementFactory;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class BeanCountingApplicationListener
|
||||
implements ApplicationListener<ApplicationReadyEvent>, ApplicationContextAware {
|
||||
|
||||
public static final String MARKER = "Invoker app started";
|
||||
private static Log logger = LogFactory.getLog(BeanCountingApplicationListener.class);
|
||||
private ApplicationContext context;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationReadyEvent event) {
|
||||
if (!event.getApplicationContext().equals(this.context)) {
|
||||
return;
|
||||
}
|
||||
int count = 0;
|
||||
ConfigurableApplicationContext context = event.getApplicationContext();
|
||||
String id = context.getId();
|
||||
List<String> names = new ArrayList<>();
|
||||
while (context != null) {
|
||||
count += context.getBeanDefinitionCount();
|
||||
names.addAll(Arrays.asList(context.getBeanDefinitionNames()));
|
||||
context = (ConfigurableApplicationContext) context.getParent();
|
||||
}
|
||||
logger.info("Bean count: " + id + "=" + count);
|
||||
logger.debug("Bean names: " + id + "=" + names);
|
||||
try {
|
||||
logger.info("Class count: " + id + "=" + ManagementFactory
|
||||
.getClassLoadingMXBean().getTotalLoadedClassCount());
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
if (isSpringBootApplication(sources(event))) {
|
||||
try {
|
||||
logger.info(MARKER);
|
||||
}
|
||||
catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSpringBootApplication(Set<Class<?>> sources) {
|
||||
for (Class<?> source : sources) {
|
||||
if (AnnotatedElementUtils.hasAnnotation(source,
|
||||
SpringBootConfiguration.class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<Class<?>> sources(ApplicationReadyEvent event) {
|
||||
Method method = ReflectionUtils.findMethod(SpringApplication.class,
|
||||
"getAllSources");
|
||||
if (method == null) {
|
||||
method = ReflectionUtils.findMethod(SpringApplication.class, "getSources");
|
||||
}
|
||||
ReflectionUtils.makeAccessible(method);
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Object> objects = (Set<Object>) ReflectionUtils.invokeMethod(method,
|
||||
event.getSpringApplication());
|
||||
Set<Class<?>> result = new LinkedHashSet<>();
|
||||
for (Object object : objects) {
|
||||
if (object instanceof String) {
|
||||
object = ClassUtils.resolveClassName((String) object, null);
|
||||
}
|
||||
result.add((Class<?>) object);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 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.net.URL;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Utility class for starting a Spring Boot application in a separate thread. Best used
|
||||
* from an isolated class loader, e.g. through {@link ApplicationRunner}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class ContextRunner {
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
private Thread runThread;
|
||||
private volatile boolean running = false;
|
||||
private Throwable error;
|
||||
private long timeout = 120000;
|
||||
|
||||
public void run(final String source, final Map<String, Object> properties,
|
||||
final String... args) {
|
||||
// Run in new thread to ensure that the context classloader is setup
|
||||
this.runThread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
resetUrlHandler();
|
||||
StandardEnvironment environment = new StandardEnvironment();
|
||||
environment.getPropertySources().addAfter(
|
||||
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
|
||||
new MapPropertySource("appDeployer", properties));
|
||||
running = true;
|
||||
SpringApplicationBuilder builder = builder(
|
||||
ClassUtils.resolveClassName(source, null));
|
||||
if (ClassUtils.isPresent(
|
||||
"org.springframework.cloud.stream.app.function.app.BeanCountingApplicationListener.BeanCountingApplicationListener()",
|
||||
null)) {
|
||||
builder.listeners(new BeanCountingApplicationListener());
|
||||
}
|
||||
context = builder.environment(environment).registerShutdownHook(false)
|
||||
.run(args);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
error = ex;
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
this.runThread.start();
|
||||
try {
|
||||
this.runThread.join(timeout);
|
||||
this.running = context != null && context.isRunning();
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
this.running = false;
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
resetUrlHandler();
|
||||
}
|
||||
// TODO: JDBC leak protection?
|
||||
this.running = false;
|
||||
this.runThread.setContextClassLoader(null);
|
||||
this.runThread = null;
|
||||
}
|
||||
|
||||
public ConfigurableApplicationContext getContext() {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
private void resetUrlHandler() {
|
||||
if (ClassUtils.isPresent(
|
||||
"org.apache.catalina.webresources.TomcatURLStreamHandlerFactory", null)) {
|
||||
setField(ClassUtils.resolveClassName(
|
||||
"org.apache.catalina.webresources.TomcatURLStreamHandlerFactory",
|
||||
null), "instance", null);
|
||||
setField(URL.class, "factory", null);
|
||||
}
|
||||
}
|
||||
|
||||
private void setField(Class<?> type, String name, Object value) {
|
||||
Field field = ReflectionUtils.findField(type, name);
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
ReflectionUtils.setField(field, null, value);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public Throwable getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
public static SpringApplicationBuilder builder(Class<?> type) {
|
||||
// Defensive reflective builder to work with Boot 1.5 and 2.0
|
||||
if (ClassUtils.hasConstructor(SpringApplicationBuilder.class, Class[].class)) {
|
||||
return BeanUtils
|
||||
.instantiateClass(
|
||||
ClassUtils.getConstructorIfAvailable(
|
||||
SpringApplicationBuilder.class, Class[].class),
|
||||
(Object) new Class<?>[] { type });
|
||||
}
|
||||
return BeanUtils
|
||||
.instantiateClass(
|
||||
ClassUtils.getConstructorIfAvailable(
|
||||
SpringApplicationBuilder.class, Object[].class),
|
||||
(Object) new Object[] { type.getName() });
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* 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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class DeployedApplicationFilter extends WebMvcConfigurerAdapter
|
||||
implements HandlerInterceptor {
|
||||
|
||||
private final FunctionExtractingFunctionCatalog deployer;
|
||||
|
||||
@Autowired
|
||||
public DeployedApplicationFilter(FunctionExtractingFunctionCatalog deployer) {
|
||||
this.deployer = deployer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
String path = (String) request
|
||||
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
|
||||
if (path != null) {
|
||||
// TODO: extract /stream to config property
|
||||
if (path.startsWith("/stream")) {
|
||||
String name = path.substring("/stream/".length());
|
||||
if (name.contains("/")) {
|
||||
name = name.substring(0, name.indexOf("/"));
|
||||
}
|
||||
if (deployer.deployed().containsKey(name)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, ModelAndView modelAndView) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, Exception ex) throws Exception {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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 {
|
||||
|
||||
private final FunctionExtractingFunctionCatalog deployer;
|
||||
|
||||
@Autowired
|
||||
public FunctionAdminController(FunctionExtractingFunctionCatalog 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 Object undeploy(@PathVariable String name) throws Exception {
|
||||
return deployer.undeploy(name);
|
||||
}
|
||||
|
||||
@GetMapping({ "", "/" })
|
||||
public Map<String, Object> deployed() {
|
||||
return deployer.deployed();
|
||||
}
|
||||
|
||||
private String deploy(String name, String path, String... args) throws Exception {
|
||||
String deployed = deployer.deploy(name, path, args);
|
||||
return deployed;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2017 the original author or authors.
|
||||
* Copyright 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.
|
||||
@@ -13,15 +13,22 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.function.deployer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class DeployedFunctionApplication {
|
||||
public class FunctionApplication {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
new ApplicationBootstrap().run(FunctionApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
/*
|
||||
* Copyright 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.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
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.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.loader.JarLauncher;
|
||||
import org.springframework.boot.loader.archive.Archive;
|
||||
import org.springframework.boot.loader.archive.JarFileArchive;
|
||||
import org.springframework.cloud.deployer.resource.maven.MavenProperties;
|
||||
import org.springframework.cloud.deployer.resource.maven.MavenResource;
|
||||
import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader;
|
||||
import org.springframework.cloud.deployer.resource.support.DelegatingResourceLoader;
|
||||
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||
import org.springframework.cloud.function.context.FunctionRegistry;
|
||||
import org.springframework.cloud.function.context.FunctionType;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* Registers beans that will be picked up by spring-cloud-function-context magic. Sets up
|
||||
* infrastructure capable of instantiating a "functional" bean (whether Supplier, Function
|
||||
* or Consumer) loaded dynamically according to {@link FunctionProperties}.
|
||||
*
|
||||
* <p>
|
||||
* Resolves jar location provided by the user using a flexible ResourceLoader.
|
||||
* </p>
|
||||
*
|
||||
* @author Eric Bottard
|
||||
* @author Mark Fisher
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties
|
||||
public class FunctionConfiguration {
|
||||
|
||||
private static Log logger = LogFactory.getLog(FunctionConfiguration.class);
|
||||
|
||||
@Autowired
|
||||
private FunctionRegistry registry;
|
||||
|
||||
@Autowired
|
||||
private FunctionProperties properties;
|
||||
|
||||
@Autowired
|
||||
private DelegatingResourceLoader delegatingResourceLoader;
|
||||
|
||||
@Autowired
|
||||
private ConfigurableApplicationContext context;
|
||||
|
||||
private BeanCreatorClassLoader functionClassLoader;
|
||||
|
||||
private BeanCreator creator;
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties("maven")
|
||||
public MavenProperties mavenProperties() {
|
||||
return new MavenProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConfigurationProperties("function")
|
||||
public FunctionProperties functionProperties() {
|
||||
return new FunctionProperties();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(DelegatingResourceLoader.class)
|
||||
public DelegatingResourceLoader delegatingResourceLoader(
|
||||
MavenProperties mavenProperties) {
|
||||
Map<String, ResourceLoader> loaders = new HashMap<>();
|
||||
loaders.put(MavenResource.URI_SCHEME, new MavenResourceLoader(mavenProperties));
|
||||
return new DelegatingResourceLoader(loaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a function for each of the function classes passed into the
|
||||
* {@link FunctionProperties}. They are named sequentially "function0", "function1",
|
||||
* etc. The instances are created in an isolated class loader, so the jar they are
|
||||
* packed in has to define all the dependencies (except core JDK).
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
URL[] urls = Arrays.stream(properties.getLocation())
|
||||
.flatMap(toResourceURL(delegatingResourceLoader)).toArray(URL[]::new);
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
"Locating function from " + Arrays.asList(properties.getLocation()));
|
||||
this.creator = new BeanCreator(expand(urls));
|
||||
this.creator.run(properties.getMain());
|
||||
Arrays.stream(properties.getBean()).map(this.creator::create).sequential()
|
||||
.forEach(this.creator::register);
|
||||
if (properties.getName().contains("|")) {
|
||||
// A composite function has to be explicitly registered before it is
|
||||
// looked up because we are using the SingleEntryFunctionRegistry
|
||||
this.registry.lookup(Consumer.class, properties.getName());
|
||||
this.registry.lookup(Function.class, properties.getName());
|
||||
this.registry.lookup(Supplier.class, properties.getName());
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot create functions", e);
|
||||
}
|
||||
}
|
||||
|
||||
private URL[] expand(URL[] urls) {
|
||||
List<URL> result = new ArrayList<>();
|
||||
for (URL url : urls) {
|
||||
result.addAll(expand(url));
|
||||
}
|
||||
return result.toArray(new URL[0]);
|
||||
}
|
||||
|
||||
private List<URL> expand(URL url) {
|
||||
if (!"file".equals(url.getProtocol())) {
|
||||
return Collections.singletonList(url);
|
||||
}
|
||||
if (!url.toString().endsWith(".jar")) {
|
||||
return Collections.singletonList(url);
|
||||
}
|
||||
try {
|
||||
JarFileArchive archive = new JarFileArchive(new File(url.toURI()));
|
||||
return Arrays.asList(new ComputeLauncher(archive).getClassLoaderUrls());
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot create class loader for " + url, e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void close() {
|
||||
if (this.creator != null) {
|
||||
this.creator.close();
|
||||
}
|
||||
if (this.functionClassLoader != null) {
|
||||
try {
|
||||
this.functionClassLoader.close();
|
||||
this.functionClassLoader = null;
|
||||
Runtime.getRuntime().gc();
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("Cannot close function class loader", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Function<String, Stream<URL>> toResourceURL(
|
||||
DelegatingResourceLoader resourceLoader) {
|
||||
return l -> {
|
||||
if (l.equals("app:classpath")) {
|
||||
return Stream
|
||||
.of(((URLClassLoader) getClass().getClassLoader()).getURLs());
|
||||
}
|
||||
try {
|
||||
return Stream.of(resourceLoader.getResource(l).getFile().toURI().toURL());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class ComputeLauncher extends JarLauncher {
|
||||
|
||||
public ComputeLauncher(JarFileArchive archive) {
|
||||
super(archive);
|
||||
}
|
||||
|
||||
public URL[] getClassLoaderUrls() throws Exception {
|
||||
List<Archive> archives = getClassPathArchives();
|
||||
if (archives.isEmpty()) {
|
||||
return new URL[] { getArchive().getUrl() };
|
||||
}
|
||||
return archives.stream().map(archive -> {
|
||||
try {
|
||||
return archive.getUrl();
|
||||
}
|
||||
catch (MalformedURLException e) {
|
||||
throw new IllegalStateException("Bad URL: " + archive, e);
|
||||
}
|
||||
}).collect(Collectors.toList()).toArray(new URL[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the bean and spring application context creation concerns for
|
||||
* functions. Creates a single application context if <code>run()</code> is called
|
||||
* with a non-null main class, and then uses it to lookup a function (by name and then
|
||||
* by type).
|
||||
*/
|
||||
private class BeanCreator {
|
||||
|
||||
private AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
private ApplicationRunner runner;
|
||||
|
||||
public BeanCreator(URL[] urls) {
|
||||
functionClassLoader = new BeanCreatorClassLoader(urls,
|
||||
getClass().getClassLoader().getParent());
|
||||
}
|
||||
|
||||
public void run(String main) {
|
||||
if (main == null) {
|
||||
return;
|
||||
}
|
||||
if (ClassUtils.isPresent(SpringApplication.class.getName(),
|
||||
functionClassLoader)) {
|
||||
logger.info("SpringApplication available. Bootstrapping: " + main);
|
||||
ClassLoader contextClassLoader = ClassUtils
|
||||
.overrideThreadContextClassLoader(functionClassLoader);
|
||||
try {
|
||||
ApplicationRunner runner = new ApplicationRunner(functionClassLoader,
|
||||
main);
|
||||
// TODO: make the runtime properties configurable
|
||||
runner.run("--spring.main.webEnvironment=false",
|
||||
"--spring.cloud.stream.enabled=false",
|
||||
"--spring.main.bannerMode=OFF",
|
||||
"--spring.main.webApplicationType=none");
|
||||
this.runner = runner;
|
||||
}
|
||||
finally {
|
||||
ClassUtils.overrideThreadContextClassLoader(contextClassLoader);
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(
|
||||
"SpringApplication not available and main class requested: "
|
||||
+ main);
|
||||
}
|
||||
}
|
||||
|
||||
public Object create(String type) {
|
||||
ClassLoader contextClassLoader = ClassUtils
|
||||
.overrideThreadContextClassLoader(functionClassLoader);
|
||||
AutowireCapableBeanFactory factory = context.getAutowireCapableBeanFactory();
|
||||
try {
|
||||
Object result = null;
|
||||
if (this.runner != null) {
|
||||
result = this.runner.getBean(type);
|
||||
}
|
||||
if (result == null) {
|
||||
logger.info("No bean found. Instantiating: " + type);
|
||||
if (ClassUtils.isPresent(type, functionClassLoader)) {
|
||||
result = factory.createBean(
|
||||
ClassUtils.resolveClassName(type, functionClassLoader));
|
||||
}
|
||||
}
|
||||
if (result != null) {
|
||||
logger.info("Located bean: " + type);
|
||||
return result;
|
||||
}
|
||||
throw new IllegalStateException("Cannot create bean for: " + type);
|
||||
}
|
||||
finally {
|
||||
ClassUtils.overrideThreadContextClassLoader(contextClassLoader);
|
||||
}
|
||||
}
|
||||
|
||||
public void register(Object bean) {
|
||||
if (bean == null) {
|
||||
return;
|
||||
}
|
||||
FunctionRegistration<Object> registration = new FunctionRegistration<Object>(
|
||||
bean).names(
|
||||
FunctionProperties.functionName(counter.getAndIncrement()));
|
||||
if (this.runner != null) {
|
||||
if (this.runner.containsBean(FunctionInspector.class.getName())) {
|
||||
Object inspector = this.runner
|
||||
.getBean(FunctionInspector.class.getName());
|
||||
Class<?> input = (Class<?>) this.runner.evaluate(
|
||||
"getInputType(#function)", inspector, "function", bean);
|
||||
FunctionType type = FunctionType.from(input);
|
||||
Class<?> output = findType("getOutputType", inspector, bean);
|
||||
type = type.to(output);
|
||||
if (((Boolean) this.runner.evaluate("isMessage(#function)", inspector,
|
||||
"function", bean))) {
|
||||
type = type.message();
|
||||
}
|
||||
Class<?> inputWrapper = findType("getInputWrapper", inspector, bean);
|
||||
if (FunctionType.isWrapper(inputWrapper)) {
|
||||
type = type.wrap(inputWrapper);
|
||||
}
|
||||
Class<?> outputWrapper = findType("getOutputWrapper", inspector,
|
||||
bean);
|
||||
if (FunctionType.isWrapper(outputWrapper)) {
|
||||
type = type.wrap(outputWrapper);
|
||||
}
|
||||
registration.type(type.getType());
|
||||
}
|
||||
}
|
||||
else {
|
||||
registration.type(FunctionType.of(bean.getClass()).getType());
|
||||
}
|
||||
registration.target(bean);
|
||||
registry.register(registration);
|
||||
}
|
||||
|
||||
private Class<?> findType(String method, Object inspector, Object bean) {
|
||||
return (Class<?>) this.runner.evaluate(method + "(#function)", inspector,
|
||||
"function", bean);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (this.runner != null) {
|
||||
this.runner.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class BeanCreatorClassLoader extends URLClassLoader {
|
||||
private BeanCreatorClassLoader(URL[] urls, ClassLoader parent) {
|
||||
super(urls, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve)
|
||||
throws ClassNotFoundException {
|
||||
try {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
catch (ClassNotFoundException e) {
|
||||
if (name.contains(ContextRunner.class.getName())) {
|
||||
// Special case for the ContextRunner. We can re-use the bytes for it,
|
||||
// and the function jar doesn't have to include them since it is only
|
||||
// used here.
|
||||
byte[] bytes;
|
||||
try {
|
||||
bytes = StreamUtils.copyToByteArray(
|
||||
getClass().getClassLoader().getResourceAsStream(
|
||||
ClassUtils.convertClassNameToResourcePath(name)
|
||||
+ ".class"));
|
||||
return defineClass(name, bytes, 0, bytes.length);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ClassNotFoundException(
|
||||
"Cannot find runner class: " + name, ex);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected static class SingleEntryConfiguration implements BeanPostProcessor {
|
||||
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName)
|
||||
throws BeansException {
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName)
|
||||
throws BeansException {
|
||||
String name = FunctionProperties
|
||||
.functionName(env.getProperty("function.bean", ""));
|
||||
if (bean instanceof FunctionRegistry && name.contains("|")) {
|
||||
bean = new SingleEntryFunctionRegistry((FunctionRegistry) bean, name);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* 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.context.config.ContextFunctionCatalogAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(FunctionExtractingFunctionCatalog.class)
|
||||
@AutoConfigureBefore(ContextFunctionCatalogAutoConfiguration.class)
|
||||
public class FunctionExtractingAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FunctionExtractingFunctionCatalog functionCatalog() {
|
||||
return new FunctionExtractingFunctionCatalog();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
/*
|
||||
* 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.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.loader.thin.ArchiveUtils;
|
||||
import org.springframework.cloud.deployer.spi.app.AppDeployer;
|
||||
import org.springframework.cloud.deployer.spi.core.AppDefinition;
|
||||
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
|
||||
import org.springframework.cloud.deployer.thin.ThinJarAppDeployer;
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||
import org.springframework.cloud.function.context.FunctionType;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||
import org.springframework.cloud.function.stream.config.SupplierInvokingMessageProducer;
|
||||
import org.springframework.cloud.stream.binder.servlet.RouteRegistrar;
|
||||
import org.springframework.context.support.LiveBeansView;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.MethodInvoker;
|
||||
|
||||
public class FunctionExtractingFunctionCatalog
|
||||
implements FunctionCatalog, FunctionInspector, DisposableBean {
|
||||
|
||||
private static Log logger = LogFactory
|
||||
.getLog(FunctionExtractingFunctionCatalog.class);
|
||||
|
||||
private RouteRegistrar routes;
|
||||
|
||||
private SupplierInvokingMessageProducer<?> producer;
|
||||
|
||||
private ThinJarAppDeployer deployer;
|
||||
|
||||
private Map<String, String> deployed = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, String> names = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, String> ids = new LinkedHashMap<>();
|
||||
|
||||
public FunctionExtractingFunctionCatalog() {
|
||||
this("thin", "slim");
|
||||
}
|
||||
|
||||
public FunctionExtractingFunctionCatalog(String name, String... profiles) {
|
||||
deployer = new ThinJarAppDeployer(name, profiles);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setRouteRegistrar(RouteRegistrar routes) {
|
||||
this.routes = routes;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setProducer(SupplierInvokingMessageProducer<?> producer) {
|
||||
this.producer = producer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
for (String name : new HashSet<>(names.keySet())) {
|
||||
undeploy(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public FunctionRegistration<?> getRegistration(Object function) {
|
||||
String name = getName(function);
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
return new FunctionRegistration<>(function).name(name)
|
||||
.type(findType(function).getType());
|
||||
}
|
||||
|
||||
private FunctionType findType(Object function) {
|
||||
FunctionType type = FunctionType.from((Class<?>) type(function, "getInputType"))
|
||||
.to((Class<?>) type(function, "getOutputType"))
|
||||
.wrap((Class<?>) type(function, "getInputWrapper"));
|
||||
if ((Boolean) type(function, "isMessage")) {
|
||||
type = type.message();
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T lookup(Class<?> type, String name) {
|
||||
return (T) lookup(type, name, "lookup");
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Set<String> getNames(Class<?> type) {
|
||||
return (Set<String>) getNames("getNames", type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Object function) {
|
||||
Set<String> names = getNames(function);
|
||||
return names.isEmpty() ? null : names.iterator().next();
|
||||
}
|
||||
|
||||
public String deploy(String name, String path, String... args) {
|
||||
Resource resource = new FileSystemResource(
|
||||
ArchiveUtils.getArchiveRoot(ArchiveUtils.getArchive(path)));
|
||||
AppDefinition definition = new AppDefinition(resource.getFilename(),
|
||||
Collections.singletonMap(LiveBeansView.MBEAN_DOMAIN_PROPERTY_NAME,
|
||||
"functions." + name));
|
||||
AppDeploymentRequest request = new AppDeploymentRequest(definition, resource,
|
||||
Collections.singletonMap(AppDeployer.GROUP_PROPERTY_KEY, "functions"),
|
||||
Arrays.asList(args));
|
||||
String id = this.deployer.deploy(request);
|
||||
this.deployed.put(id, path);
|
||||
this.names.put(name, id);
|
||||
this.ids.put(id, name);
|
||||
register(name);
|
||||
return id;
|
||||
}
|
||||
|
||||
public DeployedArtifact undeploy(String name) {
|
||||
String id = this.names.get(name);
|
||||
if (id == null) {
|
||||
// TODO: Convert to 404
|
||||
throw new IllegalStateException("No such app");
|
||||
}
|
||||
unregister(name);
|
||||
this.deployer.undeploy(id);
|
||||
String path = this.deployed.remove(id);
|
||||
this.names.remove(name);
|
||||
this.ids.remove(id);
|
||||
return new DeployedArtifact(name, id, path);
|
||||
}
|
||||
|
||||
private void register(String name) {
|
||||
Set<String> names = getSupplierNames(name);
|
||||
if (routes != null) {
|
||||
logger.info("Registering routes: " + names);
|
||||
routes.registerRoutes(getSupplierNames(name));
|
||||
}
|
||||
if (producer != null) {
|
||||
// Need an ApplicationEvent that we can react to in the producer?
|
||||
for (String supplier : names) {
|
||||
producer.start(supplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<String> getSupplierNames(String name) {
|
||||
String id = this.names.get(name);
|
||||
return (Set<String>) invoke(id, FunctionCatalog.class, "getNames",
|
||||
Supplier.class);
|
||||
}
|
||||
|
||||
private void unregister(String name) {
|
||||
Set<String> names = getSupplierNames(name);
|
||||
if (routes != null) {
|
||||
logger.info("Unregistering routes: " + names);
|
||||
routes.unregisterRoutes(names);
|
||||
}
|
||||
if (producer != null) {
|
||||
for (String supplier : names) {
|
||||
producer.stop(supplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getNames(Object arg) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Inspecting names");
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> result = (Set<String>) invoke(FunctionInspector.class,
|
||||
"getRegistration", this::extractNames, arg);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Set<String> extractNames(String id, Object result) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<String> prefixed = (Set<String>) prefix(id, invoke(result, "getNames"));
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Result (from " + this.ids.get(id) + "): " + prefixed);
|
||||
}
|
||||
if (prefixed.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return prefixed;
|
||||
}
|
||||
|
||||
private Object type(Object arg, String method) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Inspecting type " + method);
|
||||
}
|
||||
Object result = invoke(invoke(invoke(FunctionInspector.class, "getRegistration",
|
||||
this::discardEmpty, arg), "getType"), method);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Result: " + result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object discardEmpty(String id, Object result) {
|
||||
if (result == null || invoke(result, "getTarget") == null) {
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Object prefix(String id, Object result) {
|
||||
String name = this.ids.get(id);
|
||||
String prefix = name + "/";
|
||||
if (result != null) {
|
||||
if (result instanceof Collection) {
|
||||
Set<String> results = new LinkedHashSet<>();
|
||||
for (Object value : (Collection<?>) result) {
|
||||
results.add(prefix + value);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
else if (result instanceof String) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Prefixed (from \" + name + \"): " + result);
|
||||
}
|
||||
return prefix + result;
|
||||
}
|
||||
|
||||
else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object lookup(Class<?> type, String name, String method) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Looking up " + type + " named " + name + " with " + method);
|
||||
}
|
||||
return invoke(FunctionCatalog.class, method, type, name);
|
||||
}
|
||||
|
||||
private Object getNames(String method, Class<?> type) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Calling " + method);
|
||||
}
|
||||
return invoke(FunctionCatalog.class, method, type);
|
||||
}
|
||||
|
||||
private Object invoke(Class<?> type, String method, Object... arg) {
|
||||
return invoke(type, method, null, arg);
|
||||
}
|
||||
|
||||
private Object invoke(Class<?> type, String method, Callback<?> callback,
|
||||
Object... arg) {
|
||||
Set<Object> results = new LinkedHashSet<>();
|
||||
Object fallback = null;
|
||||
for (String id : this.deployed.keySet()) {
|
||||
Object result = invoke(id, type, method, arg);
|
||||
if (result instanceof Collection) {
|
||||
results.addAll((Collection<?>) result);
|
||||
continue;
|
||||
}
|
||||
if (result != null) {
|
||||
if (result == Object.class) {
|
||||
// Type fallback is Object
|
||||
fallback = Object.class;
|
||||
continue;
|
||||
}
|
||||
if (result instanceof Boolean && !((Boolean) result)) {
|
||||
// Boolean fallback is false
|
||||
fallback = false;
|
||||
continue;
|
||||
}
|
||||
if (callback != null) {
|
||||
result = callback.call(id, result);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Results: " + results);
|
||||
}
|
||||
return "lookup".equals(method) ? null : results;
|
||||
}
|
||||
|
||||
private Object invoke(String id, Class<?> type, String method, Object... arg) {
|
||||
Object catalog = this.deployer.getBean(id, type);
|
||||
if (catalog == null) {
|
||||
return null;
|
||||
}
|
||||
String name = this.ids.get(id);
|
||||
String prefix = name + "/";
|
||||
if (arg.length == 2 && arg[0] instanceof Class) {
|
||||
if (arg[1] instanceof String) {
|
||||
String specific = arg[1].toString();
|
||||
if (specific.startsWith(prefix)) {
|
||||
arg[1] = specific.substring(prefix.length());
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
Object result = invoke(catalog, method, arg);
|
||||
return prefix(id, result);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot extract", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Object invoke(Object target, String method, Object... arg) {
|
||||
MethodInvoker invoker = new MethodInvoker();
|
||||
invoker.setTargetObject(target);
|
||||
invoker.setTargetMethod(method);
|
||||
invoker.setArguments(arg);
|
||||
try {
|
||||
invoker.prepare();
|
||||
return invoker.invoke();
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot invoke method", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> deployed() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (String name : this.names.keySet()) {
|
||||
String id = this.names.get(name);
|
||||
result.put(name, new DeployedArtifact(name, id, this.deployed.get(id)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface Callback<T> {
|
||||
T call(String id, Object result);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 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.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Configuration properties for deciding how to locate the functional class to execute.
|
||||
*
|
||||
* @author Eric Bottard
|
||||
*/
|
||||
public class FunctionProperties {
|
||||
|
||||
/**
|
||||
* Location(s) of jar archives containing the supplier/function/consumer class to run.
|
||||
*/
|
||||
private String[] location = new String[0];
|
||||
|
||||
/**
|
||||
* The bean name or fully qualified class name of the supplier/function/consumer to
|
||||
* run.
|
||||
*/
|
||||
private String[] bean = new String[0];
|
||||
|
||||
/**
|
||||
* Optional main class from which to build a Spring application context
|
||||
*/
|
||||
private String main;
|
||||
|
||||
public String getName() {
|
||||
return functionName(StringUtils.arrayToDelimitedString(bean, ","));
|
||||
}
|
||||
|
||||
public String[] getBean() {
|
||||
return bean;
|
||||
}
|
||||
|
||||
public void setBean(String[] bean) {
|
||||
this.bean = bean;
|
||||
}
|
||||
|
||||
public String[] getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String[] location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getMain() {
|
||||
return main;
|
||||
}
|
||||
|
||||
public void setMain(String main) {
|
||||
this.main = main;
|
||||
}
|
||||
|
||||
public static String functionName(String name) {
|
||||
if (!name.contains(",")) {
|
||||
return "function0";
|
||||
}
|
||||
List<String> names = new ArrayList<>();
|
||||
for (int i = 0; i <= StringUtils.countOccurrencesOf(name, ","); i++) {
|
||||
names.add("function" + i);
|
||||
}
|
||||
return StringUtils.collectionToDelimitedString(names, "|");
|
||||
}
|
||||
|
||||
public static String functionName(int value) {
|
||||
return "function" + value;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
if (location.length == 0) {
|
||||
throw new IllegalStateException(
|
||||
"No archive location provided, please configure function.location as a jar or directory.");
|
||||
}
|
||||
if (bean.length == 0) {
|
||||
throw new IllegalStateException(
|
||||
"No function bean locator provided, please configure function.bean as a bean name or class name.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||
import org.springframework.cloud.function.context.FunctionRegistry;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class SingleEntryFunctionRegistry implements FunctionRegistry {
|
||||
|
||||
private final FunctionRegistry delegate;
|
||||
|
||||
private final String name;
|
||||
|
||||
public SingleEntryFunctionRegistry(FunctionRegistry delegate, String name) {
|
||||
this.delegate = delegate;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T lookup(Class<?> type, String name) {
|
||||
return this.name.equals(name) ? this.delegate.lookup(type, name) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getNames(Class<?> type) {
|
||||
Set<String> names = this.delegate.getNames(type);
|
||||
return names.contains(this.name) ? Collections.singleton(this.name)
|
||||
: Collections.emptySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> void register(FunctionRegistration<T> registration) {
|
||||
this.delegate.register(registration);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.function.deployer.FunctionExtractingAutoConfiguration
|
||||
org.springframework.cloud.function.deployer.FunctionConfiguration
|
||||
@@ -1,15 +0,0 @@
|
||||
exclusions.spring-web-reactive: org.springframework:spring-web-reactive
|
||||
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-servlet: org.springframework.cloud:spring-cloud-stream-binder-servlet
|
||||
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-web: org.springframework:spring-web
|
||||
dependencies.jackson-databind: com.fasterxml.jackson.core:jackson-databind
|
||||
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
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?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.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>1.5.11.RELEASE</version>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<reactor.version>3.1.4.RELEASE</reactor.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>Edgware.RELEASE</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
</project>
|
||||
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2015 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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.junit.Assume;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Parameterized;
|
||||
|
||||
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.cloud.deployer.thin.ThinJarAppDeployer;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Mark Fisher
|
||||
*/
|
||||
@RunWith(Parameterized.class)
|
||||
public class FunctionAppDeployerTests {
|
||||
|
||||
static {
|
||||
LogbackInitializer.initialize();
|
||||
}
|
||||
|
||||
private static ThinJarAppDeployer deployer = new ThinJarAppDeployer();
|
||||
|
||||
@BeforeClass
|
||||
public static void skip() {
|
||||
try {
|
||||
ArchiveUtils.getArchiveRoot(ArchiveUtils
|
||||
.getArchive("maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
Assume.assumeNoException(
|
||||
"Could not locate jar for tests. Please build spring-cloud-function locally first.",
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
@Parameterized.Parameters
|
||||
public static List<Object[]> data() {
|
||||
// Repeat a couple of times to ensure it's consistent
|
||||
return Arrays.asList(new Object[2][0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void directory() throws Exception {
|
||||
String first = deploy("file:../spring-cloud-function-samples/function-sample/target/classes", "",
|
||||
"--spring.cloud.function.stream.supplier.enabled=false");
|
||||
// Deployment is blocking so it either failed or succeeded.
|
||||
assertThat(deployer.status(first).getState()).isEqualTo(DeploymentState.deployed);
|
||||
deployer.undeploy(first);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void web() throws Exception {
|
||||
String first = deploy("maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT", "",
|
||||
"--spring.cloud.function.stream.supplier.enabled=false");
|
||||
// Deployment is blocking so it either failed or succeeded.
|
||||
assertThat(deployer.status(first).getState()).isEqualTo(DeploymentState.deployed);
|
||||
deployer.undeploy(first);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void stream() throws Exception {
|
||||
String first = deploy("maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT",
|
||||
"spring.cloud.deployer.thin.profile=rabbit",
|
||||
"--spring.cloud.function.stream.supplier.enabled=false", "--debug=true");
|
||||
// Deployment is blocking so it either failed or succeeded.
|
||||
assertThat(deployer.status(first).getState()).isEqualTo(DeploymentState.deployed);
|
||||
deployer.undeploy(first);
|
||||
}
|
||||
|
||||
private String deploy(String jarName, String properties, 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,
|
||||
properties(properties), Arrays.asList(args));
|
||||
String deployed = deployer.deploy(request);
|
||||
return deployed;
|
||||
}
|
||||
|
||||
private Map<String, String> properties(String properties) {
|
||||
Map<String, String> map = new LinkedHashMap<>();
|
||||
Properties props = StringUtils.splitArrayElementsIntoProperties(
|
||||
StringUtils.commaDelimitedListToStringArray(properties), "=");
|
||||
if (props != null) {
|
||||
for (Object name : props.keySet()) {
|
||||
String key = (String) name;
|
||||
map.put(key, props.getProperty(key));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 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.concurrent.TimeUnit;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.stream.messaging.Processor;
|
||||
import org.springframework.cloud.stream.messaging.Sink;
|
||||
import org.springframework.cloud.stream.messaging.Source;
|
||||
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FunctionConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@TestPropertySource(properties = {
|
||||
"function.location=file:target/it/support/target/function-sample-1.0.0.M1.jar", })
|
||||
public abstract class FunctionConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
protected MessageCollector messageCollector;
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = { "function.bean=com.example.functions.Emitter" })
|
||||
public static class SourceTests extends FunctionConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Source source;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
|
||||
Message<?> received = messageCollector.forChannel(source.output()).poll(2,
|
||||
TimeUnit.SECONDS);
|
||||
assertThat(received.getPayload(), Matchers.is("one"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = {
|
||||
"function.bean=com.example.functions.Emitter,com.example.functions.LengthCounter" })
|
||||
public static class CompositeTests extends FunctionConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Source source;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
|
||||
Message<?> received = messageCollector.forChannel(source.output()).poll(2,
|
||||
TimeUnit.SECONDS);
|
||||
assertThat(received.getPayload(), Matchers.is(3));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = {
|
||||
"function.bean=com.example.functions.LengthCounter" })
|
||||
public static class ProcessorTests extends FunctionConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Processor processor;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
processor.input().send(MessageBuilder.withPayload("hello").build());
|
||||
Message<?> received = messageCollector.forChannel(processor.output()).poll(1,
|
||||
TimeUnit.SECONDS);
|
||||
assertThat(received.getPayload(), Matchers.is("hello".length()));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = {
|
||||
"function.bean=com.example.functions.DoubleLogger" })
|
||||
public static class SinkTests extends FunctionConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Sink sink;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
// Can't assert side effects.
|
||||
sink.input().send(MessageBuilder.withPayload(5).build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
/*
|
||||
* 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.net.URI;
|
||||
|
||||
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.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.SocketUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class FunctionExtractingFunctionCatalogIntegrationTests {
|
||||
|
||||
private static ConfigurableApplicationContext context;
|
||||
private static int port;
|
||||
|
||||
@BeforeClass
|
||||
public static void open() throws Exception {
|
||||
port = SocketUtils.findAvailableTcpPort();
|
||||
// System.setProperty("debug", "true");
|
||||
context = new ApplicationRunner().start("--server.port=" + port, "--debug",
|
||||
"--logging.level.org.springframework.cloud.function=DEBUG");
|
||||
deploy("sample", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
||||
}
|
||||
|
||||
private static void deploy(String name, String path) throws Exception {
|
||||
ResponseEntity<String> result = new TestRestTemplate().postForEntity(
|
||||
"http://localhost:" + port + "/admin/" + name + "?path=" + path, "",
|
||||
String.class);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
private static String undeploy(String name) throws Exception {
|
||||
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
||||
.delete(new URI("http://localhost:" + port + "/admin/" + name)).build(),
|
||||
String.class);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
return result.getBody();
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void close() {
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listing() {
|
||||
assertThat(new TestRestTemplate()
|
||||
.getForObject("http://localhost:" + port + "/admin", String.class))
|
||||
.startsWith("{").contains("sample");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void words() {
|
||||
assertThat(new TestRestTemplate().getForObject(
|
||||
"http://localhost:" + port + "/stream/sample/words", String.class))
|
||||
.isEqualTo("[\"foo\",\"bar\"]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missing() throws Exception {
|
||||
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
||||
.get(new URI("http://localhost:" + port + "/stream/missing/words"))
|
||||
.build(), String.class);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uppercase() throws Exception {
|
||||
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
||||
.post(new URI("http://localhost:" + port + "/stream/sample/uppercase"))
|
||||
.contentType(MediaType.TEXT_PLAIN)
|
||||
.body("foo"), String.class);
|
||||
assertThat(result.getBody()).isEqualTo("FOO");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void another() throws Exception {
|
||||
deploy("pof",
|
||||
"maven://io.spring.sample:function-sample-pof:jar:exec:1.0.0.BUILD-SNAPSHOT");
|
||||
assertThat(new TestRestTemplate().postForObject(
|
||||
"http://localhost:" + port + "/stream/pof/greeter", "Foo",
|
||||
String.class)).isEqualTo("Hello Foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cycle() throws Exception {
|
||||
String undeploy = undeploy("sample");
|
||||
assertThat(undeploy.contains("\"name\":\"sample\""));
|
||||
assertThat(undeploy.contains(
|
||||
"\"path\":\"maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT\""));
|
||||
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
||||
.get(new URI("http://localhost:" + port + "/stream/sample/words"))
|
||||
.build(), String.class);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
deploy("sample", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
||||
assertThat(new TestRestTemplate().postForObject(
|
||||
"http://localhost:" + port + "/stream/sample/uppercase", "foo",
|
||||
String.class)).isEqualTo("FOO");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
/*
|
||||
* 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.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.boot.loader.tools.LogbackInitializer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
public class FunctionExtractingFunctionCatalogTests {
|
||||
|
||||
private static String id;
|
||||
|
||||
static {
|
||||
LogbackInitializer.initialize();
|
||||
}
|
||||
|
||||
private static FunctionExtractingFunctionCatalog deployer = new FunctionExtractingFunctionCatalog();
|
||||
|
||||
@Rule
|
||||
public ExpectedException expected = ExpectedException.none();
|
||||
|
||||
@Before
|
||||
public void init() throws Exception {
|
||||
if (id == null) {
|
||||
deploy("sample",
|
||||
"maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
||||
// "--debug");
|
||||
id = deploy("pojos",
|
||||
"maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void close() {
|
||||
if (id != null) {
|
||||
deployer.undeploy("sample");
|
||||
deployer.undeploy("pojos");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listFunctions() throws Exception {
|
||||
assertThat(deployer.getNames(Function.class)).contains("sample/uppercase",
|
||||
"pojos/uppercase");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameFunction() throws Exception {
|
||||
assertThat(deployer.getName(deployer.lookup(Function.class, "sample/uppercase")))
|
||||
.isEqualTo("sample/uppercase");
|
||||
}
|
||||
|
||||
@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);
|
||||
Function<Flux<String>, Flux<String>> function = deployer.lookup(Function.class,
|
||||
"pojos/uppercase");
|
||||
Flux<String> result = function.apply(Flux.just("foo"));
|
||||
assertThat(result.blockFirst()).isEqualTo("FOO");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listConsumers() throws Exception {
|
||||
assertThat(deployer.getNames(Consumer.class)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deployAndExtractConsumers() throws Exception {
|
||||
assertThat(deployer.<Consumer<?>>lookup(Consumer.class, "pojos/sink")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listSuppliers() throws Exception {
|
||||
assertThat(deployer.getNames(Supplier.class)).contains("sample/words",
|
||||
"pojos/words");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameSupplier() throws Exception {
|
||||
assertThat(deployer.getName(deployer.lookup(Supplier.class, "sample/words")))
|
||||
.isEqualTo("sample/words");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deployAndExtractSuppliers() throws Exception {
|
||||
assertThat(deployer.<Supplier<?>>lookup(Supplier.class, "sample/words"))
|
||||
.isNotNull();
|
||||
assertThat(deployer.<Supplier<?>>lookup(Supplier.class, "pojos/words"))
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
private static String deploy(String name, String path, String... args)
|
||||
throws Exception {
|
||||
String deployed = deployer.deploy(name, path, args);
|
||||
return deployed;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 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.concurrent.TimeUnit;
|
||||
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.stream.messaging.Processor;
|
||||
import org.springframework.cloud.stream.messaging.Sink;
|
||||
import org.springframework.cloud.stream.messaging.Source;
|
||||
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest(classes = FunctionConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
|
||||
@TestPropertySource(properties = {
|
||||
"function.location=file:target/it/support/target/function-sample-1.0.0.M1-exec.jar", })
|
||||
public abstract class SpringFunctionAppConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
protected MessageCollector messageCollector;
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = { "function.bean=myEmitter",
|
||||
"function.main=com.example.functions.FunctionApp" })
|
||||
public static class SourceTests extends SpringFunctionAppConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Source source;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
|
||||
Message<?> received = messageCollector.forChannel(source.output()).poll(2,
|
||||
TimeUnit.SECONDS);
|
||||
assertThat(received.getPayload(), Matchers.is("one"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = { "function.bean=myEmitter,myCounter",
|
||||
"function.main=com.example.functions.FunctionApp" })
|
||||
public static class CompositeTests extends SpringFunctionAppConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Source source;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
|
||||
Message<?> received = messageCollector.forChannel(source.output()).poll(2,
|
||||
TimeUnit.SECONDS);
|
||||
assertThat(received.getPayload(), Matchers.is(3));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = { "function.bean=myCounter",
|
||||
"function.main=com.example.functions.FunctionApp" })
|
||||
public static class ProcessorTests extends SpringFunctionAppConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Processor processor;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
processor.input().send(MessageBuilder.withPayload("hello").build());
|
||||
Message<?> received = messageCollector.forChannel(processor.output()).poll(1,
|
||||
TimeUnit.SECONDS);
|
||||
assertThat(received.getPayload(), Matchers.is("hello".length()));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@TestPropertySource(properties = { "function.bean=myDoubler",
|
||||
"function.main=com.example.functions.FunctionApp" })
|
||||
public static class SinkTests extends SpringFunctionAppConfigurationTests {
|
||||
|
||||
@Autowired
|
||||
private Sink sink;
|
||||
|
||||
@Test
|
||||
public void test() throws Exception {
|
||||
// Can't assert side effects.
|
||||
sink.input().send(MessageBuilder.withPayload(5).build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user