views:

582

answers:

7

While setting up Hudson for continous integration testing (on a JeOS server), I've come across some strange behaviour I'm hoping the fine people at SO can explain to me.

Our unit tests depend heavily on the use of domain objects, with lots of properties that must be set (due to null constraints in the database). In order to keep our tests readable, we have created a class InstantiationUtils that can instantiate an object and set a series of properties through reflection:

public static <T> T newInstance(final Class<T> type, final KeyValuePair<?>... propertyValues) {

    return ReflectionUtils.reflectionOperation(new ReflectionOperation<T>() {

        @Override
        public T perform() throws Exception {

            T object = type.newInstance();
            for (KeyValuePair<?> propertyValue : propertyValues) {

                String propertyName = propertyValue.getKey();
                Object value = propertyValue.getValue();
                String setterName = "set" + StringUtils.capitalize(propertyName);
                ReflectionUtils.invoke(object, setterName, value);
            }
            return object;
        }
    });
}

public static void invoke(final Object target, final String methodName, final Object... params) {

    List<Class<?>> parameterTypes = ListUtils.map(asList(params), "class");
    Class<?> targetClass = target.getClass();
    Method method = MethodUtils.getMatchingAccessibleMethod(targetClass, methodName,
        parameterTypes.toArray(new Class<?>[] {}));
    invoke(target, method, params);
}

public class Foo {
    private String foo;

    public void setFoo(final String foo) {
        this.foo = foo;
    }
}

public class Bar extends Foo {
    private String bar;

    public void setBar(final String bar) {
       this.bar = bar;
    }
}

The person who wrote this code unfortunately no longer works for us, but as far as I can see, there is nothing wrong with it. Which is also true for Windows - we use InstantiationUtils throughout our unit tests without any problems.

Linux, however, is different. It turns out that in Linux, the newInstance() method only works for direct (i.e. not inherited) members of the class we want to instantiate.

InstantiationUtils.newInstance(Bar.class, "bar", "12345"); will work, while InstantiationUtils.newInstance(Bar.class, "foo", "98765"); will fail on Linux, with the following exception:

xxx.xxx.xxx.ReflectionUtils$ReflectionException: java.lang.NoSuchMethodException: Property 'foo' has no setter method

On Windows, both calls will work (I know the newInstance signature doesn't match; we have several overloaded newInstance() methods that convert the parameters to KeyValuePairs).

I had a hard time accepting that inherited public methods are treated differently, so I have tested this in all ways I can think of. And it always ends up with the conclusion that under Linux, at least with the above usage of Reflection, we can't access public inherited methods.

On Windows, I use Sun's JRE 1.6.0.11, in Linux it's also Sun, but version 1.6.0.7.

Can anyone confirm if this is correct? Or is the Reflection usage somehow flawed?

+2  A: 

Could it be that the SecurityManager settings are different between the different Java runtimes?

Certainly I doubt that this is a platform issue - it is almost certainly something to do with the JRE version/setup between the two environments

You really need to post the source code to MethodUtils.getMatchingAccessibleMethod

oxbow_lakes
I have thought about that, but usually when there's a SecurityManager issue, I get a very specific exception stating just that. In this case, it's NoSuchMethodException, and no sign of SecurityManager in the stack trace.
Nils-Petter Nilsen
That's a good point
oxbow_lakes
A: 

Which JVM do you use on Linux, Sun, GCJ etc? If you use something else than Sun's JVM, you could try installing it and see if that makes a difference.

Nils Weinander
Windows is Sun 1.6.0.11, Linux is Sun 1.6.0.7. I'll try upgrading both to 1.6.0.12 and see what happens.
Nils-Petter Nilsen
Same behaviour with 1.6.0.12, still fails on Linux.
Nils-Petter Nilsen
A: 

Do you have different locales? StringUtils.capitalize(propertyName) may be producing different output.

Stephen Denne
The locales are actually different. But I tested the output of capitalize(), and it appears to be correct.
Nils-Petter Nilsen
Well, what did you test it on? Admittedly it's unlikely, but there could be a property using a non-ASCII letter with weird capitalization rules, such as the Turkish dotless i
Michael Borgwardt
+3  A: 

You are using MethodUtils, and it has some limitations :

Known Limitations

Accessing Public Methods In A Default Access Superclass

There is an issue when invoking public methods contained in a default access superclass. Reflection locates these methods fine and correctly assigns them as public. However, an IllegalAccessException is thrown if the method is invoked.

Another thing to check is if the setFoo() method is overloaded, this may also cause the problem...

pgras
All good points :-) But setFoo() is not overloaded, and all classes in the hierarchy are public.
Nils-Petter Nilsen
A: 

Have you checked your CLASSPATH ? Are you picking up different versions of the class you want to instantiate depending on which platform you're on ? (e.g. old codebases lying around etc.?)

Brian Agnew
+1  A: 

A couple of things to try...

On Linux, try comping the code without a reflective call to getFoo() - if it will not compile then reflection has no hope of working (well it does depending on how yoiu setup the CLASSAPTH at runtime...)

Try adding the code below and run it on both Linux and Windows.

final Properties properties;

properties = System.getProperties();

for(final Entry<Object, Object> entry : properties.entrySet())
{
    System.out.println(entry.getKey() + " " + entry.getValue());
}

The check the output to make sure that you are using the smae JDK/JRE. Also check to make sure that the classpath is correct so that you are actually loading what you think you are loading.

TofuBeer
+1  A: 

Mystery partially solved:

MethodUtils.getMatchingAccessibleMethod() apparently works differently on Linux and Windows.

By using MethodUtils.getAccessibleMethod() instead, it works. Why, I don't know, but I'm guessing that MethodUtils somehow misinterprets the parameter list when figuring out what signature the Method should have.

I'd like to spend more time investigating this, but as always there are things to do and projects to deliver, so I just have to accept that getAccessibleMethod works, and move on :-)

Thanks to everyone for their input!

Nils-Petter Nilsen