diff --git a/spring-cloud-function-deployer/README.md b/spring-cloud-function-deployer/README.md
index 069d4501d..252cf6fe8 100644
--- a/spring-cloud-function-deployer/README.md
+++ b/spring-cloud-function-deployer/README.md
@@ -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). |
diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml
index cfdc29e91..0981676a3 100644
--- a/spring-cloud-function-deployer/pom.xml
+++ b/spring-cloud-function-deployer/pom.xml
@@ -15,8 +15,8 @@
- 1.0.10.RELEASE1.0.10.RELEASE
+ 1.3.2.RELEASE
@@ -27,38 +27,40 @@
org.springframework.cloudspring-cloud-function-stream
-
-
- org.springframework.cloud
- spring-cloud-stream-binder-servlet
+ testorg.springframework.bootspring-boot-configuration-processortrue
+
+ org.springframework.boot
+ spring-boot-loader
+ org.springframework.cloud
- spring-cloud-deployer-thin
- ${spring-cloud-deployer-thin.version}
+ spring-cloud-deployer-resource-maven
+ ${spring.cloud.deployer.version}
+
+
+ org.springframework.cloud
+ spring-cloud-deployer-resource-support
+ ${spring.cloud.deployer.version}org.springframework.bootspring-boot-starter-testtest
+
+ org.springframework.cloud
+ spring-cloud-stream-test-support
+ true
+ test
+
-
-
-
- org.apache.maven
- maven-aether-provider
- 3.3.9
-
-
-
-
@@ -72,7 +74,65 @@
+
+ org.apache.maven.plugins
+ maven-invoker-plugin
+
+ ${project.build.directory}/local-repo
+
+
+
+ prepare-test
+ test-compile
+
+ run
+
+
+ ${project.build.directory}/it
+ src/it/settings.xml
+ true
+ ${skipTests}
+ true
+
+
+
+
+
+
+
+
+ org.eclipse.m2e
+ lifecycle-mapping
+ 1.0.0
+
+
+
+
+
+
+ org.apache.maven.plugins
+
+
+ maven-invoker-plugin
+
+
+ [1.10,)
+
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/spring-cloud-function-deployer/src/it/settings.xml b/spring-cloud-function-deployer/src/it/settings.xml
new file mode 100644
index 000000000..e1e0ace34
--- /dev/null
+++ b/spring-cloud-function-deployer/src/it/settings.xml
@@ -0,0 +1,35 @@
+
+
+
+
+ it-repo
+
+ true
+
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+ local.central
+ @localRepositoryUrl@
+
+ true
+
+
+ true
+
+
+
+
+
+
diff --git a/spring-cloud-function-deployer/src/it/support/pom.xml b/spring-cloud-function-deployer/src/it/support/pom.xml
new file mode 100644
index 000000000..8c375b326
--- /dev/null
+++ b/spring-cloud-function-deployer/src/it/support/pom.xml
@@ -0,0 +1,107 @@
+
+
+ 4.0.0
+
+ com.example
+ function-sample
+ 1.0.0.M1
+ jar
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 1.5.12.RELEASE
+
+
+
+
+ 1.8
+ 1.0.0.BUILD-SNAPSHOT
+ 1.0.10.RELEASE
+ 3.2.0.M1
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-function-context
+ ${spring-cloud-function.version}
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+ exec
+
+
+
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/libs-snapshot-local
+
+ true
+
+
+ false
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/libs-milestone-local
+
+ false
+
+
+
+ spring-releases
+ Spring Releases
+ https://repo.spring.io/release
+
+ false
+
+
+
+
+
+ spring-snapshots
+ Spring Snapshots
+ https://repo.spring.io/libs-snapshot-local
+
+ true
+
+
+ false
+
+
+
+ spring-milestones
+ Spring Milestones
+ https://repo.spring.io/libs-milestone-local
+
+ false
+
+
+
+ spring-releases
+ Spring Releases
+ https://repo.spring.io/libs-release-local
+
+ false
+
+
+
+
+
diff --git a/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/DoubleLogger.java b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/DoubleLogger.java
new file mode 100644
index 000000000..feb1c616e
--- /dev/null
+++ b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/DoubleLogger.java
@@ -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 {
+
+ @Override
+ public void accept(Integer i) {
+ System.out.println(2 * i);
+ }
+}
diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/Emitter.java
similarity index 51%
rename from spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java
rename to spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/Emitter.java
index 50831ca68..bafe64cf1 100644
--- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java
+++ b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/Emitter.java
@@ -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 {
+ private int i = 0;
+
+ private String[] values = {"one", "two", "three", "four"};
+
+ @Override
+ public String get() {
+ return values[i++ % values.length];
+ }
}
diff --git a/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/FunctionApp.java b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/FunctionApp.java
new file mode 100644
index 000000000..f3f56cc51
--- /dev/null
+++ b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/FunctionApp.java
@@ -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);
+ }
+}
diff --git a/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/LengthCounter.java b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/LengthCounter.java
new file mode 100644
index 000000000..a3eb4c38f
--- /dev/null
+++ b/spring-cloud-function-deployer/src/it/support/src/main/java/com/example/functions/LengthCounter.java
@@ -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 {
+
+ @Override
+ public Integer apply(String string) {
+ return string.length();
+ }
+}
diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationBootstrap.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationBootstrap.java
new file mode 100644
index 000000000..2904b89d8
--- /dev/null
+++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationBootstrap.java
@@ -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 child = new ArrayList<>();
+ List 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 list = Collections
+ .list(getClass().getClassLoader().getResources("META-INF"));
+ List 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 result = new ArrayList<>();
+ for (String element : path.split(" ")) {
+ result.add(new URL(element));
+ }
+ return result.toArray(new URL[0]);
+ }
+ }
+ catch (Exception e) {
+ }
+ }
+ return null;
+ }
+}
diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java
index 872d1ab72..96bb6c23d 100644
--- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java
+++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ApplicationRunner.java
@@ -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 @SpringBootApplication), 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 defaultProperties(String id) {
+ Map 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 beans = (Map) 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 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 child = new ArrayList<>();
- List parent = new ArrayList<>();
- for (URL url : urls) {
- child.add(url);
- }
- List 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 resolveParent() {
- DependencyResolver resolver = DependencyResolver.instance();
- List dependencies = resolver
- .dependencies(new ClassPathResource("core-pom.xml"));
- List resolved = new ArrayList<>();
- for (Dependency dependency : dependencies) {
- resolved.add(resolver.resolve(dependency));
- }
- return resolved;
}
}
diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java
new file mode 100644
index 000000000..72aca30ca
--- /dev/null
+++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java
@@ -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, 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 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> sources) {
+ for (Class> source : sources) {
+ if (AnnotatedElementUtils.hasAnnotation(source,
+ SpringBootConfiguration.class)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Set> 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