Add FunctionFactoryMetadata interface for type discovery

The compiled functions implement that interface which means you can inspect
the signature of the method that created them and discover its
generic types.
This commit is contained in:
Dave Syer
2017-05-26 10:09:47 +01:00
parent 5a7c95bd97
commit 99c7b995e0
27 changed files with 551 additions and 55 deletions

View File

@@ -44,6 +44,12 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -23,7 +23,4 @@ public interface CompilationResultFactory<T> {
T getResult();
String getInputType();
String getOutputType();
}

View File

@@ -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<T> implements CompilationResultFactory<T> {
private String outputType;
public CompiledFunctionFactory(String className, CompilationResult compilationResult) {
private Method method;
public CompiledFunctionFactory(String className,
CompilationResult compilationResult) {
List<Class<?>> 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<T> factory = (CompilationResultFactory<T>) clazz.newInstance();
CompilationResultFactory<T> factory = (CompilationResultFactory<T>) 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<T> implements CompilationResultFactory<T> {
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> 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;
}

View File

@@ -45,6 +45,7 @@ public class FunctionCompiler<T, R> extends AbstractFunctionCompiler<Function<T,
protected CompiledFunctionFactory<Function<T, R>> postProcessCompiledFunctionFactory(CompiledFunctionFactory<Function<T, R>> factory) {
factory.setInputType(this.inputType);
factory.setOutputType(this.outputType);
return factory;
}
}

View File

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

View File

@@ -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>") String type) {
this.registry.registerConsumer(name, lambda, type);
}
}

View File

@@ -115,6 +115,9 @@ public class FunctionProxyApplicationListener implements ApplicationListener<App
else if ("consumer".equals(type.toLowerCase())) {
proxyClass = LambdaCompilingConsumer.class;
args.addGenericArgumentValue(this.consumerCompiler);
if (inputType != null) {
props.add("typeParameterizations", inputType);
}
}
else {
proxyClass = LambdaCompilingFunction.class;

View File

@@ -16,12 +16,16 @@
package org.springframework.cloud.function.compiler.proxy;
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicReference;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.function.compiler.CompilationResultFactory;
import org.springframework.cloud.function.compiler.FunctionCompiler;
import org.springframework.cloud.function.compiler.java.SimpleClassLoader;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ReflectionUtils;
/**
* @author Mark Fisher
@@ -36,6 +40,8 @@ public abstract class AbstractByteCodeLoadingProxy<T> 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<T> 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<T>) 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> 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;
}
}

View File

@@ -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<T> implements InitializingBean, BeanNameAware {
public class AbstractLambdaCompilingProxy<T> implements InitializingBean, BeanNameAware, FunctionFactoryMetadata {
private final Resource resource;
@@ -67,11 +69,7 @@ public class AbstractLambdaCompilingProxy<T> 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();
}
}

View File

@@ -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<Consumer<String>> compiled = new ConsumerCompiler<String>(
String.class.getName()).compile("foos",
"flux -> flux.subscribe(System.out::println)", "Flux<String>");
assertThat(FunctionUtils.isFluxConsumer(compiled.getFactoryMethod())).isTrue();
}
@Test
public void consumesString() {
CompiledFunctionFactory<Consumer<String>> compiled = new ConsumerCompiler<String>(
String.class.getName()).compile("foos", "System.out::println", "String");
assertThat(FunctionUtils.isFluxConsumer(compiled.getFactoryMethod())).isFalse();
}
}

View File

@@ -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<Function<String, String>> compiled = new FunctionCompiler<String, String>(
String.class.getName()).compile("foos",
"flux -> flux.map(v -> v.toUpperCase())", "Flux<String>", "Flux<String>");
assertThat(FunctionUtils.isFluxFunction(compiled.getFactoryMethod())).isTrue();
}
@Test
public void transformsString() {
CompiledFunctionFactory<Function<String, String>> compiled = new FunctionCompiler<String, String>(
String.class.getName()).compile("foos", "v -> v.toUpperCase()", "String", "String");
assertThat(FunctionUtils.isFluxFunction(compiled.getFactoryMethod())).isFalse();
assertThat(compiled.getResult().apply("hello")).isEqualTo("HELLO");
}
}

View File

@@ -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<Supplier<String>> compiled = new SupplierCompiler<String>(
String.class.getName()).compile("foos",
"() -> Flux.just(\"foo\", \"bar\")", "Flux<String>");
assertThat(FunctionUtils.isFluxSupplier(compiled.getFactoryMethod())).isTrue();
}
@Test
public void supppliesString() {
CompiledFunctionFactory<Supplier<String>> compiled = new SupplierCompiler<String>(
String.class.getName()).compile("foos",
"() -> \"foo\"", "String");
assertThat(FunctionUtils.isFluxSupplier(compiled.getFactoryMethod())).isFalse();
assertThat(compiled.getResult().get()).isEqualTo("foo");
}
}

View File

@@ -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<Consumer<String>> compiled = new ConsumerCompiler<String>(
String.class.getName()).compile("foos", "System.out::println", "String");
ByteArrayResource resource = new ByteArrayResource(compiled.getGeneratedClassBytes(), "foos") {
@Override
public String getFilename() {
return "foos.fun";
}
};
ByteCodeLoadingConsumer<String> consumer = new ByteCodeLoadingConsumer<>(resource);
consumer.afterPropertiesSet();
assertThat(FunctionUtils.isFluxConsumer(consumer.getFactoryMethod())).isFalse();
consumer.accept("foo");
}
@Test
public void compileSupplier() throws Exception {
CompiledFunctionFactory<Supplier<String>> compiled = new SupplierCompiler<String>(
String.class.getName()).compile("foos", "() -> \"foo\"", "String");
ByteArrayResource resource = new ByteArrayResource(compiled.getGeneratedClassBytes(), "foos") {
@Override
public String getFilename() {
return "foos.fun";
}
};
ByteCodeLoadingSupplier<String> supplier = new ByteCodeLoadingSupplier<>(resource);
supplier.afterPropertiesSet();
assertThat(FunctionUtils.isFluxSupplier(supplier.getFactoryMethod())).isFalse();
assertThat(supplier.get()).isEqualTo("foo");
}
@Test
public void compileFunction() throws Exception {
CompiledFunctionFactory<Function<String, String>> compiled = new FunctionCompiler<String, String>(
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<String, String> 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<Function<Flux<String>, Flux<String>>> compiled = new FunctionCompiler<Flux<String>, Flux<String>>(
String.class.getName()).compile("foos", "flux -> flux.map(v -> v.toUpperCase())", "Flux<String>", "Flux<String>");
ByteArrayResource resource = new ByteArrayResource(compiled.getGeneratedClassBytes(), "foos") {
@Override
public String getFilename() {
return "foos.fun";
}
};
ByteCodeLoadingFunction<Flux<String>, Flux<String>> function = new ByteCodeLoadingFunction<>(resource);
function.afterPropertiesSet();
assertThat(FunctionUtils.isFluxFunction(function.getFactoryMethod())).isTrue();
assertThat(function.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO");
}
}