Java 8 interface default method doesn't seem to declare property
Asked Answered
P

3

17

In my application I run into a problem that when a getter in a class is defaulted in an interface only (Java 8 feature), there is no Java Beans property as a result. I.e. for normal method invocation it works just as a standard method, but for access through "properties" it suddenly behaves differently...

Here is a test case:

import java.beans.Introspector;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.apache.commons.beanutils.PropertyUtils;

public class test
{
    public static void main (String[] arguments) throws Exception
    {
        // Normal language-level invocation, works fine.
        System.out.println (new Bean1 ().getFoo ());
        System.out.println (new Bean2 ().getFoo ());

        // Printing Java Beans properties; Bean2 doesn't have 'foo' property...
        System.out.println (Arrays.stream (Introspector.getBeanInfo (Bean1.class).getPropertyDescriptors ())
                            .map ((property) -> property.getName ())
                            .collect (Collectors.joining (", ")));
        System.out.println (Arrays.stream (Introspector.getBeanInfo (Bean2.class).getPropertyDescriptors ())
                            .map ((property) -> property.getName ())
                            .collect (Collectors.joining (", ")));

        // First call behaves as expected, second dies with exception.
        System.out.println (PropertyUtils.getProperty (new Bean1 (), "foo"));
        System.out.println (PropertyUtils.getProperty (new Bean2 (), "foo"));
    }

    public interface Foo
    {
        default String getFoo ()
        {
            return "default foo";
        }
    }

    public static class Bean1 implements Foo
    {
        @Override
        public String getFoo ()
        {
            return "special foo";
        }
    }

    public static class Bean2 implements Foo
    { }
}

Result:

special foo
default foo
class, foo
class
special foo
Exception in thread "main" java.lang.NoSuchMethodException: Unknown property 'foo' on class 'class test$Bean2'
        at org.apache.commons.beanutils.PropertyUtilsBean.getSimpleProperty(PropertyUtilsBean.java:1257)
        at org.apache.commons.beanutils.PropertyUtilsBean.getNestedProperty(PropertyUtilsBean.java:808)
        at org.apache.commons.beanutils.PropertyUtilsBean.getProperty(PropertyUtilsBean.java:884)
        at org.apache.commons.beanutils.PropertyUtils.getProperty(PropertyUtils.java:464)
        at test.main(test.java:21)

Questions: do I do something wrong or is it a bug in Java? Is there a workaround other than never using defaulted methods (for getters/setters) in case you might need to access them as a "property" at some point later?

I always hated Java "properties by convention" that tend to break because you sneeze the wrong way.

Pamilapammi answered 29/7, 2015 at 14:34 Comment(6)
Looks like this is covered by JDK-8071693, not yet fixed in any JDK release.Samy
Yes, indeed. Hope they fix it.Pamilapammi
I've been bitten by this bug as well. The OpenJDK bug is scheduled for Java 9 which means we will have to wait until at least september 2016 for it to be fixed. In the meantime I would simply create a delegate method in the class that needs the property.Nicely
@HennoVermeulen: Yeah, the problem is that I use properties really often because of interfacing with EL and a scripting language. And this bug basically made me wary of default methods, because properties (that are not checked by compiler in Java) can break without notice. Especially annoying for moving some standard implementation to default interface methods: such seemingly safe operation can screw something up and there is no good way to tell in advance.Pamilapammi
@StuartMarks: Is it somehow possible to bump that JDK bug? My code is infested with @Override public Foo getFoo () { return MyInterface.super.getFoo (); } because of it...Pamilapammi
JDK-8071693 has now been fixed as part of Java 21.Freudberg
E
3

This seems to be indeed an erroneous omission in the Beans Introspector. Here is a workaround other than not using default methods:

public static void main (String[] arguments) throws Exception {
    testBean(new Bean1());
    System.out.println();
    testBean(new Bean2());
}
static void testBean(Object bean) throws Exception {
    PropertyDescriptor[] pd
        = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors();
    System.out.println(Arrays.stream(pd)
        .map(PropertyDescriptor::getName).collect(Collectors.joining(", ")));
    for(PropertyDescriptor p: pd)
        System.out.println(p.getDisplayName()+": "+p.getReadMethod().invoke(bean));
}
public interface Foo {
    default String getFoo() {
        return "default foo";
    }
}
public static class Bean1 implements Foo {
    @Override
    public String getFoo() {
        return "special foo";
    }
}
public static class Bean2BeanInfo extends SimpleBeanInfo {
    private final BeanInfo ifBeanInfo;
    public Bean2BeanInfo() throws IntrospectionException {
        ifBeanInfo=Introspector.getBeanInfo(Foo.class);
    }
    @Override
    public BeanInfo[] getAdditionalBeanInfo() {
        return new BeanInfo[]{ifBeanInfo};
    }
}
public static class Bean2 implements Foo { }
class, foo
class: class helper.PropTest$Bean1
foo: special foo
class, foo
class: class helper.PropTest$Bean2
foo: default foo
Edee answered 29/7, 2015 at 15:36 Comment(6)
Frankly, this is more inconvenient than never using default methods to begin with.Pamilapammi
Hmm, you said you hate “properties by convention”, so what’s the problem with an explicit beaninfo then? But anyway, Java 8 allows you to create function based property-like objects with single-liners, so I don’t see the point of Reflection based tools for that either. Nevertheless, the only alternative is to wait for Oracle to fix that…Edee
Holger: I would like explicit properties, but if Java had a sane syntax for them, not hacking something on top of existing mechanism. A huge problem with your method is that everything is spread over different places, so you have getters/setters in one place, bean info elsewhere etc.Pamilapammi
By the way, what do you mean with function base property-like objects?Pamilapammi
I don’t see that there is something spread over different places. The Bean2BeanInfo is right at the Bean2 class which it describes and all it says is that Bean2 should incorporate the info for Foo, the same thing Bean2 implements Foo says seven lines below it. The bean class and the bean info class form a pair at one place. That the default methods are located somewhere else is a property of the default methods.Edee
“function based property-like objects” means to have something like class Property<B,V> { String name; Function<B,V> getter; BiConsumer<B, V> setter; } plus the usual stuff, factory methods for it, etc. Depending on the use case, it doesn’t even have to have a name. Then, a property can be declared like readOnly(Bean1::getFoo), assuming an appropriate factory method and you’ll get a Property<Bean1,String> then. As said, declaring all properties of a class can become a single line, producing an array of properties or a Map<String,Property<Bean,?>>, etc…Edee
D
1

I don't know if my answer will be helpful, but I solved similar problem by using BeanUtils.getPropertyDescriptors(clazz) from Spring. It understands default methods.

Dobruja answered 13/10, 2017 at 13:46 Comment(3)
I believe that is incorrect. I assume you are using Apache's BeanUtils library, which contains a method PropertyUtils.getPropertyDescriptors(Class). This method uses the same java.beans.Introspector that is in question (source).Sergius
As I said, my solution was using BeanUtils from Spring instead of one from Apache CommonsDobruja
It's not about accessing the properties myself, it's about letting code that I don't control (EL script implementation) see that there is a property.Pamilapammi
M
0

A quick work-around:

try {
    return PropertyUtils.getProperty(bean, property);
}
catch (NoSuchMethodException e) {
    String getMethod = "get" + property.substring(0, 1).toUpperCase() + property.substring(1);
    return MethodUtils.invokeMethod(bean, getMethod, new Object[]{});
}
Malignancy answered 28/1, 2016 at 14:31 Comment(1)
It doesn't help in my case, because I need properties to be visible from Java EL. I.e. I don't control the place where they are accessed.Pamilapammi

© 2022 - 2024 — McMap. All rights reserved.