Записи Java, которые будут использоваться в качестве значений JSF

#jsf #el #java-record

#jsf #el #java-запись

Вопрос:

Я работаю над простым веб-приложением JSF (в основном для обучения), используя Java 15 и JSF 2.3 плюс PrimeFaces 8, и я использую простую запись Java для моделирования сущности. Приложение не использует какую-либо базу данных.

Мой вопрос в том, можно ли использовать записи Java в качестве значений на страницах xhtml, подобных этому

     <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>
  

Спасибо за вашу помощь!

Комментарии:

1. Не могли бы вы поделиться классом car, чтобы помочь вам лучше?

2. на самом деле класс car, является ли эта запись формой вопроса, но вот она public record Car (String randomId, String randomBrand, int randomYear, String randomColor, int randomPrice, boolean randomSoldState) { }

Ответ №1:

Технически говоря, проблема не в JSF, а в EL. Имя пакета исключения уже намекает на это: javax.el.PropertyNotFoundException . Используемая версия EL еще не распознает записи Java. Jakarta EE 8 связана с Java 8, но функция Java Records была введена в Java 14. Теоретически, он будет изначально поддерживаться только в версии EL, связанной с версией Jakarta EE, связанной с Java 14. Но даже это очень маловероятно, потому что записи Java доступны только как «функция предварительного просмотра» (и, следовательно, по умолчанию вообще не включены).

Возвращаясь к вашей конкретной проблеме, использование синтаксиса выражения метода, как в #{car.randomId()} , действительно работало для меня на WildFly 21. В любом случае разрешение EL всегда можно настроить с помощью пользовательского ELResolver . Вот начальный пример, который проверяет наличие записей на основе Class#isRecord() и собирает поля, доступные через Class#getRecordComponents() свойства as:

 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 amp;amp; 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.Method представляющее средство доступа к записи Java и вызывает его.

Тем не менее, запись Java не подходит для замены полноценного JavaBean, в первую очередь потому, что записи Java неизменяемы. Таким образом, они не могут использоваться как реальные (JPA) объекты, потому что они должны быть изменяемыми. Записи Java наиболее полезны в качестве DTO.

Комментарии:

1. Большое вам спасибо за ваши подробные объяснения и пример кода!

2. Небольшая придирка: return String.class getCommonPropertyType поскольку вы адресуете компоненты записи с помощью строки.

3. И для getFeatureDescriptors : return return RECORD_COMPONENT_CACHE .computeIfAbsent(...).values().map(p -> new java.beans.PropertyDescriptor(p.getName(), p.getAccessor(), null).iterator(); может работать.

4. @Johannes: Абсолютно правильно. Это был просто начальный пример, основанный на существующем javax.el.BeanELResolver . Упомянутые методы фактически никогда не вызываются в течение обычного жизненного цикла JSF, но я обновлю код.

5. Большое спасибо за дополнительные комментарии.

Ответ №2:

После более глубокого изучения трассировки стека и изучения кода для чтения свойств, кажется, что на данный момент чтение свойств из записей Java не реализовано yes.

Возможно, в будущей версии.

Обратите внимание, что я обновлю ответ, как только найду соответствующую информацию по этому вопросу.

Обновление 8.11.2020 К сожалению, я неверно истолковал область действия записей.

В реальном приложении моя сущность была бы сущностью JPA.