diff --git a/README.adoc b/README.adoc index dc40153a3..18686ccd1 100644 --- a/README.adoc +++ b/README.adoc @@ -156,7 +156,7 @@ curl -H "Accept: application/json" localhost:9001/words === Register a Consumer: ---- -./registerConsumer.sh -n print -f "System.out::println" +./registerConsumer.sh -n print -f "f->f.subscribe(System.out::println)" ---- === Run a REST Microservice using that Consumer: diff --git a/scripts/registerConsumer.sh b/scripts/registerConsumer.sh index 7836c40de..0cfbf985b 100755 --- a/scripts/registerConsumer.sh +++ b/scripts/registerConsumer.sh @@ -1,6 +1,6 @@ #!/bin/bash -while getopts ":n:f:" opt; do +while getopts ":n:f:t:" opt; do case $opt in n) NAME=$OPTARG @@ -8,7 +8,10 @@ while getopts ":n:f:" opt; do f) FUNC=$OPTARG ;; + t) + TYPE=$OPTARG + ;; esac done -curl -X POST -H "Content-Type: text/plain" -d $FUNC localhost:8080/consumer/$NAME +curl -X POST -H "Content-Type: text/plain" -d $FUNC localhost:8080/consumer/$NAME?type=$TYPE diff --git a/spring-cloud-function-compiler/pom.xml b/spring-cloud-function-compiler/pom.xml index e92882002..09ac1974d 100644 --- a/spring-cloud-function-compiler/pom.xml +++ b/spring-cloud-function-compiler/pom.xml @@ -44,6 +44,12 @@ org.springframework.boot spring-boot-starter-web + true + + + org.springframework.boot + spring-boot-starter-test + test diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompilationResultFactory.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompilationResultFactory.java index 3f17f669a..5a4a6a46d 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompilationResultFactory.java +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompilationResultFactory.java @@ -23,7 +23,4 @@ public interface CompilationResultFactory { T getResult(); - String getInputType(); - - String getOutputType(); } diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompiledFunctionFactory.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompiledFunctionFactory.java index 109d15e58..c7a5797bc 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompiledFunctionFactory.java +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompiledFunctionFactory.java @@ -16,9 +16,12 @@ package org.springframework.cloud.function.compiler; +import java.lang.reflect.Method; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import org.springframework.cloud.function.compiler.java.CompilationResult; +import org.springframework.util.ReflectionUtils; /** * @author Mark Fisher @@ -33,18 +36,26 @@ public class CompiledFunctionFactory implements CompilationResultFactory { private String outputType; - public CompiledFunctionFactory(String className, CompilationResult compilationResult) { + private Method method; + + public CompiledFunctionFactory(String className, + CompilationResult compilationResult) { List> clazzes = compilationResult.getCompiledClasses(); T result = null; - for (Class clazz: clazzes) { + Method method = null; + for (Class clazz : clazzes) { if (clazz.getName().equals(className)) { try { @SuppressWarnings("unchecked") - CompilationResultFactory factory = (CompilationResultFactory) clazz.newInstance(); + CompilationResultFactory factory = (CompilationResultFactory) clazz + .newInstance(); result = factory.getResult(); + method = findFactoryMethod(clazz); } catch (Exception e) { - throw new IllegalArgumentException("Unexpected problem during retrieval of Function from compiled class", e); + throw new IllegalArgumentException( + "Unexpected problem during retrieval of Function from compiled class", + e); } } } @@ -52,13 +63,29 @@ public class CompiledFunctionFactory implements CompilationResultFactory { throw new IllegalArgumentException("Failed to extract compilation result."); } this.result = result; + this.method = method; this.generatedClassBytes = compilationResult.getClassBytes(className); } + private Method findFactoryMethod(Class clazz) { + AtomicReference method = new AtomicReference<>(); + ReflectionUtils.doWithLocalMethods(clazz, m -> { + if (m.getName().equals("getResult") + && m.getReturnType().getName().startsWith("java.util.function")) { + method.set(m); + } + }); + return method.get(); + } + public T getResult() { return result; } + public Method getFactoryMethod() { + return method; + } + public String getInputType() { return inputType; } diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java index 8385ef745..8431efd97 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java @@ -45,6 +45,7 @@ public class FunctionCompiler extends AbstractFunctionCompiler> postProcessCompiledFunctionFactory(CompiledFunctionFactory> factory) { factory.setInputType(this.inputType); factory.setOutputType(this.outputType); + return factory; } } diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompiledFunctionRegistry.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompiledFunctionRegistry.java index 7177e7675..8bee9a04e 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompiledFunctionRegistry.java +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompiledFunctionRegistry.java @@ -94,8 +94,8 @@ public class CompiledFunctionRegistry { } } - public void registerConsumer(String name, String consumer) { - CompiledFunctionFactory factory = this.consumerCompiler.compile(name, consumer); + public void registerConsumer(String name, String consumer, String type) { + CompiledFunctionFactory factory = this.consumerCompiler.compile(name, consumer, type); File file = new File(this.consumerDirectory, fileName(name)); try { FileCopyUtils.copy(factory.getGeneratedClassBytes(), file); diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerController.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerController.java index 49134959a..14f6c3b02 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerController.java +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerController.java @@ -44,7 +44,8 @@ public class CompilerController { } @PostMapping(path = "/consumer/{name}") - public void registerConsumer(@PathVariable String name, @RequestBody String lambda) { - this.registry.registerConsumer(name, lambda); + public void registerConsumer(@PathVariable String name, @RequestBody String lambda, + @RequestParam(defaultValue="Flux") String type) { + this.registry.registerConsumer(name, lambda, type); } } diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/config/FunctionProxyApplicationListener.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/config/FunctionProxyApplicationListener.java index ee7478d1f..b748806b5 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/config/FunctionProxyApplicationListener.java +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/config/FunctionProxyApplicationListener.java @@ -115,6 +115,9 @@ public class FunctionProxyApplicationListener implements ApplicationListener implements InitializingBea private final SimpleClassLoader classLoader = new SimpleClassLoader(AbstractByteCodeLoadingProxy.class.getClassLoader()); + private Method method; + public AbstractByteCodeLoadingProxy(Resource resource, Class type) { this.resource = resource; this.type = type; @@ -45,28 +51,37 @@ public abstract class AbstractByteCodeLoadingProxy implements InitializingBea @SuppressWarnings("unchecked") public void afterPropertiesSet() throws Exception { byte[] bytes = FileCopyUtils.copyToByteArray(this.resource.getInputStream()); - String functionName = this.resource.getFilename().replaceAll(".fun$", ""); + String filename = this.resource.getFilename(); + String functionName = filename == null ? type.getSimpleName() : filename.replaceAll(".fun$", ""); String firstLetter = functionName.substring(0, 1).toUpperCase(); String upperCasedName = (functionName.length() > 1) ? firstLetter + functionName.substring(1) : firstLetter; String className = String.format("%s.%s%sFactory", FunctionCompiler.class.getPackage().getName(), upperCasedName, this.type.getSimpleName()); Class factoryClass = this.classLoader.defineClass(className, bytes); try { this.factory = (CompilationResultFactory) factoryClass.newInstance(); + this.method = findFactoryMethod(factoryClass); } catch (InstantiationException | IllegalAccessException e) { throw new IllegalArgumentException("failed to load Function byte code", e); } } + private Method findFactoryMethod(Class clazz) { + AtomicReference method = new AtomicReference<>(); + ReflectionUtils.doWithLocalMethods(clazz, m -> { + if (m.getName().equals("getResult") + && m.getReturnType().getName().startsWith("java.util.function")) { + method.set(m); + } + }); + return method.get(); + } + public final T getTarget() { return this.factory.getResult(); } - public String getInputType() { - return this.factory.getInputType(); - } - - public String getOutputType() { - return this.factory.getOutputType(); + public Method getFactoryMethod() { + return this.method; } } diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractLambdaCompilingProxy.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractLambdaCompilingProxy.java index af6b1b5cc..469780dd3 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractLambdaCompilingProxy.java +++ b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractLambdaCompilingProxy.java @@ -17,11 +17,13 @@ package org.springframework.cloud.function.compiler.proxy; import java.io.InputStreamReader; +import java.lang.reflect.Method; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.cloud.function.compiler.AbstractFunctionCompiler; import org.springframework.cloud.function.compiler.CompiledFunctionFactory; +import org.springframework.cloud.function.support.FunctionFactoryMetadata; import org.springframework.core.io.Resource; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; @@ -29,7 +31,7 @@ import org.springframework.util.FileCopyUtils; /** * @author Mark Fisher */ -public class AbstractLambdaCompilingProxy implements InitializingBean, BeanNameAware { +public class AbstractLambdaCompilingProxy implements InitializingBean, BeanNameAware, FunctionFactoryMetadata { private final Resource resource; @@ -67,11 +69,7 @@ public class AbstractLambdaCompilingProxy implements InitializingBean, BeanNa return this.factory.getResult(); } - public final String getInputType() { - return this.factory.getInputType(); - } - - public final String getOutputType() { - return this.factory.getOutputType(); + public Method getFactoryMethod() { + return this.factory.getFactoryMethod(); } } diff --git a/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/ConsumerCompilerTests.java b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/ConsumerCompilerTests.java new file mode 100644 index 000000000..d52e72d6d --- /dev/null +++ b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/ConsumerCompilerTests.java @@ -0,0 +1,48 @@ +/* + * 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.compiler; + +import java.util.function.Consumer; + +import org.junit.Test; + +import org.springframework.cloud.function.support.FunctionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +public class ConsumerCompilerTests { + + @Test + public void consumesFluxString() { + CompiledFunctionFactory> compiled = new ConsumerCompiler( + String.class.getName()).compile("foos", + "flux -> flux.subscribe(System.out::println)", "Flux"); + assertThat(FunctionUtils.isFluxConsumer(compiled.getFactoryMethod())).isTrue(); + } + + @Test + public void consumesString() { + CompiledFunctionFactory> compiled = new ConsumerCompiler( + String.class.getName()).compile("foos", "System.out::println", "String"); + assertThat(FunctionUtils.isFluxConsumer(compiled.getFactoryMethod())).isFalse(); + } + +} diff --git a/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/FunctionCompilerTests.java b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/FunctionCompilerTests.java new file mode 100644 index 000000000..aa3e523cb --- /dev/null +++ b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/FunctionCompilerTests.java @@ -0,0 +1,49 @@ +/* + * 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.compiler; + +import java.util.function.Function; + +import org.junit.Test; + +import org.springframework.cloud.function.support.FunctionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +public class FunctionCompilerTests { + + @Test + public void transformsFluxString() { + CompiledFunctionFactory> compiled = new FunctionCompiler( + String.class.getName()).compile("foos", + "flux -> flux.map(v -> v.toUpperCase())", "Flux", "Flux"); + assertThat(FunctionUtils.isFluxFunction(compiled.getFactoryMethod())).isTrue(); + } + + @Test + public void transformsString() { + CompiledFunctionFactory> compiled = new FunctionCompiler( + String.class.getName()).compile("foos", "v -> v.toUpperCase()", "String", "String"); + assertThat(FunctionUtils.isFluxFunction(compiled.getFactoryMethod())).isFalse(); + assertThat(compiled.getResult().apply("hello")).isEqualTo("HELLO"); + } + +} diff --git a/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/SupplierCompilerTests.java b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/SupplierCompilerTests.java new file mode 100644 index 000000000..0fc939082 --- /dev/null +++ b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/SupplierCompilerTests.java @@ -0,0 +1,50 @@ +/* + * 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.compiler; + +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.cloud.function.support.FunctionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Dave Syer + * + */ +public class SupplierCompilerTests { + + @Test + public void supppliesFluxString() { + CompiledFunctionFactory> compiled = new SupplierCompiler( + String.class.getName()).compile("foos", + "() -> Flux.just(\"foo\", \"bar\")", "Flux"); + assertThat(FunctionUtils.isFluxSupplier(compiled.getFactoryMethod())).isTrue(); + } + + @Test + public void supppliesString() { + CompiledFunctionFactory> compiled = new SupplierCompiler( + String.class.getName()).compile("foos", + "() -> \"foo\"", "String"); + assertThat(FunctionUtils.isFluxSupplier(compiled.getFactoryMethod())).isFalse(); + assertThat(compiled.getResult().get()).isEqualTo("foo"); + } + +} diff --git a/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunctionTests.java b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunctionTests.java new file mode 100644 index 000000000..de01c42da --- /dev/null +++ b/spring-cloud-function-compiler/test/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunctionTests.java @@ -0,0 +1,106 @@ +/* + * 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.compiler.proxy; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.cloud.function.compiler.CompiledFunctionFactory; +import org.springframework.cloud.function.compiler.ConsumerCompiler; +import org.springframework.cloud.function.compiler.FunctionCompiler; +import org.springframework.cloud.function.compiler.SupplierCompiler; +import org.springframework.cloud.function.support.FunctionUtils; +import org.springframework.core.io.ByteArrayResource; + +import static org.assertj.core.api.Assertions.assertThat; + +import reactor.core.publisher.Flux; + +/** + * @author Dave Syer + * + */ +public class ByteCodeLoadingFunctionTests { + + @Test + public void compileConsumer() throws Exception { + CompiledFunctionFactory> compiled = new ConsumerCompiler( + String.class.getName()).compile("foos", "System.out::println", "String"); + ByteArrayResource resource = new ByteArrayResource(compiled.getGeneratedClassBytes(), "foos") { + @Override + public String getFilename() { + return "foos.fun"; + } + }; + ByteCodeLoadingConsumer consumer = new ByteCodeLoadingConsumer<>(resource); + consumer.afterPropertiesSet(); + assertThat(FunctionUtils.isFluxConsumer(consumer.getFactoryMethod())).isFalse(); + consumer.accept("foo"); + } + + @Test + public void compileSupplier() throws Exception { + CompiledFunctionFactory> compiled = new SupplierCompiler( + String.class.getName()).compile("foos", "() -> \"foo\"", "String"); + ByteArrayResource resource = new ByteArrayResource(compiled.getGeneratedClassBytes(), "foos") { + @Override + public String getFilename() { + return "foos.fun"; + } + }; + ByteCodeLoadingSupplier supplier = new ByteCodeLoadingSupplier<>(resource); + supplier.afterPropertiesSet(); + assertThat(FunctionUtils.isFluxSupplier(supplier.getFactoryMethod())).isFalse(); + assertThat(supplier.get()).isEqualTo("foo"); + } + + @Test + public void compileFunction() throws Exception { + CompiledFunctionFactory> compiled = new FunctionCompiler( + String.class.getName()).compile("foos", "v -> v.toUpperCase()", "String", "String"); + ByteArrayResource resource = new ByteArrayResource(compiled.getGeneratedClassBytes(), "foos") { + @Override + public String getFilename() { + return "foos.fun"; + } + }; + ByteCodeLoadingFunction function = new ByteCodeLoadingFunction<>(resource); + function.afterPropertiesSet(); + assertThat(FunctionUtils.isFluxFunction(function.getFactoryMethod())).isFalse(); + assertThat(function.apply("foo")).isEqualTo("FOO"); + } + + @Test + public void compileFluxFunction() throws Exception { + CompiledFunctionFactory, Flux>> compiled = new FunctionCompiler, Flux>( + String.class.getName()).compile("foos", "flux -> flux.map(v -> v.toUpperCase())", "Flux", "Flux"); + ByteArrayResource resource = new ByteArrayResource(compiled.getGeneratedClassBytes(), "foos") { + @Override + public String getFilename() { + return "foos.fun"; + } + }; + ByteCodeLoadingFunction, Flux> function = new ByteCodeLoadingFunction<>(resource); + function.afterPropertiesSet(); + assertThat(FunctionUtils.isFluxFunction(function.getFactoryMethod())).isTrue(); + assertThat(function.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO"); + } + +} diff --git a/spring-cloud-function-context/pom.xml b/spring-cloud-function-context/pom.xml index 8722712d9..fce40e0f3 100644 --- a/spring-cloud-function-context/pom.xml +++ b/spring-cloud-function-context/pom.xml @@ -42,5 +42,11 @@ spring-boot-starter-test test + + org.springframework.cloud + spring-cloud-function-compiler + ${spring-cloud-function.version} + test + diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java index 3ad423ef5..3caf1cc9c 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java @@ -35,7 +35,7 @@ import java.util.function.Function; import java.util.function.Supplier; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanDefinition; @@ -51,6 +51,7 @@ import org.springframework.cloud.function.registry.FunctionCatalog; import org.springframework.cloud.function.support.FluxConsumer; import org.springframework.cloud.function.support.FluxFunction; import org.springframework.cloud.function.support.FluxSupplier; +import org.springframework.cloud.function.support.FunctionFactoryMetadata; import org.springframework.cloud.function.support.FunctionUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -391,7 +392,7 @@ public class ContextFunctionCatalogAutoConfiguration { private Class findType(String name, AbstractBeanDefinition definition, ParamType paramType) { Object source = definition.getSource(); - Type param; + Type param = null; // Start by assuming output -> Function int index = paramType.isOutput() ? 1 : 0; if (source instanceof StandardMethodMetadata) { @@ -422,18 +423,26 @@ public class ContextFunctionCatalogAutoConfiguration { if (resolvable != null) { param = resolvable.getGeneric(index).getGeneric(0).getType(); } - else { - // TODO: compiled functions (only work as String -> String) - if (paramType.isWrapper() && !Consumer.class.isAssignableFrom(definition.getBeanClass())) { - return Flux.class; + else if (registry instanceof BeanFactory) { + Object bean = ((BeanFactory) registry).getBean(name); + if (bean instanceof FunctionFactoryMetadata) { + FunctionFactoryMetadata factory = (FunctionFactoryMetadata) bean; + Type type = factory.getFactoryMethod().getGenericReturnType(); + param = extractType(type, paramType, index); } - return String.class; } } if (param instanceof ParameterizedType) { ParameterizedType concrete = (ParameterizedType) param; param = concrete.getRawType(); } + if (param == null) { + // Last ditch attempt to guess: Flux + if (paramType.isWrapper()) { + return Flux.class; + } + return String.class; + } return (Class) param; } diff --git a/spring-cloud-function-context/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java similarity index 84% rename from spring-cloud-function-context/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java rename to spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java index ac9ff168b..f796d6844 100644 --- a/spring-cloud-function-context/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java @@ -30,12 +30,16 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.compiler.CompiledFunctionFactory; +import org.springframework.cloud.function.compiler.FunctionCompiler; import org.springframework.cloud.function.test.GenericFunction; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.core.io.FileSystemResource; +import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -143,12 +147,35 @@ public class ContextFunctionCatalogAutoConfigurationTests { assertThat(catalog.lookupFunction("other")).isInstanceOf(Function.class); } - private void create(Class... types) { - context = new SpringApplicationBuilder((Object[]) types).run(); + @Test + public void compiledFunction() throws Exception { + CompiledFunctionFactory> compiled = new FunctionCompiler( + String.class.getName()).compile("foos", "v -> v.toUpperCase()", "String", + "String"); + FileSystemResource resource = new FileSystemResource("target/foos.fun"); + StreamUtils.copy(compiled.getGeneratedClassBytes(), resource.getOutputStream()); + create(EmptyConfiguration.class, + "spring.cloud.function.import.foos.location=file:./target/foos.fun"); + assertThat(context.getBean("foos")).isInstanceOf(Function.class); + assertThat(catalog.lookupFunction("foos")).isInstanceOf(Function.class); + assertThat(inspector.getInputWrapper("foos")).isEqualTo(String.class); + } + + private void create(Class type, String... props) { + create(new Class[] { type }, props); + } + + private void create(Class[] types, String... props) { + context = new SpringApplicationBuilder((Object[]) types).properties(props).run(); catalog = context.getBean(InMemoryFunctionCatalog.class); inspector = context.getBean(FunctionInspector.class); } + @EnableAutoConfiguration + @Configuration + protected static class EmptyConfiguration { + } + @EnableAutoConfiguration @Configuration protected static class SimpleConfiguration { @@ -188,7 +215,7 @@ public class ContextFunctionCatalogAutoConfigurationTests { @EnableAutoConfiguration @Configuration - @ComponentScan(basePackageClasses=GenericFunction.class) + @ComponentScan(basePackageClasses = GenericFunction.class) protected static class ComponentScanConfiguration { } @@ -237,4 +264,3 @@ public class ContextFunctionCatalogAutoConfigurationTests { } } - diff --git a/spring-cloud-function-context/test/java/org/springframework/cloud/function/test/GenericFunction.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/test/GenericFunction.java similarity index 100% rename from spring-cloud-function-context/test/java/org/springframework/cloud/function/test/GenericFunction.java rename to spring-cloud-function-context/src/test/java/org/springframework/cloud/function/test/GenericFunction.java diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/ConsumerProxy.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/ConsumerProxy.java index 2ffbaaa58..2c10bc5f8 100644 --- a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/ConsumerProxy.java +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/ConsumerProxy.java @@ -23,13 +23,12 @@ import java.util.function.Consumer; * * @param output type of target Consumer */ -public interface ConsumerProxy extends Consumer { +public interface ConsumerProxy extends Consumer, FunctionFactoryMetadata { default boolean isFluxConsumer() { - return FunctionUtils.isFluxConsumer(getTarget()); + return FunctionUtils.isFluxConsumer(getFactoryMethod()); } Consumer getTarget(); - String getInputType(); } diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionFactoryMetadata.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionFactoryMetadata.java new file mode 100644 index 000000000..27b791e3b --- /dev/null +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionFactoryMetadata.java @@ -0,0 +1,30 @@ +/* + * 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.support; + +import java.lang.reflect.Method; + +/** + * @author Dave Syer + * + * @param + */ +public interface FunctionFactoryMetadata { + + Method getFactoryMethod(); + +} \ No newline at end of file diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionProxy.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionProxy.java index e84529699..cf687e067 100644 --- a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionProxy.java +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionProxy.java @@ -24,15 +24,12 @@ import java.util.function.Function; * @param input type of target Function * @param output type of target Function */ -public interface FunctionProxy extends Function { +public interface FunctionProxy extends Function, FunctionFactoryMetadata { default boolean isFluxFunction() { - return FunctionUtils.isFluxFunction(getTarget()); + return FunctionUtils.isFluxFunction(getFactoryMethod()); } Function getTarget(); - String getInputType(); - - String getOutputType(); } diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionUtils.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionUtils.java index 98e7e1d30..c74788fe2 100644 --- a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionUtils.java +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/FunctionUtils.java @@ -118,4 +118,51 @@ public abstract class FunctionUtils { } return typeNames.toArray(new String[typeNames.size()]); } + + public static boolean isFluxSupplier(Method method) { + String[] types = getParameterizedTypeNamesForMethod(method, Supplier.class); + if (ObjectUtils.isEmpty(types)) { + return false; + } + return (types[0].startsWith(FLUX_CLASS_NAME)); + } + + public static boolean isFluxConsumer(Method method) { + String[] types = getParameterizedTypeNamesForMethod(method, Consumer.class); + if (ObjectUtils.isEmpty(types)) { + return false; + } + return (types[0].startsWith(FLUX_CLASS_NAME)); + } + + public static boolean isFluxFunction(Method method) { + String[] types = getParameterizedTypeNamesForMethod(method, Function.class); + if (ObjectUtils.isEmpty(types)) { + return false; + } + if (ObjectUtils.isEmpty(types) || types.length != 2) { + return false; + } + return (types[0].startsWith(FLUX_CLASS_NAME) + && types[1].startsWith(FLUX_CLASS_NAME)); + } + + private static String[] getParameterizedTypeNamesForMethod(Method method, + Class interfaceClass) { + Type genericInterface = method.getGenericReturnType(); + if ((genericInterface instanceof ParameterizedType) && interfaceClass + .getTypeName().equals(((ParameterizedType) genericInterface) + .getRawType().getTypeName())) { + ParameterizedType type = (ParameterizedType) genericInterface; + Type[] args = type.getActualTypeArguments(); + if (args != null) { + String[] typeNames = new String[args.length]; + for (int i = 0; i < args.length; i++) { + typeNames[i] = args[i].getTypeName(); + } + return typeNames; + } + } + return new String[0]; + } } diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/SupplierProxy.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/SupplierProxy.java index 27b931c85..94a2b1029 100644 --- a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/SupplierProxy.java +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/support/SupplierProxy.java @@ -23,13 +23,11 @@ import java.util.function.Supplier; * * @param output type of target Supplier */ -public interface SupplierProxy extends Supplier { +public interface SupplierProxy extends Supplier, FunctionFactoryMetadata { default boolean isFluxSupplier() { - return FunctionUtils.isFluxSupplier(getTarget()); + return FunctionUtils.isFluxSupplier(getFactoryMethod()); } Supplier getTarget(); - - String getOutputType(); } diff --git a/spring-cloud-function-core/src/test/java/org/springframework/cloud/function/gateway/FunctionUtilsTests.java b/spring-cloud-function-core/src/test/java/org/springframework/cloud/function/gateway/FunctionUtilsTests.java new file mode 100644 index 000000000..b233fbdbc --- /dev/null +++ b/spring-cloud-function-core/src/test/java/org/springframework/cloud/function/gateway/FunctionUtilsTests.java @@ -0,0 +1,77 @@ +/* + * 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.gateway; + +import java.lang.reflect.Method; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.Test; + +import org.springframework.cloud.function.support.FunctionUtils; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +import reactor.core.publisher.Flux; + +/** + * @author Dave Syer + * + */ +public class FunctionUtilsTests { + + @Test + public void isFluxConsumer() { + Method method = ReflectionUtils.findMethod(FunctionUtilsTests.class, "fluxConsumer"); + assertThat(FunctionUtils.isFluxConsumer(method)).isTrue(); + assertThat(FunctionUtils.isFluxSupplier(method)).isFalse(); + assertThat(FunctionUtils.isFluxFunction(method)).isFalse(); + } + + @Test + public void isFluxSupplier() { + Method method = ReflectionUtils.findMethod(FunctionUtilsTests.class, "fluxSupplier"); + assertThat(FunctionUtils.isFluxSupplier(method)).isTrue(); + assertThat(FunctionUtils.isFluxConsumer(method)).isFalse(); + assertThat(FunctionUtils.isFluxFunction(method)).isFalse(); + } + + @Test + public void isFluxFunction() { + Method method = ReflectionUtils.findMethod(FunctionUtilsTests.class, "fluxFunction"); + assertThat(FunctionUtils.isFluxFunction(method)).isTrue(); + assertThat(FunctionUtils.isFluxSupplier(method)).isFalse(); + assertThat(FunctionUtils.isFluxConsumer(method)).isFalse(); + } + + public Function, Flux> fluxFunction() { + return foos -> foos.map(foo -> new Foo()); + } + + public Supplier> fluxSupplier() { + return () -> Flux.just(new Foo()); + } + + public Consumer> fluxConsumer() { + return flux -> flux.subscribe(System.out::println); + } + + class Foo {} + +} diff --git a/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledConsumerTests.java b/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledConsumerTests.java index d5995ccc3..8fa4d765a 100644 --- a/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledConsumerTests.java +++ b/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledConsumerTests.java @@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.cloud.function.compile.test.lambda=com.example.SampleCompiledConsumerTests.Reference::set", + "spring.cloud.function.compile.test.inputType=String", "spring.cloud.function.compile.test.type=consumer" }) public class SampleCompiledConsumerTests { diff --git a/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledFunctionTests.java b/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledFunctionTests.java index ba44a65f1..29435826c 100644 --- a/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledFunctionTests.java +++ b/spring-cloud-function-samples/spring-cloud-function-sample-compiler/src/test/java/com/example/SampleCompiledFunctionTests.java @@ -31,8 +31,10 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Mark Fisher */ @RunWith(SpringRunner.class) -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, - properties = "spring.cloud.function.compile.test.lambda=f->f.map(s->s+\"!!!\")") +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { + "spring.cloud.function.compile.test.lambda=f->f.map(s->s+\"!!!\")", + "spring.cloud.function.compile.test.inputType=Flux", + "spring.cloud.function.compile.test.outputType=Flux" }) public class SampleCompiledFunctionTests { @LocalServerPort @@ -41,8 +43,8 @@ public class SampleCompiledFunctionTests { @Test public void lowercase() { assertThat(new TestRestTemplate().postForObject( - "http://localhost:" + port + "/test", "it works", - String.class)).isEqualTo("it works!!!"); + "http://localhost:" + port + "/test", "it works", String.class)) + .isEqualTo("it works!!!"); } }