GH-387 Added initial support for flexible function signatures
- Added support for simple POJO functions - Added additional utility methods
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.function.context.catalog;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
@@ -32,6 +33,8 @@ import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Publisher;
|
||||
@@ -39,6 +42,8 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuples;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
@@ -98,10 +103,9 @@ public class BeanFactoryAwareFunctionRegistry
|
||||
this.messageConverter = messageConverter;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T lookup(Class<?> type, String definition) {
|
||||
return (T) this.compose(type, definition);
|
||||
return this.lookup(definition, new String[] {});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -113,8 +117,8 @@ public class BeanFactoryAwareFunctionRegistry
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T lookup(String definition, String... acceptedOutputTypes) {
|
||||
Assert.notEmpty(acceptedOutputTypes, "'acceptedOutputTypes' must not be null or empty");
|
||||
return (T) this.compose(null, definition, acceptedOutputTypes);
|
||||
Object function = this.proxyInvokerIfNecessary((FunctionInvocationWrapper) this.compose(null, definition, acceptedOutputTypes));
|
||||
return (T) function;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -232,6 +236,7 @@ public class BeanFactoryAwareFunctionRegistry
|
||||
+ "Function available in catalog are: " + this.getNames(null));
|
||||
return null;
|
||||
}
|
||||
|
||||
composedNameBuilder.append(prefix);
|
||||
composedNameBuilder.append(name);
|
||||
|
||||
@@ -239,12 +244,17 @@ public class BeanFactoryAwareFunctionRegistry
|
||||
Type currentFunctionType = null;
|
||||
if (function instanceof FunctionRegistration) {
|
||||
registration = (FunctionRegistration<Object>) function;
|
||||
currentFunctionType = registration.getType().getType();
|
||||
currentFunctionType = currentFunctionType == null ? registration.getType().getType() : currentFunctionType;
|
||||
function = registration.getTarget();
|
||||
}
|
||||
else {
|
||||
if (isFunctionPojo(function)) {
|
||||
Method functionalMethod = FunctionTypeUtils.discoverFunctionalMethod(function.getClass());
|
||||
currentFunctionType = FunctionTypeUtils.fromFunctionMethod(functionalMethod);
|
||||
function = this.proxyTarget(function, functionalMethod);
|
||||
}
|
||||
String[] aliasNames = this.getAliases(name).toArray(new String[] {});
|
||||
currentFunctionType = this.discoverFunctionType(function, aliasNames);
|
||||
currentFunctionType = currentFunctionType == null ? this.discoverFunctionType(function, aliasNames) : currentFunctionType;
|
||||
registration = new FunctionRegistration<>(function, name).type(currentFunctionType);
|
||||
}
|
||||
|
||||
@@ -275,6 +285,73 @@ public class BeanFactoryAwareFunctionRegistry
|
||||
return resultFunction;
|
||||
}
|
||||
|
||||
private boolean isFunctionPojo(Object function) {
|
||||
return !function.getClass().isSynthetic()
|
||||
&& !(function instanceof Supplier) && !(function instanceof Function) && !(function instanceof Consumer)
|
||||
&& !function.getClass().getPackage().getName().startsWith("org.springframework.cloud.function.compiler");
|
||||
}
|
||||
|
||||
/*
|
||||
* == OUTER PROXY ===
|
||||
* For cases where function is POJO we need to be able to look it up as Function
|
||||
* as well as the type of actual pojo (e.g., MyFunction f1 = catalog.lookup("myFunction");)
|
||||
* To do this we wrap the target into CglibProxy (for cases when function is a POJO ) with the
|
||||
* actual target class (e.g., MyFunction). Meanwhile the invocation will be delegated to
|
||||
* the FunctionInvocationWrapper which will trigger the INNER PROXY. This effectively ensures that
|
||||
* conversion, composition and/or fluxification would happen (code inside of FunctionInvocationWrapper)
|
||||
* while the inner proxy invocation will delegate the invocation with already converted arguments
|
||||
* to the actual target class (e.g., MyFunction).
|
||||
*/
|
||||
private Object proxyInvokerIfNecessary(FunctionInvocationWrapper functionInvoker) {
|
||||
if (functionInvoker != null && AopUtils.isCglibProxy(functionInvoker.getTarget())) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Proxying POJO function: " + functionInvoker.functionDefinition + ". . ." + functionInvoker.target.getClass());
|
||||
}
|
||||
ProxyFactory pf = new ProxyFactory(functionInvoker.getTarget());
|
||||
pf.setProxyTargetClass(true);
|
||||
pf.setInterfaces(Function.class, Supplier.class, Consumer.class);
|
||||
pf.addAdvice(new MethodInterceptor() {
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
// this will trigger the INNER PROXY
|
||||
if (ObjectUtils.isEmpty(invocation.getArguments())) {
|
||||
Object o = functionInvoker.get();
|
||||
return o;
|
||||
}
|
||||
else {
|
||||
// this is where we probably would need to gather all arguments into tuples
|
||||
return functionInvoker.apply(invocation.getArguments()[0]);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
return pf.getProxy();
|
||||
}
|
||||
return functionInvoker;
|
||||
}
|
||||
|
||||
/*
|
||||
* == INNER PROXY ===
|
||||
* When dealing with POJO functions we still want to be able to treat them as any other
|
||||
* function for purposes of composition, type conversion and fluxification.
|
||||
* So this proxy will ensure that the target class can be represented as Function while delegating
|
||||
* any call to apply to the actual target method.
|
||||
* Since this proxy is part of the FunctionInvocationWrapper composition and copnversion will be applied
|
||||
* as tyo any other function.
|
||||
*/
|
||||
private Object proxyTarget(Object targetFunction, Method actualMethodToCall) {
|
||||
ProxyFactory pf = new ProxyFactory(targetFunction);
|
||||
pf.setProxyTargetClass(true);
|
||||
pf.setInterfaces(Function.class);
|
||||
pf.addAdvice(new MethodInterceptor() {
|
||||
@Override
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
return actualMethodToCall.invoke(invocation.getThis(), invocation.getArguments());
|
||||
}
|
||||
});
|
||||
return pf.getProxy();
|
||||
}
|
||||
|
||||
private Collection<String> getAliases(String key) {
|
||||
Collection<String> names = new LinkedHashSet<>();
|
||||
String value = getQualifier(key);
|
||||
|
||||
@@ -19,18 +19,25 @@ package org.springframework.cloud.function.context.catalog;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.util.function.Tuple2;
|
||||
|
||||
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* Set of utility operations to interrogate function definitions.
|
||||
@@ -61,6 +68,43 @@ public final class FunctionTypeUtils {
|
||||
return rawType instanceof Class<?> && Collection.class.isAssignableFrom((Class<?>) rawType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will attempt to discover functional methods on the class. It's applicable for POJOs as well as
|
||||
* functional classes in `java.util.function` package. For the later the names of the methods are
|
||||
* well known (`apply`, `accept` and `get`). For the former it will attempt to discover a single method
|
||||
* following semantics described in (see {@link FunctionalInterface})
|
||||
*
|
||||
* @param pojoFunctionClass the class to introspect
|
||||
* @return functional method
|
||||
*/
|
||||
public static Method discoverFunctionalMethod(Class<?> pojoFunctionClass) {
|
||||
|
||||
if (Supplier.class.isAssignableFrom(pojoFunctionClass)) {
|
||||
return Stream.of(ReflectionUtils.getDeclaredMethods(pojoFunctionClass)).filter(m -> m.getName().equals("get")).findFirst().get();
|
||||
}
|
||||
else if (Consumer.class.isAssignableFrom(pojoFunctionClass) || BiConsumer.class.isAssignableFrom(pojoFunctionClass)) {
|
||||
return Stream.of(ReflectionUtils.getDeclaredMethods(pojoFunctionClass)).filter(m -> m.getName().equals("accept")).findFirst().get();
|
||||
}
|
||||
else if (Function.class.isAssignableFrom(pojoFunctionClass) || BiFunction.class.isAssignableFrom(pojoFunctionClass)) {
|
||||
return Stream.of(ReflectionUtils.getDeclaredMethods(pojoFunctionClass)).filter(m -> m.getName().equals("apply")).findFirst().get();
|
||||
}
|
||||
|
||||
List<Method> methods = new ArrayList<>();
|
||||
ReflectionUtils.doWithMethods(pojoFunctionClass, method -> {
|
||||
if (method.getDeclaringClass() == pojoFunctionClass) {
|
||||
methods.add(method);
|
||||
}
|
||||
|
||||
}, method ->
|
||||
!method.getDeclaringClass().isAssignableFrom(Object.class)
|
||||
&& !method.isSynthetic() && !method.isBridge() && !method.isVarArgs());
|
||||
|
||||
Assert.isTrue(methods.size() == 1, "Discovered " + methods.size() + " methods that would qualify as 'functional' - "
|
||||
+ methods + ".\n Class '" + pojoFunctionClass + "' is not a FunctionalInterface.");
|
||||
|
||||
return methods.get(0);
|
||||
}
|
||||
|
||||
public static Type getFunctionTypeFromFunctionMethod(Method functionMethod) {
|
||||
Assert.isTrue(
|
||||
functionMethod.getName().equals("apply") ||
|
||||
@@ -231,10 +275,6 @@ public final class FunctionTypeUtils {
|
||||
return argument != null && argument.getClass().getName().startsWith("reactor.util.function.Tuple");
|
||||
}
|
||||
|
||||
private static boolean isMulti(Type type) {
|
||||
return type.getTypeName().startsWith("reactor.util.function.Tuple");
|
||||
}
|
||||
|
||||
public static boolean isSupplier(Type type) {
|
||||
return type.getTypeName().startsWith("java.util.function.Supplier");
|
||||
}
|
||||
@@ -268,6 +308,48 @@ public final class FunctionTypeUtils {
|
||||
return originType;
|
||||
}
|
||||
|
||||
static Type fromFunctionMethod(Method functionalMethod) {
|
||||
Type[] parameterTypes = functionalMethod.getGenericParameterTypes();
|
||||
|
||||
Type functionType = null;
|
||||
switch (parameterTypes.length) {
|
||||
case 0:
|
||||
functionType = ResolvableType.forClassWithGenerics(Supplier.class,
|
||||
ResolvableType.forMethodReturnType(functionalMethod)).getType();
|
||||
break;
|
||||
case 1:
|
||||
if (Void.class.isAssignableFrom(functionalMethod.getReturnType())) {
|
||||
functionType = ResolvableType.forClassWithGenerics(Consumer.class,
|
||||
ResolvableType.forMethodParameter(functionalMethod, 0)).getType();
|
||||
}
|
||||
else {
|
||||
functionType = ResolvableType.forClassWithGenerics(Function.class,
|
||||
ResolvableType.forMethodParameter(functionalMethod, 0),
|
||||
ResolvableType.forMethodReturnType(functionalMethod)).getType();
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
ResolvableType canonicalParametersWrapper = fromTwoArityFunction(functionalMethod);
|
||||
functionType = ResolvableType.forClassWithGenerics(Function.class,
|
||||
canonicalParametersWrapper,
|
||||
ResolvableType.forMethodReturnType(functionalMethod)).getType();
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Functional method: " + functionalMethod + " is not supported");
|
||||
}
|
||||
return functionType;
|
||||
}
|
||||
|
||||
private static ResolvableType fromTwoArityFunction(Method functionalMethod) {
|
||||
return ResolvableType.forClassWithGenerics(Tuple2.class,
|
||||
ResolvableType.forMethodParameter(functionalMethod, 0),
|
||||
ResolvableType.forMethodParameter(functionalMethod, 1));
|
||||
}
|
||||
|
||||
private static boolean isMulti(Type type) {
|
||||
return type.getTypeName().startsWith("reactor.util.function.Tuple");
|
||||
}
|
||||
|
||||
private static void assertSupportedTypes(Type type) {
|
||||
if (type instanceof ParameterizedType) {
|
||||
type = ((ParameterizedType) type).getRawType();
|
||||
|
||||
Reference in New Issue
Block a user