diff --git a/spring-cloud-function-deployer/README.md b/spring-cloud-function-deployer/README.md index 252cf6fe8..a2e4b0201 100644 --- a/spring-cloud-function-deployer/README.md +++ b/spring-cloud-function-deployer/README.md @@ -2,6 +2,7 @@ Spring Cloud Function Deployer is an library for building apps that can deploy f ```java @SpringBootApplication +@EnableFunctionDeployer public class FunctionApplication { public static void main(String[] args) throws IOException { diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml index 0981676a3..c64b3be00 100644 --- a/spring-cloud-function-deployer/pom.xml +++ b/spring-cloud-function-deployer/pom.xml @@ -24,11 +24,6 @@ org.springframework.cloud spring-cloud-function-context - - org.springframework.cloud - spring-cloud-function-stream - test - org.springframework.boot spring-boot-configuration-processor @@ -54,9 +49,8 @@ test - org.springframework.cloud - spring-cloud-stream-test-support - true + org.springframework.boot + spring-boot-starter-logging test 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 deleted file mode 100644 index 72aca30ca..000000000 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/BeanCountingApplicationListener.java +++ /dev/null @@ -1,118 +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.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 objects = (Set) ReflectionUtils.invokeMethod(method, - event.getSpringApplication()); - Set> result = new LinkedHashSet<>(); - for (Object object : objects) { - if (object instanceof String) { - object = ClassUtils.resolveClassName((String) object, null); - } - result.add((Class) object); - } - return result; - } - -} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java index 718556c65..c54e7ec7b 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/ContextRunner.java @@ -57,11 +57,6 @@ public class ContextRunner { 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); } diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/EnableFunctionDeployer.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/EnableFunctionDeployer.java new file mode 100644 index 000000000..355b0a296 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/EnableFunctionDeployer.java @@ -0,0 +1,39 @@ +/* + * 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.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Annotation to be used on a Spring Boot application if it wants to deploy a jar file + * containing a function definition. + * + * @author Dave Syer + * + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(FunctionDeployerConfiguration.class) +public @interface EnableFunctionDeployer { + +} \ No newline at end of file diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java index a6b929f31..0d58bb3d6 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionApplication.java @@ -25,6 +25,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; * @author Dave Syer */ @SpringBootApplication +@EnableFunctionDeployer public class FunctionApplication { public static void main(String[] args) throws IOException { diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionCreatorConfiguration.java similarity index 89% rename from spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionConfiguration.java rename to spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionCreatorConfiguration.java index 87af9ca01..97f5f3edf 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionConfiguration.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionCreatorConfiguration.java @@ -25,13 +25,12 @@ 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.jar.JarFile; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -46,25 +45,17 @@ 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; @@ -83,10 +74,9 @@ import org.springframework.util.StreamUtils; * @author Dave Syer */ @Configuration -@EnableConfigurationProperties -public class FunctionConfiguration { +class FunctionCreatorConfiguration { - private static Log logger = LogFactory.getLog(FunctionConfiguration.class); + private static Log logger = LogFactory.getLog(FunctionCreatorConfiguration.class); @Autowired private FunctionRegistry registry; @@ -104,27 +94,6 @@ public class FunctionConfiguration { 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 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", @@ -222,6 +191,13 @@ public class FunctionConfiguration { public URL[] getClassLoaderUrls() throws Exception { List archives = getClassPathArchives(); if (archives.isEmpty()) { + URL url = getArchive().getUrl(); + if (url.toString().contains(".jar")) { // Surefire or IntelliJ? + URL[] classpath = extractClasspath(url.toString()); + if (classpath != null) { + return classpath; + } + } return new URL[] { getArchive().getUrl() }; } return archives.stream().map(archive -> { @@ -234,6 +210,36 @@ public class FunctionConfiguration { }).collect(Collectors.toList()).toArray(new URL[0]); } + private URL[] extractClasspath(String url) { + // This works for a jar indirection like in surefire and IntelliJ + if (url.endsWith(".jar!/")) { + url = url.substring(0, url.length() - "!/".length()); + if (url.startsWith("jar:")) { + url = url.substring("jar:".length()); + } + if (url.startsWith("file:")) { + url = url.substring("file:".length()); + } + } + if (url.endsWith(".jar")) { + JarFile jar; + try { + jar = new JarFile(new File(url)); + 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; + } } /** @@ -269,7 +275,8 @@ public class FunctionConfiguration { runner.run("--spring.main.webEnvironment=false", "--spring.cloud.stream.enabled=false", "--spring.main.bannerMode=OFF", - "--spring.main.webApplicationType=none"); + "--spring.main.webApplicationType=none", + "--function.deployer.enabled=false"); this.runner = runner; } finally { diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java new file mode 100644 index 000000000..f4749a167 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java @@ -0,0 +1,65 @@ +/* + * 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.HashMap; +import java.util.Map; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.ResourceLoader; + +/** + * @author Dave Syer + * + */ +@Configuration +@ConditionalOnProperty(prefix = "function.deployer", name = "enabled", matchIfMissing = true) +@EnableConfigurationProperties +@Import(FunctionCreatorConfiguration.class) +public class FunctionDeployerConfiguration { + + @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 loaders = new HashMap<>(); + loaders.put(MavenResource.URI_SCHEME, new MavenResourceLoader(mavenProperties)); + return new DelegatingResourceLoader(loaders); + } + +} diff --git a/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories b/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 4486ed0a4..000000000 --- a/spring-cloud-function-deployer/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.cloud.function.deployer.FunctionConfiguration \ No newline at end of file diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java new file mode 100644 index 000000000..c7f0d5737 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/AdhocTestSuite.java @@ -0,0 +1,43 @@ +/* + * 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 org.junit.Ignore; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * A test suite for probing weird ordering problems in the tests. + * + * @author Dave Syer + */ +@RunWith(Suite.class) +@SuiteClasses({ FunctionCreatorConfigurationTests.FunctionCompositionTests.class, + FunctionCreatorConfigurationTests.SingleFunctionTests.class, + FunctionCreatorConfigurationTests.ManualSpringFunctionTests.class, + ContextRunnerTests.class, + SpringFunctionAppConfigurationTests.ProcessorTests.class, + SpringFunctionAppConfigurationTests.SourceTests.class, + FunctionCreatorConfigurationTests.ConsumerCompositionTests.class, + SpringFunctionAppConfigurationTests.CompositeTests.class, + ApplicationRunnerTests.class, SpringFunctionAppConfigurationTests.SinkTests.class, + FunctionCreatorConfigurationTests.SupplierCompositionTests.class }) +@Ignore +public class AdhocTestSuite { + +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ApplicationRunnerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ApplicationRunnerTests.java new file mode 100644 index 000000000..a28904004 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ApplicationRunnerTests.java @@ -0,0 +1,40 @@ +/* + * 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 org.junit.Test; + +import org.springframework.cloud.function.test.Doubler; +import org.springframework.cloud.function.test.FunctionApp; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + */ +public class ApplicationRunnerTests { + + @Test + public void startEvaluateAndStop() { + ApplicationRunner runner = new ApplicationRunner(getClass().getClassLoader(), + FunctionApp.class.getName()); + runner.run("--spring.main.webEnvironment=false"); + assertThat(runner.containsBean(Doubler.class.getName())).isTrue(); + assertThat(runner.getBean(Doubler.class.getName())).isNotNull(); + runner.close(); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ContextRunnerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ContextRunnerTests.java new file mode 100644 index 000000000..a3e3a7a59 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/ContextRunnerTests.java @@ -0,0 +1,40 @@ +/* + * 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.Collections; + +import org.junit.Test; + +import org.springframework.cloud.function.test.Doubler; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + */ +public class ContextRunnerTests { + + @Test + public void startEvaluateAndStop() { + ContextRunner runner = new ContextRunner(); + runner.run(Doubler.class.getName(), Collections.emptyMap(), + "--spring.main.webEnvironment=false"); + assertThat(runner.getContext()).isNotNull(); + runner.close(); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionConfigurationTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionConfigurationTests.java deleted file mode 100644 index 3809eeecd..000000000 --- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionConfigurationTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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()); - } - - } - -} \ No newline at end of file diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionCreatorConfigurationTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionCreatorConfigurationTests.java new file mode 100644 index 000000000..f9bebf7f6 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionCreatorConfigurationTests.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.deployer; + +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.Rule; +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.boot.test.rule.OutputCapture; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = { FunctionDeployerConfiguration.class }) +@DirtiesContext +public abstract class FunctionCreatorConfigurationTests { + + @Autowired + protected FunctionCatalog catalog; + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.Doubler" }) + public static class SingleFunctionTests extends FunctionCreatorConfigurationTests { + + @Test + public void testDouble() { + Function, Flux> function = catalog + .lookup(Function.class, "function0"); + assertThat(function.apply(Flux.just(2)).blockFirst()).isEqualTo(4); + } + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=app:classpath", + "function.bean=org.springframework.cloud.function.test.SpringDoubler" }) + public static class ManualSpringFunctionTests + extends FunctionCreatorConfigurationTests { + + @Test + public void testDouble() { + Function, Flux> function = catalog + .lookup(Function.class, "function0"); + assertThat(function.apply(Flux.just(2)).blockFirst()).isEqualTo(4); + } + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.NumberEmitter," + + "org.springframework.cloud.function.test.Frenchizer" }) + public static class SupplierCompositionTests + extends FunctionCreatorConfigurationTests { + + @Test + public void testSupplier() { + Supplier function = catalog.lookup(Supplier.class, "function0"); + assertThat(function).isNull(); + } + + @Test + public void testFunction() { + Supplier> function = catalog.lookup(Supplier.class, + "function0|function1"); + assertThat(function.get().blockFirst()).isEqualTo("un"); + } + + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.Doubler," + + "org.springframework.cloud.function.test.Frenchizer" }) + public static class FunctionCompositionTests + extends FunctionCreatorConfigurationTests { + + @Test + public void testFunction() { + Function, Flux> function = catalog + .lookup(Function.class, "function0|function1"); + assertThat(function.apply(Flux.just(2)).blockFirst()).isEqualTo("quatre"); + } + + @Test + public void testThen() { + Function function = catalog.lookup(Function.class, + "function1"); + assertThat(function).isNull(); + } + } + + @EnableAutoConfiguration + @TestPropertySource(properties = { "function.location=file:target/test-classes", + "function.bean=org.springframework.cloud.function.test.Frenchizer," + + "org.springframework.cloud.function.test.Printer" }) + public static class ConsumerCompositionTests + extends FunctionCreatorConfigurationTests { + + @Rule + public OutputCapture capture = new OutputCapture(); + + @Test + public void testConsumer() { + Function, Mono> function = catalog.lookup(Function.class, + "function0|function1"); + function.apply(Flux.just(2)).block(); + capture.expect(containsString("Seen deux")); + } + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java index 175fc6fb5..0716ee552 100644 --- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/SpringFunctionAppConfigurationTests.java @@ -16,50 +16,45 @@ package org.springframework.cloud.function.deployer; -import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; -import org.hamcrest.Matchers; +import org.junit.Rule; 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.boot.test.rule.OutputCapture; +import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.containsString; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; @RunWith(SpringRunner.class) -@SpringBootTest(classes = FunctionConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@SpringBootTest(classes = FunctionDeployerConfiguration.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; + protected FunctionCatalog catalog; @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")); - + Supplier> function = catalog.lookup(Supplier.class, "function0"); + assertThat(function.get().blockFirst()).isEqualTo("one"); } } @@ -69,16 +64,11 @@ public abstract class SpringFunctionAppConfigurationTests { "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)); - + Supplier> function = catalog.lookup(Supplier.class, + "function0|function1"); + assertThat(function.get().blockFirst()).isEqualTo(3); } } @@ -88,16 +78,11 @@ public abstract class SpringFunctionAppConfigurationTests { "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())); - + Function, Flux> function = catalog + .lookup(Function.class, "function0"); + assertThat(function.apply(Flux.just("spam")).blockFirst()).isEqualTo(4); } } @@ -107,13 +92,16 @@ public abstract class SpringFunctionAppConfigurationTests { "function.main=com.example.functions.FunctionApp" }) public static class SinkTests extends SpringFunctionAppConfigurationTests { - @Autowired - private Sink sink; + @Rule + public OutputCapture capture = new OutputCapture(); @Test public void test() throws Exception { // Can't assert side effects. - sink.input().send(MessageBuilder.withPayload(5).build()); + Function, Mono> function = catalog.lookup(Function.class, + "function0"); + function.apply(Flux.just(5)).block(); + capture.expect(containsString(String.format("10%n"))); } } diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Doubler.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Doubler.java new file mode 100644 index 000000000..bd5310951 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Doubler.java @@ -0,0 +1,26 @@ +/* + * 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.test; + +import java.util.function.Function; + +public class Doubler implements Function { + @Override + public Integer apply(Integer integer) { + return 2 * integer; + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Frenchizer.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Frenchizer.java new file mode 100644 index 000000000..7c9eb3fbd --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Frenchizer.java @@ -0,0 +1,43 @@ +/* + * 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.test; + +import java.util.function.Function; + +import javax.annotation.PostConstruct; + +public class Frenchizer implements Function { + + private String[] numbers; + + @PostConstruct + public void init() { + this.numbers = new String[4]; + numbers[0] = "un"; + numbers[1] = "deux"; + numbers[2] = "trois"; + numbers[3] = "quatre"; + } + + @Override + public String apply(Integer integer) { + if (integer < this.numbers.length + 1) { + return this.numbers[integer - 1]; + } + throw new RuntimeException(); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/FunctionApp.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/FunctionApp.java new file mode 100644 index 000000000..d0972702e --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/FunctionApp.java @@ -0,0 +1,42 @@ +/* + * 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.test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * @author Dave Syer + */ +@SpringBootConfiguration +public class FunctionApp { + + @Bean + public Doubler myDoubler() { + return new Doubler(); + } + + @Bean + public Frenchizer myFrenchizer() { + return new Frenchizer(); + } + + public static void main(String[] args) throws Exception { + SpringApplication.run(FunctionApp.class, args); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/NumberEmitter.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/NumberEmitter.java new file mode 100644 index 000000000..2bfde2188 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/NumberEmitter.java @@ -0,0 +1,26 @@ +/* + * 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.test; + +import java.util.function.Supplier; + +public class NumberEmitter implements Supplier { + @Override + public Integer get() { + return 1; + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Printer.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Printer.java new file mode 100644 index 000000000..99e64502a --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/Printer.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 org.springframework.cloud.function.test; + +import java.util.function.Consumer; + +public class Printer implements Consumer { + + @Override + public void accept(Object o) { + System.err.println("Seen " + o); + } +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/SpringDoubler.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/SpringDoubler.java new file mode 100644 index 000000000..20a779800 --- /dev/null +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/test/SpringDoubler.java @@ -0,0 +1,53 @@ +/* + * 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.test; + +import java.util.function.Function; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.Banner.Mode; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; + +public class SpringDoubler implements Function { + + @Autowired + private ConfigurableApplicationContext context; + + @PostConstruct + public void init() { + if (this.context == null) { + context = new SpringApplicationBuilder(FunctionApp.class).bannerMode(Mode.OFF).registerShutdownHook(false) + .web(false).run(); + } + } + + @PreDestroy + public void close() { + if (context != null) { + context.close(); + } + } + + @Override + public Integer apply(Integer integer) { + return 2 * integer; + } +} diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java index 7a90fb165..525c17ac7 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/request/FluxHandlerMethodArgumentResolver.java @@ -99,10 +99,10 @@ public class FluxHandlerMethodArgumentResolver body = null; } else { - try { + if (json.startsWith("[")) { body = mapper.toList(json, type); } - catch (IllegalArgumentException e) { + else { nativeRequest.setAttribute(WebRequestConstants.INPUT_SINGLE, true); body = Arrays.asList(mapper.toSingle(json, type)); }