1

I have a map in my config class that looks like the code below (though my actual problem deals with a different set of classes):

private Map<Class, Function<String, ?>> someParser = ImmutableMap.of(
            Short.class, Short::parseShort, Integer.class, Integer::parseInt, 
            Double.class, Double::parseDouble);

Is there a way to configure this in XML file? Like treating method references as beans in XML file? Since the code below obviously doesn't work:

<util:map id="someParser" key-type="java.lang.Class">
    <entry key="java.lang.Short" value-ref="Short::parseShort" />
    <entry key="java.lang.Integer" value-ref="Integer::parseInteger" />
    <entry key="java.lang.Double" value-ref="Double::parseDouble" />
</util:map>
Julius Delfino
  • 991
  • 10
  • 27
  • Why not just write a Java config class instead of XML? – M. Deinum May 19 '20 at 06:33
  • Because XML is easily configurable in production. – Julius Delfino May 19 '20 at 06:44
  • So is java... If you configure XML in production you are basicaly running untested code. That being said you might be able to workaround this using SpEL but that means you are programming in XML (which you shouldn't be doing generally speaking). – M. Deinum May 19 '20 at 06:46
  • We can discuss that in another topic. In case you didn't notice, I mentioned that I'm already using a Java config class. I'm curious to know if this can be moved to XML. – Julius Delfino May 19 '20 at 06:48

1 Answers1

2

Short answer is YES, but I needed a method-reference factory class to help me generate those beans. It's definitely too verbose, but it works. I ended up with something like so:

<bean id="methodFactory" class="com.foo.bar.util.MethodRefFactory" />    
<util:map id="someParser" key-type="java.lang.Class">
    <entry key="java.lang.Integer" value-ref="parseInteger" />
    <entry key="java.lang.Double" value-ref="parseDouble" />
</util:map>
<bean id="parseInteger" factory-bean="methodFactory" factory-method="getMethodRef">
    <constructor-arg value="java.lang.Integer::parseInt" />
    <constructor-arg value="java.util.function.Function" type="java.lang.Class" />
    <constructor-arg><null /></constructor-arg>
</bean>
<bean id="parseDouble" factory-bean="methodFactory" factory-method="getMethodRef">
    <constructor-arg value="java.lang.Double::parseDouble" />
    <constructor-arg value="java.util.function.Function" type="java.lang.Class" />
    <constructor-arg><null /></constructor-arg>
</bean>

The factory class should just take in the method-reference syntax <class|instance>::<method> as a String, split it and return the method-reference as a Function. But I wanted a generic factory class that can handle both static and non-static method references (eg. java.class.Integer::parseInt and fileValidator::validate), so here's the factory class that I came up with. I hope you find this class useful as well:

public class MethodRefFactory {

    private ApplicationContext context;

    public MethodRefFactory(ApplicationContext context) {
        this.context = context;
    }

    /** Use this method to create non-static method references like fileValidator::validate */
    public <T> T getMethodSpring(String signature, Class<T> lambdaFuncType) throws Throwable {
        String[] sigTokens = signature.split("::");
        String sigClass = sigTokens[0];
        Object objToInvoke = null;
        try {
            objToInvoke = context.getBean(sigClass);
        } catch(BeansException ex) {
            ex.printStackTrace();
        }
        return getMethodRef(signature, lambdaFuncType, objToInvoke);
    }

    /** Use this method to create static method references like java.lang.Integer::parseInt */
    public <T> T getMethodRef(String signature, Class<T> lambdaFuncType, Object objToInvoke) throws Throwable {
        String[] sigTokens = signature.split("::");
        String sigClass = sigTokens[0];
        String sigMethod = sigTokens[1];
        boolean isStaticMethod = Objects.isNull(objToInvoke);
        Class realType = isStaticMethod ? Class.forName(sigClass) : objToInvoke.getClass();
        Method realMethod = getRealMethod(realType, sigMethod);

        MethodHandles.Lookup caller = MethodHandles.lookup();
        MethodHandle realMethodHandle = caller.unreflect(realMethod);
        MethodType realMethodHandleType = realMethodHandle.type();
        MethodType lambdaFuncMethodType = isStaticMethod ? realMethodHandleType.generic() :
                generateLambdaFuncMethodType(lambdaFuncType);
        MethodType targetMethodType = isStaticMethod ? realMethodHandleType :
                extractMatchingMethodTypeForRealMethod(realMethodHandleType);
        String lambdaFuncMethodName = lambdaFuncType.getMethods()[0].getName();
        MethodType lambdaFuncAndRealType = isStaticMethod ? MethodType.methodType(lambdaFuncType) :
                MethodType.methodType(lambdaFuncType, realType);
        CallSite site = LambdaMetafactory.metafactory(caller, lambdaFuncMethodName, lambdaFuncAndRealType,
                lambdaFuncMethodType, realMethodHandle, targetMethodType);
        MethodHandle factory = site.getTarget();
        if (!isStaticMethod) {
            factory = factory.bindTo(objToInvoke);
        }
        return (T) factory.invoke();
    }

    private Method getRealMethod(Class type, String methodName) {
        return Arrays.stream(type.getMethods()).filter(m -> m.getName().equals(methodName))
                .sorted(this::compareMethods).findFirst().get();
    }

    private MethodType extractMatchingMethodTypeForRealMethod(MethodType target) {
        return MethodType.methodType(target.returnType(), Arrays.copyOfRange(target.parameterArray(),1, target.parameterCount()));
    }

    private <T> MethodType generateLambdaFuncMethodType(Class<T> funcType) {
        Method method = funcType.getMethods()[0];
        if (method.getParameterCount() == 0) {
            return MethodType.methodType(Object.class);
        }
        Class[] params = Arrays.copyOfRange(method.getParameterTypes(), 1, method.getParameterCount());
        return MethodType.methodType(method.getReturnType(), method.getParameterTypes()[0], params);
    }

    private int compareMethods(Method m1, Method m2) {
        return m1.getName().equals(m2.getName()) ? Integer.compare(m1.getParameterCount(), m2.getParameterCount()) :
                m1.getName().compareTo(m2.getName());
    }
}

Here's SimpleBean class and MyInterface interface:

interface MyInterface<R> {

    R process(Object in);
}

class SimpleBean {

    public String simpleFunction(String in) {
        return "java.util.function.Function test: " + in;
    }

    public String simpleBiFunction(Double in, Integer y) {
        return "java.util.function.BiFunction test: " + (in + y);
    }

    public String simpleIntFunction(Integer y) {
        return "java.util.function.IntFunction test: " + y;
    }

    public String simpleSupplier() {
        return "java.util.function.Supplier test ";
    }

    public void simpleConsumer(String param) {
        System.out.println("java.util.function.Consumer test: " + param);
    }
}

Here are my test cases. These demonstrate that the factory class supports Function, BiFunction, IntFunction, Supplier, Consumer, even a custom functional interface:

public static void main(String[] args) throws Throwable {

    MethodRefFactory factory = new MethodRefFactory(null);
    Function f = factory.getMethodRef("java.lang.Short::parseShort", Function.class, null);
    System.out.println(f.apply("2343"));

    SimpleBean bean = new SimpleBean();

    Function f2 = factory.getMethodRef("com.foo.bar.util.SimpleBean::simpleFunction", Function.class, bean);
    System.out.println(f2.apply("foo"));

    BiFunction f3 = factory.getMethodRef("com.foo.bar.util.SimpleBean::simpleBiFunction", BiFunction.class, bean);
    System.out.println(f3.apply(25.0, 4));

    Supplier f4 = factory.getMethodRef("com.foo.bar.util.SimpleBean::simpleSupplier", Supplier.class, bean);
    System.out.println(f4.get());

    Consumer f5 = factory.getMethodRef("com.foo.bar.util.SimpleBean::simpleConsumer", Consumer.class, bean);
    f5.accept("bar");

    IntFunction f6 = factory.getMethodRef("com.foo.bar.util.SimpleBean::simpleIntFunction", IntFunction.class, bean);
    System.out.println(f6.apply(1234));

    MyInterface f7 = factory.getMethodRef("com.foo.bar.util.SimpleBean::simpleFunction", MyInterface.class, bean);
    System.out.println(f7.process("myInterface param"));
}

And here's one last sample XML for non-static method references:

<util:map id="executorMap" key-type="java.lang.Class">
    <entry key="com.foo.bar.action.Read" value-ref="readerReadMsg" />
    <entry key="com.foo.bar.action.Validate" value-ref="validatorValidateMsg" />
    <entry key="com.foo.bar.action.Transform" value-ref="transformerTransformMsg" />
    <entry key="com.foo.bar.action.Persist" value-ref="persisterPersistMsg" />
</util:map>

<bean id="readerReadMsg" factory-bean="methodFactory" factory-method="getMethodSpring">
    <constructor-arg value="reader::readMsg" />
    <constructor-arg value="java.util.function.BiFunction" type="java.lang.Class" />
</bean>
<bean id="validatorValidateMsg" factory-bean="methodFactory" factory-method="getMethodSpring">
    <constructor-arg value="validator::validateMsg" />
    <constructor-arg value="java.util.function.BiFunction" type="java.lang.Class" />
</bean>
<bean id="transformerTransformMsg" factory-bean="methodFactory" factory-method="getMethodSpring">
    <constructor-arg value="transformer::transformMsg" />
    <constructor-arg value="java.util.function.BiFunction" type="java.lang.Class" />
</bean>
<bean id="persisterPersistMsg" factory-bean="methodFactory" factory-method="getMethodSpring">
    <constructor-arg value="persister::persistMsg" />
    <constructor-arg value="java.util.function.BiFunction" type="java.lang.Class" />
</bean>

The following links substantially helped me understand how LambdaMetafactory.metafactory works and how it can enable me to achieve what I want: this, this and this.

Julius Delfino
  • 991
  • 10
  • 27