转换错误:设置“null Converter”的值 - 为什么在JSF中需要转换器?

43

我不太了解如何在JSF 2中有效地使用POJO/entity进行选择。例如,我正在尝试通过以下下拉框选择Warehouse实体:

<h:selectOneMenu value="#{bean.selectedWarehouse}">
    <f:selectItem itemLabel="Choose one .." itemValue="#{null}" />
    <f:selectItems value="#{bean.availableWarehouses}" />
</h:selectOneMenu>

以下是管理的Bean:

@Named
@ViewScoped
public class Bean {

    private Warehouse selectedWarehouse;
    private List<SelectItem> availableWarehouses;

    // ...

    @PostConstruct
    public void init() {
        // ...

        availableWarehouses = new ArrayList<>();

        for (Warehouse warehouse : warehouseService.listAll()) {
            availableWarehouses.add(new SelectItem(warehouse, warehouse.getName()));
        }
    }

    // ...
}
请注意,我使用整个Warehouse实体作为SelectItem的值。

当我提交表单时,将失败并显示以下面向用户的消息:

Conversion Error setting value 'com.example.Warehouse@cafebabe' for 'null Converter'.

我希望JSF在我用SelectItem包装实体时可以正确地将Warehouse对象设置到我的托管bean中。将实体包装在SelectItem中旨在跳过为实体创建Converter

每当我想在我的<h:selectOneMenu>中使用实体时,我真的必须使用一个Converter吗?对于JSF而言,它应该能够从可用项列表中提取所选项。如果我真的必须使用转换器,那么实际操作是什么?到目前为止,我想到了这个:

  1. 为实体创建Converter实现。
  2. 覆盖getAsString()。我认为我不需要这个,因为SelectItem的标签属性将用于显示下拉选项标签。
  3. 覆盖getAsObject()。我认为这将用于根据托管bean中定义的所选字段类型返回正确的SelectItem或实体。

getAsObject()让我感到困惑。如何高效地做到这一点?有了字符串值,我如何获取关联的实体对象?我应该基于字符串值从服务对象查询实体对象并返回实体吗?还是我可以以某种方式访问形成选择项的实体列表,循环它们以找到正确的实体,然后返回实体?

这个问题的正常解决方案是什么?

3个回答

79

介绍

JSF生成HTML。在Java术语中,HTML基本上是一个大的String。为了在HTML中表示Java对象,它们必须转换为String。此外,当提交HTML表单时,提交的值被视为HTTP请求参数中的String。在幕后,JSF从HttpServletRequest#getParameter()中提取它们,该方法返回String

要在非标准Java对象(即EL具有内置转换的StringNumberBoolean,或JSF提供内置<f:convertDateTime>标记的Date/LocalDate/ZonedDateTime之外的对象)之间进行转换,您确实需要提供自定义ConverterSelectItem没有任何特殊用途。它只是JSF 1.x遗留下来的东西,在那个版本中不可能直接向<f:selectItems>提供例如List<Warehouse>。它也没有特殊的标签和转换处理。

getAsString()

您需要实现getAsString()方法,以便所需的Java对象被表示为一个唯一String表示形式,该表示形式可用作HTTP请求参数。通常在此处使用技术ID(数据库主键)。

public String getAsString(FacesContext context, UIComponent component, Object modelValue) {
    if (modelValue == null) {
        return ""; // Never return null here!
    }

    if (modelValue instanceof Warehouse) {
        return String.valueOf(((Warehouse) modelValue).getId());
    } else {
        throw new ConverterException(new FacesMessage(modelValue + " is not a valid Warehouse"));
    }
}

请注意,如果模型值为null/空,则返回空字符串是重要的,并且javadoc要求如此:
返回:如果值为null,则为零长度字符串,否则为转换结果
否则,生成的

非常感谢您提供详细的答案,它解决了我的问题!通用的JSF实体转换器非常好! - Bertie
你好@BalusC,我已经实现了这个解决方案并且它可以工作,但是我发现一个问题。如果我尝试将自定义转换器文件移动到一个单独的bean中,那么当我重新加载页面时,它会显示“表达式错误:找不到名称为MyCustomCoverter的对象”。所以这个文本<f:converter converterId="MyCustomConverter"/>无法定位转换器。现在,我该如何让它工作? - Joe Almore
我漏掉了 <f:converter converterId="ContactConverter" /> 这一部分。感谢 @BalusC 指引我正确的方向。 - Roland

4

使用ABaseEntity和标识符的JSF通用转换器示例:

ABaseEntity.java

public abstract class ABaseEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    public abstract Long getIdentifier();
}

SelectItemToEntityConverter.java

@FacesConverter(value = "SelectItemToEntityConverter")
public class SelectItemToEntityConverter implements Converter {

    @Override
    public Object getAsObject(FacesContext ctx, UIComponent comp, String value) {
        Object o = null;
        if (!(value == null || value.isEmpty())) {
            o = this.getSelectedItemAsEntity(comp, value);
        }
        return o;
    }

    @Override
    public String getAsString(FacesContext ctx, UIComponent comp, Object value) {
        String s = "";
        if (value != null) {
            s = ((ABaseEntity) value).getIdentifier().toString();
        }
        return s;
    }

    private ABaseEntity getSelectedItemAsEntity(UIComponent comp, String value) {
        ABaseEntity item = null;

        List<ABaseEntity> selectItems = null;
        for (UIComponent uic : comp.getChildren()) {
            if (uic instanceof UISelectItems) {
                Long itemId = Long.valueOf(value);
                selectItems = (List<ABaseEntity>) ((UISelectItems) uic).getValue();

                if (itemId != null && selectItems != null && !selectItems.isEmpty()) {
                    Predicate<ABaseEntity> predicate = i -> i.getIdentifier().equals(itemId);
                    item = selectItems.stream().filter(predicate).findFirst().orElse(null);
                }
            }
        }

        return item;
    }
}

使用方法:

<p:selectOneMenu id="somItems" value="#{exampleBean.selectedItem}" converter="SelectItemToEntityConverter">
    <f:selectItem itemLabel="< select item >" itemValue="#{null}"/>
    <f:selectItems value="#{exampleBean.availableItems}" var="item" itemLabel="${item.identifier}" itemValue="#{item}"/>
</p:selectOneMenu>

这个问题已经在原始答案中引用了另一个问答:https://dev59.com/4HPYa4cB1Zd3GeqPndDk - Kukeltje
不同之处在于不需要进行服务和数据库调用。 - proxymo
请解释更详细一些。 - Kukeltje
不要忘记,在这种情况下,availableItems 应该是一个列表。我们使用了 HashMap 并将其更改为 List。 - Andrei
这个例子不能与 p:autoComplete 一起使用。 - Jasper de Vries

0
我通过将<h:selectOneMenu..中的"value"类型更改为String来实现这一点。

这并不是一个真正的解决方案。你需要在某个时候将该字符串转换为一个对象。 - Jasper de Vries

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