Push deployer configuration out of autoconfig

It tends to pop back into function apps where it is not needed
otherwise. Users that want to use the library need to import
the FunctionConfiguration directly using the
@EnableFunctionDeployer convenience annotation..
This commit is contained in:
Dave Syer
2018-04-25 17:33:24 +01:00
parent 7fa0ed7b6b
commit ebd1646308
22 changed files with 656 additions and 328 deletions

View File

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

View File

@@ -24,11 +24,6 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-stream</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
@@ -54,9 +49,8 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<optional>true</optional>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<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",
@@ -222,6 +191,13 @@ public class FunctionConfiguration {
public URL[] getClassLoaderUrls() throws Exception {
List<Archive> 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<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;
}
}
/**
@@ -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 {

View File

@@ -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<String, ResourceLoader> loaders = new HashMap<>();
loaders.put(MavenResource.URI_SCHEME, new MavenResourceLoader(mavenProperties));
return new DelegatingResourceLoader(loaders);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<Integer>, Flux<Integer>> 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<Integer>, Flux<Integer>> 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<Integer> function = catalog.lookup(Supplier.class, "function0");
assertThat(function).isNull();
}
@Test
public void testFunction() {
Supplier<Flux<String>> 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<Integer>, Flux<String>> function = catalog
.lookup(Function.class, "function0|function1");
assertThat(function.apply(Flux.just(2)).blockFirst()).isEqualTo("quatre");
}
@Test
public void testThen() {
Function<Integer, String> 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<Flux<Integer>, Mono<Void>> function = catalog.lookup(Function.class,
"function0|function1");
function.apply(Flux.just(2)).block();
capture.expect(containsString("Seen deux"));
}
}
}

View File

@@ -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<Flux<String>> 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<Flux<Integer>> 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<String>, Flux<Integer>> 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<Flux<Integer>, Mono<Void>> function = catalog.lookup(Function.class,
"function0");
function.apply(Flux.just(5)).block();
capture.expect(containsString(String.format("10%n")));
}
}

View File

@@ -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<Integer, Integer> {
@Override
public Integer apply(Integer integer) {
return 2 * integer;
}
}

View File

@@ -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<Integer, String> {
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();
}
}

View File

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

View File

@@ -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<Integer> {
@Override
public Integer get() {
return 1;
}
}

View File

@@ -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<Object> {
@Override
public void accept(Object o) {
System.err.println("Seen " + o);
}
}

View File

@@ -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<Integer, Integer> {
@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;
}
}