带有默认方法的接口中的Getter(JSF)

8

我有一个带有以下默认方法的接口:

default Integer getCurrentYear() {return DateUtil.getYear();}

我有一个实现这个接口的控制器,但它没有覆盖这个方法。
public class NotifyController implements INotifyController

我正在尝试通过以下方式从我的xhtml访问此方法:
#{notifyController.currentYear}

然而,当我打开屏幕时,出现以下错误:
The class 'br.com.viasoft.controller.notify.notifyController' does not have the property 'anoAtual'

如果我从我的控制器实例访问此方法,它会返回正确的值,但是当我尝试将其作为“属性”从我的xhtml访问时,就会出现此错误。

有没有一种方法可以从我的控制器引用中访问此接口属性而无需实现该方法?


2
你为什么认为关于“anoAtual”的错误与名为getCurrentYear()的方法有任何关系? - Mike Nakis
3
它与Java SE或JSF有什么关联? - Kukeltje
1
@MikeNakis,anoAtual是葡萄牙语中的currentYear。OP已经翻译了他的代码,但没有翻译他的异常。可能他实际的方法是getAnoActual。 - RubioRic
1
这可以通过使用自定义EL解析器或将属性视为方法来解决。 - BalusC
3个回答

3
这可能被视为一个bug,或者有人认为不支持默认方法作为属性是一个决定。
在JDK8中,请参见java.beans.Introspector.getPublicDeclaredMethods(Class<?>),或者在JDK13中,请参见com.sun.beans.introspect.MethodInfo.get(Class<?>)的第if (!method.getDeclaringClass().equals(clz))行。
只有超类(递归到Object,但不包括接口)会被添加,参见java.beans.Introspector.Introspector(Class<?>, Class<?>, int)设置superBeanInfo时。
解决方案:
  • 在您的情况下,使用EL方法调用语法(即不是属性访问):#{notifyController.getCurrentYear()}
    缺点: 您必须更改JSF代码,并且必须考虑每个用途是否可能是默认方法。此外,重构强制要求进行在运行时才能识别的更改,而不是编译器。

  • 创建一个EL解析器,以通用方式支持默认方法。但这应该像标准的java.beans.Introspector一样使用良好的内部缓存,以不降低EL解析的速度。
    请参见"Property not found on type" when using interface default methods in JSP EL,了解基本示例(没有缓存)。

  • 如果只有少数类/接口受到影响,只需创建小的BeanInfo类。
    下面的代码示例显示了这一点(基于您的示例)。
    缺点: 必须为每个实现此类接口的类(在JSF / EL中使用)创建单独的类。
    还请参阅:Default method in interface in Java 8 and Bean Info Introspector


=> 使用默认方法静态getBeanInfo()接口
=> 对于扩展接口的每个类,使用简单明了的BeanInfo

interface INotifyController {
    default Integer getCurrentYear() { ... }
    default boolean isAHappyYear() { ... }
    default void setSomething(String param) { ... }

    /** Support for JSF-EL/Beans to get default methods. */
    static java.beans.BeanInfo[] getBeanInfo() {
        try {
            java.beans.BeanInfo info = java.beans.Introspector.getBeanInfo(INotifyController.class);
            if (info != null)  return new java.beans.BeanInfo[] { info };
        } catch (java.beans.IntrospectionException e) {
            //nothing to do
        }
        return null;
    }

}

public class NotifyController implements INotifyController {
    // your class implementation
    ...
}


// must be a public class and thus in its own file
public class NotifyControllerBeanInfo extends java.beans.SimpleBeanInfo {
    @Override
    public java.beans.BeanInfo[] getAdditionalBeanInfo() {
        return INotifyController.getBeanInfo();
    }
}

2
我发现这个问题将在Jakarta EE 10中得到解决。 https://github.com/eclipse-ee4j/el-ri/issues/43 在Jakarta EE 10之前,您可以使用自定义EL解析器。
package ru.example.el;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ELResolver;
import java.beans.*;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class DefaultMethodELResolver extends ELResolver {
    private static final Map<Class<?>, BeanProperties> properties = new ConcurrentHashMap<>();

    @Override
    public Object getValue(ELContext context, Object base, Object property) {
        if (base == null || property == null) {
            return null;
        }

        BeanProperty beanProperty = getBeanProperty(base, property);
        if (beanProperty != null) {
            Method method = beanProperty.getReadMethod();
            if (method == null) {
                throw new ELException(String.format("Read method for property '%s' not found", property));
            }

            Object value;
            try {
                value = method.invoke(base);
                context.setPropertyResolved(base, property);
            } catch (Exception e) {
                throw new ELException(String.format("Read error for property '%s' in class '%s'", property, base.getClass()), e);
            }

            return value;
        }

        return null;
    }

    @Override
    public Class<?> getType(ELContext context, Object base, Object property) {
        if (base == null || property == null) {
            return null;
        }

        BeanProperty beanProperty = getBeanProperty(base, property);
        if (beanProperty != null) {
            context.setPropertyResolved(true);
            return beanProperty.getPropertyType();
        }

        return null;
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (base == null || property == null) {
            return;
        }

        BeanProperty beanProperty = getBeanProperty(base, property);
        if (beanProperty != null) {
            Method method = beanProperty.getWriteMethod();
            if (method == null) {
                throw new ELException(String.format("Write method for property '%s' not found", property));
            }

            try {
                method.invoke(base, value);
                context.setPropertyResolved(base, property);
            } catch (Exception e) {
                throw new ELException(String.format("Write error for property '%s' in class '%s'", property, base.getClass()), e);
            }
        }
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (base == null || property == null) {
            return false;
        }

        BeanProperty beanProperty = getBeanProperty(base, property);
        if (beanProperty != null) {
            context.setPropertyResolved(true);
            return beanProperty.isReadOnly();
        }

        return false;
    }

    @Override
    public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context, Object base) {
        return null;
    }

    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        return Object.class;
    }

    private BeanProperty getBeanProperty(Object base, Object property) {
        return properties.computeIfAbsent(base.getClass(), BeanProperties::new)
                .getBeanProperty(property);
    }

    private static final class BeanProperties {
        private final Map<String, BeanProperty> propertyByName = new HashMap<>();

        public BeanProperties(Class<?> cls) {
            try {
                scanInterfaces(cls);
            } catch (IntrospectionException e) {
                throw new ELException(e);
            }
        }

        private void scanInterfaces(Class<?> cls) throws IntrospectionException {
            for (Class<?> ifc : cls.getInterfaces()) {
                processInterface(ifc);
            }

            Class<?> superclass = cls.getSuperclass();
            if (superclass != null) {
                scanInterfaces(superclass);
            }
        }

        private void processInterface(Class<?> ifc) throws IntrospectionException {
            BeanInfo info = Introspector.getBeanInfo(ifc);
            for (PropertyDescriptor propertyDescriptor : info.getPropertyDescriptors()) {
                String propertyName = propertyDescriptor.getName();
                BeanProperty beanProperty = propertyByName
                        .computeIfAbsent(propertyName, key -> new BeanProperty(propertyDescriptor.getPropertyType()));

                if (beanProperty.getReadMethod() == null && propertyDescriptor.getReadMethod() != null) {
                    beanProperty.setReadMethod(propertyDescriptor.getReadMethod());
                }

                if (beanProperty.getWriteMethod() == null && propertyDescriptor.getWriteMethod() != null) {
                    beanProperty.setWriteMethod(propertyDescriptor.getWriteMethod());
                }
            }

            for (Class<?> parentIfc : ifc.getInterfaces()) {
                processInterface(parentIfc);
            }
        }

        public BeanProperty getBeanProperty(Object property) {
            return propertyByName.get(property.toString());
        }
    }

    private static final class BeanProperty {
        private final Class<?> propertyType;
        private Method readMethod;
        private Method writeMethod;

        public BeanProperty(Class<?> propertyType) {
            this.propertyType = propertyType;
        }

        public Class<?> getPropertyType() {
            return propertyType;
        }

        public boolean isReadOnly() {
            return getWriteMethod() == null;
        }

        public Method getReadMethod() {
            return readMethod;
        }

        public void setReadMethod(Method readMethod) {
            this.readMethod = readMethod;
        }

        public Method getWriteMethod() {
            return writeMethod;
        }

        public void setWriteMethod(Method writeMethod) {
            this.writeMethod = writeMethod;
        }
    }
}

您需要在faces-config.xml中注册EL解析器。

<?xml version="1.0" encoding="utf-8"?>
<faces-config version="2.3" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd">
    <name>el_resolver</name>

    <application>
        <el-resolver>ru.example.el.DefaultMethodELResolver</el-resolver>
    </application>

</faces-config>

感谢您的代码,但它并不能解决所有情况下的问题,因为它只会获取层次结构中的第一个PropertyDescriptor,如果该描述符或接口仅具有写入方法,而另一个接口或超类具有读取方法,则会出现异常read property not available。我通过向BeanProperty类添加PropertyDescriptor列表并填充所有这些列表来解决了这个问题。此外,检查是否有任何可用的读取或写入描述符,而不仅仅是第一个。略微更改了层次结构方法,但不确定是否必要。 - djmj
感谢您的反馈@djmj。我已经修复了我的答案中的代码。我添加了一个名为processInterface的方法,它递归地处理所有接口层次结构。 - mnyakushev

0

由于这个 bug 与 JDK 相关,您需要在需要该属性的类中创建一个委托方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接