1

我正在使用 Java 15 和 JSF 2.3 以及 PrimeFaces 8 开发一个简单的 JSF Web 应用程序(主要用于学习),并且我正在使用一个简单的 Java 记录来对实体进行建模。该应用程序不使用任何数据库。

我的问题是是否可以像这样在 xhtml 页面中使用 java 记录作为值

    <p:column headerText="Id">
        <h:outputText value="#{car.randomId}" />
    </p:column>

因为我收到以下错误 javax.el.PropertyNotFoundException: The class 'com.company.Car' does not have the property 'id'.。我试过把car.year(),但它没有用。记录的定义是这样的

public record Car (String randomId, String randomBrand, int randomYear, String randomColor, int randomPrice, boolean randomSoldState) {
}

在 pom.xml 中,我使用的是以下 api

        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-api</artifactId>
            <version>8.0.0</version>
            <scope>provided</scope>
        </dependency>

谢谢您的帮助!

4

2 回答 2

8

从技术上讲,问题不在于 JSF,而在于 EL。异常的包名已经暗示了这一点:javax.el.PropertyNotFoundException. 正在使用的 EL 版本还不能识别 Java 记录。Jakarta EE 8 与 Java 8 相关联,但 Java Records 功能是在 Java 14 中引入的。理论上,它最早只会在与 Java 14 相关的 Jakarta EE 版本相关的 EL 版本中得到本地支持。但即使这样也不太可能因为 Java Records 仅作为“预览功能”提供(因此默认情况下根本不启用)。

回到您的具体问题,#{car.randomId()}在 WildFly 21 上使用方法表达式语法对我来说确实有效。无论如何,EL 解析始终可以使用自定义ELResolver. 这是一个启动示例,它根据作为属性检查Class#isRecord()并收集可用字段:Class#getRecordComponents()

public class RecordELResolver extends ELResolver {

    private static final Map<Class<?>, Map<String, PropertyDescriptor>> RECORD_PROPERTY_DESCRIPTOR_CACHE = new ConcurrentHashMap<>();

    private static boolean isRecord(Object base) {
        return base != null && base.getClass().isRecord();
    }
    
    private static Map<String, PropertyDescriptor> getRecordPropertyDescriptors(Object base) {
        return RECORD_PROPERTY_DESCRIPTOR_CACHE
            .computeIfAbsent(base.getClass(), clazz -> Arrays
                .stream(clazz.getRecordComponents())
                .collect(Collectors
                    .toMap(RecordComponent::getName, recordComponent -> {
                        try {
                            return new PropertyDescriptor(recordComponent.getName(), recordComponent.getAccessor(), null);
                        }
                        catch (IntrospectionException e) {
                            throw new IllegalStateException(e);
                        }
                    })));
    }
    
    private static PropertyDescriptor getRecordPropertyDescriptor(Object base, Object property) {
        PropertyDescriptor descriptor = getRecordPropertyDescriptors(base).get(property);
        
        if (descriptor == null) {
            throw new PropertyNotFoundException("The record '" + base.getClass().getName() + "' does not have the field '" + property + "'.");
        }

        return descriptor;
    }

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

        PropertyDescriptor descriptor = getRecordPropertyDescriptor(base, property);

        try {
            Object value = descriptor.getReadMethod().invoke(base);
            context.setPropertyResolved(base, property);
            return value;
        }
        catch (Exception e) {
            throw new ELException(e);
        }
    }

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

        PropertyDescriptor descriptor = getRecordPropertyDescriptor(base, property);
        context.setPropertyResolved(true);
        return descriptor.getPropertyType();
    }

    @Override
    public Class<?> getCommonPropertyType(ELContext context, Object base) {
        if (!isRecord(base)) {
            return null;
        }

        return String.class;
    }

    @Override
    public boolean isReadOnly(ELContext context, Object base, Object property) {
        if (!isRecord(base)) {
            return false;
        }

        getRecordPropertyDescriptor(base, property); // Forces PropertyNotFoundException if necessary.
        context.setPropertyResolved(true);
        return true;
    }

    @Override
    public void setValue(ELContext context, Object base, Object property, Object value) {
        if (!isRecord(base)) {
            return;
        }

        throw new PropertyNotWritableException("Java Records are immutable");
    }

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

        Map rawDescriptors = getRecordPropertyDescriptors(base);
        return rawDescriptors.values().iterator();
    }

}

为了让它工作,注册它faces-config.xml如下:

<application>
    <el-resolver>com.example.RecordELResolver</el-resolver>
</application>

真正的工作是在getValue()方法中完成的。它基本上定位java.lang.reflect.MethodJava Record 的代表访问器并调用它。

也就是说,Java Record 不适合替代完全有价值的 JavaBean,主要是因为 Java Records 是不可变的。因此它们不能用作真实(JPA)实体,因为它们应该是可变的。Java 记录最多可用作 DTO。

于 2020-11-08T19:20:57.513 回答
0

在深入挖掘堆栈跟踪并检查用于读取属性的代码之后,目前似乎还没有实现从 java 记录中读取属性。

也许在未来的版本中。

请注意,我会在找到有关此主题的相关信息后立即更新答案。

更新 8.11.2020 不幸的是,我误解了记录的范围。

在现实世界的应用程序中,我的实体将是 JPA 实体。

于 2020-11-08T08:39:58.410 回答