Wednesday, October 29, 2008

Proxy Object and Equals Method

Couple days back I was debugging a XA transaction related bug and I did not see what calls that JBoss TM is making into the underlying XA resource. So, I thought I will slap in a quick Proxy on top of the XAResource interface to log the methods that are being invoked. Then, things lot little more complicated, I started seeing same XAResource behaving as if they are two entirely different resources in a single transaction, which led me to believe the implementation class for XAResouce may be using "equals" method inside it's "isSameRM" call and thus showing erronious behaviour.

To understand why this was happening I started with following code


public interface Foo {
void doSomething();
}

public class FooImpl implements Foo {
public void doSomething() {
System.out.println("doing something");
}
}

public class Main {
public static void main(String[] args) {

final Foo a = new FooImpl();

Foo aProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class[] { Foo.class }, new InvocationHandler(){

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

try {
return method.invoke(a, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
});
print("a.equals(a) = " + ((a.equals(a))?"same":"not same"));
print("a.equals(aProxy) = " + ((a.equals(aProxy))?"same":"not same"));
print("aProxy.equals(a) = " + ((aProxy.equals(a))?"same":"not same"));
print("aProxy.equals(aProxy) = " + ((aProxy.equals(aProxy))?"same":"not same"));
}

public static void print(String msg){
System.out.println(msg);
}
}


The output was
---------------------------------------------
a.equals(a) = same
a.equals(aProxy) = not same
aProxy.equals(a) = same
aProxy.equals(aProxy) = not same
---------------------------------------------

this confirmed the object identity behaviour, then I added "equals" method to "FooImpl" as below

public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof FooImpl))
return false;
FooImpl other = (FooImpl) obj;
// check any non static stuff here..
return true;
}

That did not change any equality check, which I was expecting, then I thought I should intercept the "equals" method on the Proxy and call the "equals" on the non wrapped object to mimic the original behavior, so I added/modified the following code in "Main" class

Foo aProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class[] { Foo.class }, new InvocationHandler(){
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
try {

if (method.getName().equals("equals") && args.length == 1){
if (Proxy.isProxyClass(args[0].getClass())){
return args[0].equals(a);
} else {
return a.equals(args[0]);
}
}

return method.invoke(a, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
});

---------------------------------------------
a.equals(a) = same
a.equals(aProxy) = not same
aProxy.equals(a) = same
aProxy.equals(aProxy) = same
---------------------------------------------

Still, I did not see all the contracts of the "equals" method matched. Then, finally I changed code in "FooImpl"s equals method to

public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;

if(Proxy.isProxyClass(obj.getClass())){
return obj.equals(this);
}

if (!(obj instanceof FooImpl))
return false;
FooImpl other = (FooImpl) obj;
// check any non static stuff here..
return true;
}

---------------------------------------------
a.equals(a) = same
a.equals(aProxy) = same
aProxy.equals(a) = same
aProxy.equals(aProxy) = same
---------------------------------------------

Now, that made them all equal. However, I was not sure of the last code fragment, is this a viable option? When calling code is asking to check the equality, I was delegating back to a unknown proxy class to handle it (this could pose a lot of issues), but at the same time proxy could have intercepted this to begin with (in certain cases) and returned whatever value it can too. Obviously this will not work with any existing code if I were to use this pattern. Is there any other way to solve this issue? Or is this issue at all?

No comments: