add support for simple (non-Flux) types

add objectToStringHttpMessageConverter

CompilerController accepts parameterized types
This commit is contained in:
markfisher
2017-02-20 10:31:22 -05:00
parent 22cab6a3f1
commit 19fd056a5e
30 changed files with 717 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
#!/bin/bash
while getopts ":n:f:" opt; do
while getopts ":n:f:i:o:" opt; do
case $opt in
n)
NAME=$OPTARG
@@ -8,7 +8,14 @@ while getopts ":n:f:" opt; do
f)
FUNC=$OPTARG
;;
i)
INTYPE=$OPTARG
;;
o)
OUTTYPE=$OPTARG
;;
esac
done
curl -X POST -H "Content-Type: text/plain" -d $FUNC :8080/function/$NAME
curl -X POST -H "Content-Type: text/plain" -d $FUNC ":8080/function/$NAME?inputType=$INTYPE&outputType=$OUTTYPE"

View File

@@ -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 :8080/supplier/$NAME
curl -X POST -H "Content-Type: text/plain" -d $FUNC :8080/supplier/$NAME?type=$TYPE

View File

@@ -41,6 +41,11 @@
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* 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.
@@ -26,6 +26,8 @@ import org.springframework.cloud.function.compiler.java.CompilationFailedExcepti
import org.springframework.cloud.function.compiler.java.CompilationMessage;
import org.springframework.cloud.function.compiler.java.CompilationResult;
import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* @author Andy Clement
@@ -59,13 +61,13 @@ public abstract class AbstractFunctionCompiler<F> {
private final ResultType resultType;
private final String parameterizedTypes;
private final String[] defaultResultTypeParameterizations;
private final RuntimeJavaCompiler compiler = new RuntimeJavaCompiler();
AbstractFunctionCompiler(ResultType type, String parameterizedTypes) {
AbstractFunctionCompiler(ResultType type, String... defaultResultTypeParameterizations) {
this.resultType = type;
this.parameterizedTypes = parameterizedTypes;
this.defaultResultTypeParameterizations = defaultResultTypeParameterizations;
}
/**
@@ -80,23 +82,25 @@ public abstract class AbstractFunctionCompiler<F> {
*
* @return a factory instance
*/
public CompiledFunctionFactory<F> compile(String name, String code) {
public CompiledFunctionFactory<F> compile(String name, String code, String... resultTypeParameterizations) {
if (name == null || name.length() == 0) {
throw new IllegalArgumentException("name must not be empty");
}
logger.info("Initial code property value :'{}'", code);
String parameterizations = StringUtils.arrayToCommaDelimitedString(
(!ObjectUtils.isEmpty(resultTypeParameterizations)) ? resultTypeParameterizations : this.defaultResultTypeParameterizations);
code = decode(code);
if (code.startsWith("\"") && code.endsWith("\"")) {
code = code.substring(1,code.length()-1);
}
if (!code.startsWith("return ") && !code.endsWith(";")) {
code = String.format("return (%s<%s> & java.io.Serializable) %s;", resultType, this.parameterizedTypes, code);
code = String.format("return (%s<%s> & java.io.Serializable) %s;", resultType, parameterizations, code);
}
logger.info("Processed code property value :\n{}\n", code);
String firstLetter = name.substring(0, 1).toUpperCase();
name = (name.length() > 1) ? firstLetter + name.substring(1) : firstLetter;
String className = String.format("%s.%s%sFactory", this.getClass().getPackage().getName(), name, resultType);
CompilationResult compilationResult = buildAndCompileSourceCode(className, code);
CompilationResult compilationResult = buildAndCompileSourceCode(className, code, parameterizations);
if (compilationResult.wasSuccessful()) {
return new CompiledFunctionFactory(className, compilationResult);
}
@@ -107,17 +111,18 @@ public abstract class AbstractFunctionCompiler<F> {
/**
* Create the source for and then compile and load a class that embodies
* the supplied methodBody. The methodBody is inserted into a class template that
* returns a <tt>Function&lt;Flux&lt;Object&gt;,Flux&lt;Object&gt;&gt;</tt>.
* returns the specified parameterized type.
* This method can return more than one class if the method body includes local class
* declarations. An example methodBody would be <tt>return input -> input.buffer(5).map(list->list.get(0));</tt>.
*
* @param className the name of the class
* @param methodBody the source code for a method that should return a
* <tt>Function&lt;Flux&lt;Object&gt;,Flux&lt;Object&gt;&gt;</tt>
* @param methodBody the source code for a method
* @param parameterTypeString the String representation for the parameterized return type, e.g.:
* <tt>&lt;Flux&lt;Object&gt;,Flux&lt;Object&gt;</tt>
* @return the list of Classes produced by compiling and then loading the snippet of code
*/
private CompilationResult buildAndCompileSourceCode(String className, String methodBody) {
String sourceCode = makeSourceClassDefinition(className, methodBody);
private CompilationResult buildAndCompileSourceCode(String className, String methodBody, String parameterTypeString) {
String sourceCode = makeSourceClassDefinition(className, methodBody, parameterTypeString);
return compiler.compile(className, sourceCode);
}
@@ -133,9 +138,9 @@ public abstract class AbstractFunctionCompiler<F> {
* @param methodBody the code to insert into the Reactive source class template
* @return a complete Java Class definition
*/
private String makeSourceClassDefinition(String className, String methodBody) {
private String makeSourceClassDefinition(String className, String methodBody, String types) {
String shortClassName = className.substring(className.lastIndexOf('.') + 1);
String s = String.format(SOURCE_CODE_TEMPLATE, shortClassName, resultType, resultType, this.parameterizedTypes, methodBody);
String s = String.format(SOURCE_CODE_TEMPLATE, shortClassName, resultType, resultType, types, methodBody);
System.out.println(s);
return s;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* 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.
@@ -24,6 +24,14 @@ import java.util.function.Function;
public class FunctionCompiler<T, R> extends AbstractFunctionCompiler<Function<T, R>> {
public FunctionCompiler() {
super(ResultType.Function, "Flux<Object>, Flux<Object>");
this("Flux<Object>");
}
public FunctionCompiler(String type) {
this(type, type);
}
public FunctionCompiler(String inputType, String outputType) {
super(ResultType.Function, String.format("%s, %s", inputType, outputType));
}
}

View File

@@ -72,8 +72,8 @@ public class CompiledFunctionRegistry {
this.consumerDirectory.mkdir();
}
public void registerSupplier(String name, String supplier) {
CompiledFunctionFactory<?> factory = this.supplierCompiler.compile(name, supplier);
public void registerSupplier(String name, String supplier, String type) {
CompiledFunctionFactory<?> factory = this.supplierCompiler.compile(name, supplier, type);
File file = new File(this.supplierDirectory, fileName(name));
try {
FileCopyUtils.copy(factory.getGeneratedClassBytes(), file);
@@ -83,8 +83,8 @@ public class CompiledFunctionRegistry {
}
}
public void registerFunction(String name, String function) {
CompiledFunctionFactory<?> factory = this.functionCompiler.compile(name, function);
public void registerFunction(String name, String function, String... types) {
CompiledFunctionFactory<?> factory = this.functionCompiler.compile(name, function, types);
File file = new File(this.functionDirectory, fileName(name));
try {
FileCopyUtils.copy(factory.getGeneratedClassBytes(), file);

View File

@@ -19,6 +19,7 @@ package org.springframework.cloud.function.compiler.app;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
@@ -29,20 +30,21 @@ public class CompilerController {
private final CompiledFunctionRegistry registry = new CompiledFunctionRegistry();
@PostMapping(path = "/{type}/{name}")
public void registerFunction(@PathVariable String type, @PathVariable String name, @RequestBody String lambda) {
switch (type) {
case "supplier":
registry.registerSupplier(name, lambda);
break;
case "function":
registry.registerFunction(name, lambda);
break;
case "consumer":
registry.registerConsumer(name, lambda);
break;
default:
throw new IllegalArgumentException("unknown type: " + type);
}
@PostMapping(path = "/supplier/{name}")
public void registerSupplier(@PathVariable String name, @RequestBody String lambda,
@RequestParam(defaultValue="Flux<String>") String type) {
this.registry.registerSupplier(name, lambda, type);
}
@PostMapping(path = "/function/{name}")
public void registerFunction(@PathVariable String name, @RequestBody String lambda,
@RequestParam(defaultValue="Flux<String>") String inputType,
@RequestParam(defaultValue="Flux<String>") String outputType) {
this.registry.registerFunction(name, lambda, inputType, outputType);
}
@PostMapping(path = "/consumer/{name}")
public void registerConsumer(@PathVariable String name, @RequestBody String lambda) {
this.registry.registerConsumer(name, lambda);
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.function.compiler.config;
import java.util.Map;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -68,7 +69,9 @@ public class FunctionProxyApplicationListener implements ApplicationListener<App
registerByteCodeLoadingProxy(name, type, context.getResource(bytecodeResource), beanFactory);
}
else {
registerLambdaCompilingProxy(name, type, lambda, beanFactory);
String inputType = properties.get("inputType");
String outputType = properties.get("outputType");
registerLambdaCompilingProxy(name, type, inputType, outputType, lambda, beanFactory);
}
}
}
@@ -91,14 +94,18 @@ public class FunctionProxyApplicationListener implements ApplicationListener<App
beanFactory.registerBeanDefinition(name, beanDefinition);
}
private void registerLambdaCompilingProxy(String name, String type, String lambda, DefaultListableBeanFactory beanFactory) {
private void registerLambdaCompilingProxy(String name, String type, String inputType, String outputType, String lambda, DefaultListableBeanFactory beanFactory) {
Resource resource = new ByteArrayResource(lambda.getBytes());
ConstructorArgumentValues args = new ConstructorArgumentValues();
MutablePropertyValues props = new MutablePropertyValues();
args.addGenericArgumentValue(resource);
Class<?> proxyClass = null;
if ("supplier".equals(type.toLowerCase())) {
proxyClass = LambdaCompilingSupplier.class;
args.addGenericArgumentValue(this.supplierCompiler);
if (outputType != null) {
props.add("typeParameterizations", outputType);
}
}
else if ("consumer".equals(type.toLowerCase())) {
proxyClass = LambdaCompilingConsumer.class;
@@ -107,9 +114,16 @@ public class FunctionProxyApplicationListener implements ApplicationListener<App
else {
proxyClass = LambdaCompilingFunction.class;
args.addGenericArgumentValue(this.functionCompiler);
if ((inputType == null && outputType != null) || (outputType == null && inputType != null)) {
throw new IllegalArgumentException("if either input or output type is set, the other is also required");
}
if (inputType != null) {
props.add("typeParameterizations", new String[] { inputType, outputType });
}
}
RootBeanDefinition beanDefinition = new RootBeanDefinition(proxyClass);
beanDefinition.setConstructorArgumentValues(args);
beanDefinition.setPropertyValues(props);
beanFactory.registerBeanDefinition(name, beanDefinition);
}
}

View File

@@ -59,7 +59,7 @@ public abstract class AbstractByteCodeLoadingProxy<T> implements InitializingBea
}
}
protected final T getTarget() {
public final T getTarget() {
return this.target;
}
}

View File

@@ -39,6 +39,8 @@ public class AbstractLambdaCompilingProxy<T> implements InitializingBean, BeanNa
private T target;
private String[] typeParameterizations;
public AbstractLambdaCompilingProxy(Resource resource, AbstractFunctionCompiler<T> compiler) {
Assert.notNull(resource, "Resource must not be null");
Assert.notNull(compiler, "Compiler must not be null");
@@ -51,14 +53,18 @@ public class AbstractLambdaCompilingProxy<T> implements InitializingBean, BeanNa
this.beanName = beanName;
}
public void setTypeParameterizations(String... typeParameterizations) {
this.typeParameterizations = typeParameterizations;
}
@Override
public void afterPropertiesSet() throws Exception {
String lambda = FileCopyUtils.copyToString(new InputStreamReader(this.resource.getInputStream()));
CompiledFunctionFactory<T> factory = this.compiler.compile(this.beanName, lambda);
CompiledFunctionFactory<T> factory = this.compiler.compile(this.beanName, lambda, this.typeParameterizations);
this.target = factory.getResult();
}
protected final T getTarget() {
public final T getTarget() {
return this.target;
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.cloud.function.compiler.proxy;
import java.util.function.Function;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.function.support.FunctionProxy;
import org.springframework.core.io.Resource;
/**
@@ -27,7 +28,7 @@ import org.springframework.core.io.Resource;
* @param <T> Function input type
* @param <R> Function result type
*/
public class ByteCodeLoadingFunction<T, R> extends AbstractByteCodeLoadingProxy<Function<T, R>> implements Function<T, R>, InitializingBean {
public class ByteCodeLoadingFunction<T, R> extends AbstractByteCodeLoadingProxy<Function<T, R>> implements FunctionProxy<T, R>, InitializingBean {
public ByteCodeLoadingFunction(Resource resource) {
super(resource, Function.class);

View File

@@ -18,6 +18,7 @@ package org.springframework.cloud.function.compiler.proxy;
import java.util.function.Supplier;
import org.springframework.cloud.function.support.SupplierProxy;
import org.springframework.core.io.Resource;
/**
@@ -25,7 +26,7 @@ import org.springframework.core.io.Resource;
*
* @param <T> type
*/
public class ByteCodeLoadingSupplier<T> extends AbstractByteCodeLoadingProxy<Supplier<T>> implements Supplier<T> {
public class ByteCodeLoadingSupplier<T> extends AbstractByteCodeLoadingProxy<Supplier<T>> implements SupplierProxy<T> {
public ByteCodeLoadingSupplier(Resource resource) {
super(resource, Supplier.class);

View File

@@ -19,12 +19,13 @@ package org.springframework.cloud.function.compiler.proxy;
import java.util.function.Function;
import org.springframework.cloud.function.compiler.FunctionCompiler;
import org.springframework.cloud.function.support.FunctionProxy;
import org.springframework.core.io.Resource;
/**
* @author Mark Fisher
*/
public class LambdaCompilingFunction<T, R> extends AbstractLambdaCompilingProxy<Function<T, R>> implements Function<T, R> {
public class LambdaCompilingFunction<T, R> extends AbstractLambdaCompilingProxy<Function<T, R>> implements FunctionProxy<T, R> {
public LambdaCompilingFunction(Resource resource, FunctionCompiler<T, R> compiler) {
super(resource, compiler);

View File

@@ -19,12 +19,13 @@ package org.springframework.cloud.function.compiler.proxy;
import java.util.function.Supplier;
import org.springframework.cloud.function.compiler.SupplierCompiler;
import org.springframework.cloud.function.support.SupplierProxy;
import org.springframework.core.io.Resource;
/**
* @author Mark Fisher
*/
public class LambdaCompilingSupplier<T> extends AbstractLambdaCompilingProxy<Supplier<T>> implements Supplier<T> {
public class LambdaCompilingSupplier<T> extends AbstractLambdaCompilingProxy<Supplier<T>> implements SupplierProxy<T> {
public LambdaCompilingSupplier(Resource resource, SupplierCompiler<T> compiler) {
super(resource, compiler);

View File

@@ -0,0 +1,44 @@
/*
* 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.support;
import java.util.function.Function;
import reactor.core.publisher.Flux;
/**
* {@link Function} implementation that wraps a target Function so that the target's
* simple input and output types will be wrapped as {@link Flux} instances.
*
* @author Mark Fisher
*
* @param <T> input type of target function
* @param <R> output type of target function
*/
public class FluxFunction<T, R> implements Function<Flux<T>, Flux<R>> {
private final Function<T, R> function;
public FluxFunction(Function<T, R> function) {
this.function = function;
}
@Override
public Flux<R> apply(Flux<T> input) {
return input.map(i->this.function.apply(i));
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.support;
import java.time.Duration;
import java.util.function.Supplier;
import reactor.core.publisher.Flux;
/**
* {@link Supplier} implementation that wraps a target Supplier so that the
* target's simple output type will be wrapped in a {@link Flux} instance.
* If a {@link Duration} is provided, the Flux will produce output
* periodically, invoking the target Supplier's {@code get} method at each
* interval. If no Duration is provided, the target will be invoked only once.
*
* @author Mark Fisher
*
* @param <T> output type of target supplier
*/
public class FluxSupplier<T> implements Supplier<Flux<T>> {
private final Supplier<T> supplier;
private final Duration period;
public FluxSupplier(Supplier<T> supplier) {
this(supplier, null);
}
public FluxSupplier(Supplier<T> supplier, Duration period) {
this.supplier = supplier;
this.period = period;
}
@Override
public Flux<T> get() {
if (this.period != null) {
return Flux.interval(this.period).map(i->this.supplier.get());
}
return Flux.just(this.supplier.get());
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.support;
import java.util.function.Function;
/**
* @author Mark Fisher
*
* @param <T> input type of target Function
* @param <R> output type of target Function
*/
public interface FunctionProxy<T, R> extends Function<T, R> {
default boolean isFluxFunction() {
return FunctionUtils.isFluxFunction(getTarget());
}
Function<T, R> getTarget();
}

View File

@@ -0,0 +1,99 @@
/*
* 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.support;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import reactor.core.publisher.Flux;
/**
* @author Mark Fisher
*/
public abstract class FunctionUtils {
private static final String FLUX_CLASS_NAME = Flux.class.getName();
private FunctionUtils() {}
@SuppressWarnings("rawtypes")
public static boolean isFluxSupplier(Supplier<?> supplier) {
if (supplier instanceof SupplierProxy) {
return ((SupplierProxy) supplier).isFluxSupplier();
}
String[] types = getParameterizedTypeNames(supplier, Supplier.class);
if (ObjectUtils.isEmpty(types)) {
return true;
}
return (types[0].startsWith(FLUX_CLASS_NAME));
}
@SuppressWarnings("rawtypes")
public static boolean isFluxFunction(Function<?, ?> function) {
if (function instanceof FunctionProxy) {
return ((FunctionProxy) function).isFluxFunction();
}
String[] types = getParameterizedTypeNames(function, Function.class);
if (ObjectUtils.isEmpty(types) || types.length != 2) {
return true;
}
return (types[0].startsWith(FLUX_CLASS_NAME) && types[1].startsWith(FLUX_CLASS_NAME));
}
private static String[] getParameterizedTypeNames(Object source, Class<?> interfaceClass) {
Type[] genericInterfaces = source.getClass().getGenericInterfaces();
for (Type genericInterface : genericInterfaces) {
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 getSerializedLambdaParameterizedTypeNames(source);
}
private static String[] getSerializedLambdaParameterizedTypeNames(Object source) {
Method method = ReflectionUtils.findMethod(source.getClass(), "writeReplace");
if (method == null) {
return null;
}
ReflectionUtils.makeAccessible(method);
SerializedLambda serializedLambda = (SerializedLambda) ReflectionUtils.invokeMethod(method, source);
String signature = serializedLambda.getImplMethodSignature().replaceAll("[()]", "");
List<String> typeNames = new ArrayList<>();
for (String types : signature.split(";")) {
typeNames.add(types.substring(1).replace('/', '.'));
}
return typeNames.toArray(new String[typeNames.size()]);
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.support;
import java.util.function.Supplier;
/**
* @author Mark Fisher
*
* @param <T> output type of target Supplier
*/
public interface SupplierProxy<T> extends Supplier<T> {
default boolean isFluxSupplier() {
return FunctionUtils.isFluxSupplier(getTarget());
}
Supplier<T> getTarget();
}

View File

@@ -23,22 +23,19 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.function.compiler.FunctionCompiler;
import org.springframework.cloud.function.compiler.proxy.LambdaCompilingFunction;
import org.springframework.cloud.function.context.FunctionScan;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ByteArrayResource;
import reactor.core.publisher.Flux;
@FunctionScan
@SpringBootApplication
public class SampleApplication {
@Bean
public Function<Flux<String>, Flux<String>> uppercase() {
return flux -> flux.map(value -> value.toUpperCase());
}
@Bean
public Supplier<Flux<String>> words() {
return () -> Flux.fromArray(new String[] { "foo", "bar" });
public Function<String, String> uppercase() {
return value -> value.toUpperCase();
}
@Bean
@@ -47,18 +44,35 @@ public class SampleApplication {
}
@Bean
public <T, R> FunctionCompiler<T, R> compiler() {
return new FunctionCompiler<>();
public Supplier<String> hello() {
return () -> "hello";
}
@Bean
public Function<Flux<String>, Flux<String>> compiledUppercase(FunctionCompiler<Flux<String>, Flux<String>> compiler) {
String lambda = "f -> f.map(o -> o.toString().toUpperCase())";
public Supplier<Flux<String>> words() {
return () -> Flux.fromArray(new String[] { "foo", "bar" });
}
@Bean
public Function<String, String> compiledUppercase(FunctionCompiler<String, String> compiler) {
String lambda = "s -> s.toUpperCase()";
LambdaCompilingFunction<String, String> function = new LambdaCompilingFunction<>(new ByteArrayResource(lambda.getBytes()), compiler);
function.setTypeParameterizations("String", "String");
return function;
}
@Bean
public Function<Flux<String>, Flux<String>> compiledLowercase(FunctionCompiler<Flux<String>, Flux<String>> compiler) {
String lambda = "f->f.map(o->o.toString().toLowerCase())";
return new LambdaCompilingFunction<>(new ByteArrayResource(lambda.getBytes()), compiler);
}
@Bean
public <T, R> FunctionCompiler<T, R> compiler() {
return new FunctionCompiler<>();
}
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleApplication.class, args);
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.functions;
import java.util.function.Function;
/**
* @author Mark Fisher
*/
public class CharCounter implements Function<String, Integer> {
@Override
public Integer apply(String word) {
return word.length();
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.functions;
import java.util.function.Function;
import reactor.core.publisher.Flux;
/**
* @author Mark Fisher
*/
public class Exclaimer implements Function<Flux<String>, Flux<String>> {
@Override
public Flux<String> apply(Flux<String> words) {
return words.map(word->word+"!!!");
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.functions;
import java.util.function.Function;
/**
* @author Mark Fisher
*/
public class Greeter implements Function<String, String> {
@Override
public String apply(String name) {
return "Hello " + name;
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.junit.Test;
import com.example.functions.CharCounter;
import com.example.functions.Exclaimer;
import com.example.functions.Greeter;
import reactor.core.publisher.Flux;
public class FunctionTests {
private final SampleApplication functions = new SampleApplication();
@Test
public void testUppercase() {
String output = functions.uppercase().apply("foobar");
assertEquals("FOOBAR", output);
}
@Test
public void testLowercase() {
Flux<String> output = functions.lowercase().apply(Flux.just("FOO", "BAR"));
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("foo", results.get(0));
assertEquals("bar", results.get(1));
}
@Test
public void testHello() {
String output = functions.hello().get();
assertEquals("hello", output);
}
@Test
public void testWords() {
Flux<String> output = functions.words().get();
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("foo", results.get(0));
assertEquals("bar", results.get(1));
}
@Test
public void testGreeter() {
assertEquals("Hello World", new Greeter().apply("World"));
}
@Test
public void testExclaimer() {
Flux<String> input = Flux.just("foo", "bar");
Flux<String> output = new Exclaimer().apply(input);
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("foo!!!", results.get(0));
assertEquals("bar!!!", results.get(1));
}
@Test
public void testCharCounter() {
assertEquals((Integer) 21, new CharCounter().apply("this is 21 chars long"));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016 the original author or authors.
* 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.
@@ -13,44 +13,126 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example;
import static org.junit.Assert.assertEquals;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import reactor.core.publisher.Flux;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.cloud.function.scan.packages=com.example.functions")
public class SampleApplicationTests {
private final SampleApplication functions = new SampleApplication();
@Test
public void contextLoads() {
}
@Autowired
private Function<String, String> uppercase;
@Test
public void testUppercase() {
Flux<String> output = functions.uppercase().apply(Flux.just("foo", "bar"));
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("FOO", results.get(0));
assertEquals("BAR", results.get(1));
String output = this.uppercase.apply("foobar");
assertEquals("FOOBAR", output);
}
@Autowired
private Function<Flux<String>, Flux<String>> lowercase;
@Test
public void testLowercase() {
Flux<String> output = functions.lowercase().apply(Flux.just("FOO", "BAR"));
Flux<String> output = this.lowercase.apply(Flux.just("FOO", "BAR"));
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("foo", results.get(0));
assertEquals("bar", results.get(1));
}
@Autowired
private Supplier<String> hello;
@Test
public void testHello() {
String output = this.hello.get();
assertEquals("hello", output);
}
@Autowired
private Supplier<Flux<String>> words;
@Test
public void testWords() {
Flux<String> output = functions.words().get();
Flux<String> output = this.words.get();
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("foo", results.get(0));
assertEquals("bar", results.get(1));
}
}
@Autowired
private Function<String, String> compiledUppercase;
@Test
public void testCompiledUppercase() {
String output = this.compiledUppercase.apply("foobar");
assertEquals("FOOBAR", output);
}
@Autowired
private Function<Flux<String>, Flux<String>> compiledLowercase;
@Test
public void testCompiledLowercase() {
Flux<String> input = Flux.just("FOO", "BAR");
Flux<String> output = this.compiledLowercase.apply(input);
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("foo", results.get(0));
assertEquals("bar", results.get(1));
}
// the following are contributed via @FunctionScan:
@Autowired
private Function<String, String> greeter;
@Test
public void testGreeter() {
String greeting = this.greeter.apply("World");
assertEquals("Hello World", greeting);
}
@Autowired
private Function<Flux<String>, Flux<String>> exclaimer;
@Test
public void testExclaimer() {
Flux<String> input = Flux.just("foo", "bar");
Flux<String> output = this.exclaimer.apply(input);
List<String> results = output.collectList().block();
assertEquals(2, results.size());
assertEquals("foo!!!", results.get(0));
assertEquals("bar!!!", results.get(1));
}
@Autowired
private Function<String, Integer> charCounter;
@Test
public void testCharCounter() {
Integer length = this.charCounter.apply("the quick brown fox");
assertEquals(new Integer(19), length);
}
}

View File

@@ -33,6 +33,8 @@ import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.function.invoker.AbstractFunctionInvoker;
import org.springframework.cloud.function.registry.FunctionCatalog;
import org.springframework.cloud.function.support.FluxFunction;
import org.springframework.cloud.function.support.FunctionUtils;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.binder.Binder;
import org.springframework.cloud.stream.messaging.Processor;
@@ -43,6 +45,7 @@ import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
@@ -82,7 +85,8 @@ public class StreamConfiguration {
@ConditionalOnProperty("spring.cloud.stream.bindings.input.destination")
public AbstractFunctionInvoker<?, ?> invoker(FunctionCatalog registry) {
String name = properties.getEndpoint();
Function<Flux<Object>, Flux<Object>> function = registry.lookupFunction(name);
Function<Object, Object> function = registry.lookupFunction(name);
Assert.notNull(function, "no such function: " + name);
return new StreamListeningFunctionInvoker(function);
}
}

View File

@@ -19,25 +19,36 @@ package org.springframework.cloud.function.stream;
import java.util.function.Function;
import org.springframework.cloud.function.invoker.AbstractFunctionInvoker;
import org.springframework.cloud.function.support.FluxFunction;
import org.springframework.cloud.function.support.FunctionUtils;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Processor;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
/**
* @author Mark Fisher
*/
public class StreamListeningFunctionInvoker extends AbstractFunctionInvoker<Flux<Object>, Flux<Object>> {
public class StreamListeningFunctionInvoker extends AbstractFunctionInvoker<Flux<?>, Flux<?>> {
public StreamListeningFunctionInvoker(Function<Flux<Object>, Flux<Object>> function) {
super(function);
public StreamListeningFunctionInvoker(Function<?, ?> function) {
super(wrapIfNecessary(function));
}
@StreamListener
@Output(Processor.OUTPUT)
public Flux<Object> handle(@Input(Processor.INPUT) Flux<Object> input) {
public Flux<?> handle(@Input(Processor.INPUT) Flux<?> input) {
return this.doInvoke(input);
}
private static Function<Flux<?>, Flux<?>> wrapIfNecessary(Function function) {
Assert.notNull(function, "Function must not be null");
if (!FunctionUtils.isFluxFunction(function)) {
function = new FluxFunction(function);
}
return function;
}
}

View File

@@ -18,9 +18,12 @@ package org.springframework.cloud.function.stream;
import java.util.function.Supplier;
import org.springframework.cloud.function.support.FluxSupplier;
import org.springframework.cloud.function.support.FunctionUtils;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.endpoint.MessageProducerSupport;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
@@ -31,8 +34,12 @@ public class SupplierInvokingMessageProducer<T> extends MessageProducerSupport {
private final Supplier<Flux<T>> supplier;
public SupplierInvokingMessageProducer(Supplier<Flux<T>> supplier) {
this.supplier = supplier;
public SupplierInvokingMessageProducer(Supplier<?> supplier) {
Assert.notNull(supplier, "Supplier must not be null");
if (!FunctionUtils.isFluxSupplier(supplier)) {
supplier = new FluxSupplier<>(supplier);
}
this.supplier = (Supplier<Flux<T>>) supplier;
this.setOutputChannelName(Source.OUTPUT);
}

View File

@@ -18,11 +18,15 @@ package org.springframework.cloud.function.web;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.function.registry.FunctionCatalog;
import org.springframework.cloud.function.support.FluxFunction;
import org.springframework.cloud.function.support.FluxSupplier;
import org.springframework.cloud.function.support.FunctionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -54,6 +58,9 @@ public class FunctionController {
@RequestBody Flux<String> body) {
Function<Object, Object> function = functions.lookupFunction(name);
if (function != null) {
if (!FunctionUtils.isFluxFunction(function)) {
function = new FluxFunction(function);
}
@SuppressWarnings("unchecked")
Flux<String> result = (Flux<String>) function.apply(body);
return debug ? result.log() : result;
@@ -68,8 +75,11 @@ public class FunctionController {
@GetMapping(path = "/{name}")
public Flux<String> supplier(@PathVariable String name) {
@SuppressWarnings("unchecked")
Flux<String> result = (Flux<String>) functions.lookupSupplier(name).get();
Supplier<Object> supplier = functions.lookupSupplier(name);
if (!FunctionUtils.isFluxSupplier(supplier)) {
supplier = new FluxSupplier(supplier);
}
Flux<String> result = (Flux<String>) supplier.get();
return debug ? result.log() : result;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2016 the original author or authors.
* Copyright 2013-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.
@@ -22,8 +22,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.http.converter.ObjectToStringHttpMessageConverter;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@@ -32,6 +35,7 @@ import reactor.core.publisher.Flux;
/**
* @author Dave Syer
* @author Mark Fisher
*/
@Configuration
@ConditionalOnWebApplication
@@ -39,7 +43,12 @@ import reactor.core.publisher.Flux;
public class ReactorAutoConfiguration extends WebMvcConfigurerAdapter {
@Autowired
private FluxReturnValueHandler returnValueHandler;
private ApplicationContext context;
@Bean
public ObjectToStringHttpMessageConverter objectToStringHttpMessageConverter() {
return new ObjectToStringHttpMessageConverter(new DefaultConversionService());
}
@Bean
public FluxReturnValueHandler fluxReturnValueHandler(
@@ -49,6 +58,7 @@ public class ReactorAutoConfiguration extends WebMvcConfigurerAdapter {
@Configuration
protected static class FluxMessageConverterConfiguration {
@Bean
public FluxHttpMessageConverter fluxHttpMessageConverter() {
return new FluxHttpMessageConverter();
@@ -58,7 +68,6 @@ public class ReactorAutoConfiguration extends WebMvcConfigurerAdapter {
@Override
public void addReturnValueHandlers(
List<HandlerMethodReturnValueHandler> returnValueHandlers) {
returnValueHandlers.add(returnValueHandler);
returnValueHandlers.add(context.getBean(FluxReturnValueHandler.class));
}
}