Sunday, April 4, 2010

Intercepting Java method invocations in Jython

When I was creating last version of Grinderstone I got a problem when I couldn't implement Java interface in Jython because the necessary interface was package private. I choosed the way to create proxy around this interface if I couldn't generate class using this interface using Jython (as you now Jython generates class bytecode and defines it directly into classloader).

Invocation Handler
At first I create Invocation handler implementation in Jython:
from java.lang.reflect import InvocationHandler

class DelegateInvocationHandler(InvocationHandler):
    
    def __init__(self, delegateto):
        self.delegateto = delegateto
    
    def invoke(self, proxy, method, args):
        name = method.getName()
        func = getattr(self.delegateto, name)
        params = method.getParameterTypes()
        if params == None or len(params) == 0:
            return func()
        else:
            return func(*args) 
  • delegateto is a jython (or java) object which will handle interface method invocations
  • we should check target function to arguments count and if necessary use multiple paramemeters, i.e. *-sign for arguments decomposition (args is an array but we have to pass each array element as separate argument to the method)
Assign handler to proxy
In Java utility class we create an instance of proxy with the given handler instance from Jython, i.e.:
public class ProxyUtil {

    public static Object createProxy(Class[] interfaces, InvocationHandler h) throws Exception {
        ClassLoader loader = ProxyUtil.class.getClassLoader();
        return Proxy.newProxyInstance(loader, interfaces, h);
    }

}
And we can create Proxy in Jython now:
ProxyUtil.createProxy([ProcessContext], DelegateInvocationHandler(debugContext))
  • ProcessContext is a package private interface
  • debugContext is an instance of class defined in Jython
Problems
Set of problems you can get in a case if your methods use primitives or its object wrappers as return types, i.e. the following inerface usage
interface ProcessContext {
     boolean isInProgress();
}
can cause the following problems (and it doesn't matter where method call will be performed in Java code or Jython):
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Boolean
For other cases you can get exception look like this:
java.lang.ClassCastException: java.math.BigDecimal cannot be cast to java.lang.Double
and others.
Solution
I created wrapper for Jython Invocation handler for coercing data types:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PyTypeToJavaTypeCoerceInvocationHandler implements InvocationHandler {

    private InvocationHandler wrapped;

    public PyTypeToJavaTypeCoerceInvocationHandler(InvocationHandler h) {
        super();
        this.wrapped = h;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Object result = this.wrapped.invoke(proxy, method, args);

        if (result != null) {
            Class actualType = method.getReturnType();
            Class returnedType = result.getClass();

            if (isBoolean(actualType) && !isBoolean(returnedType)) {
                int value = ((Number) result).intValue();
                return Boolean.valueOf(value == 1);
            } else if (isLong(actualType) && !isLong(returnedType)) {
                Number n = (Number) result;
                return Long.valueOf(n.longValue());
            } else if (isInteger(actualType) && !isInteger(returnedType)) {
                Number n = (Number) result;
                return Integer.valueOf(n.intValue());
            } else if (isDouble(actualType) && !isDouble(returnedType)) {
                Number n = (Number) result;
                return Double.valueOf(n.doubleValue());
            } else if (isFloat(actualType) && !isFloat(returnedType)) {
                Number n = (Number) result;
                return Float.valueOf(n.floatValue());
            } else if (isByte(actualType) && !isByte(returnedType)) {
                Number n = (Number) result;
                return Byte.valueOf(n.byteValue());
            } else if (isShort(actualType) && !isShort(returnedType)) {
                Number n = (Number) result;
                return Short.valueOf(n.shortValue());
            }
        }
        return result;
    }

    private boolean isShort(Class type) {
        return type == Short.TYPE || type == Short.class;
    }

    private boolean isByte(Class type) {
        return type == Byte.TYPE || type == Byte.class;
    }

    private boolean isFloat(Class type) {
        return type == Float.TYPE || type == Float.class;
    }

    private boolean isDouble(Class type) {
        return type == Double.TYPE || type == Double.class;
    }

    private boolean isLong(Class type) {
        return type == Long.TYPE || type == Long.class;
    }

    private boolean isBoolean(Class type) {
        return type == Boolean.TYPE || type == Boolean.class;
    }

    private boolean isInteger(Class type) {
        return type == Integer.TYPE || type == Integer.class;
    }
}
and wrapped the given Invocation Handler from Jython like this:
public class ProxyUtil {

    public static Object createProxy(Class[] interfaces, InvocationHandler h) throws Exception {
        ClassLoader loader = ProxyUtil.class.getClassLoader();
        InvocationHandler wrapper = new PyTypeToJavaTypeCoerceInvocationHandler(h);
        return Proxy.newProxyInstance(loader, interfaces, wrapper);
    }
}
And all works fine now!
 
Blogged.com Technology Blogs