如果使用 Random 或 SecureRandom 生成组件 ID,则 JSF 验证器不起作用

JSF Validators do not work if Random or SecureRandom used to generate component ID

当我使用 UUID#randomUUID()(使用 SecureRandom)或 RandomStringUtils#randomAlphabetic(int)(使用 Random)为 HtmlInputText 生成组件 ID 时,验证停止工作。相反,如果我使用任意硬编码字符串(例如 "C5d682a6f")设置组件 ID,则验证会按预期工作。这是代码:

import org.apache.commons.lang3.RandomStringUtils;
import java.util.UUID;
import javax.faces.component.html.HtmlInputText;
import javax.faces.component.html.HtmlMessage;
import javax.faces.component.html.HtmlPanelGrid;

@Model
public class LoginBean
{
    private HtmlPanelGrid panelGrid;
    private String email;
    @PostConstruct void initialize()
    {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        String componentId;
        componentId = "C" + UUID.randomUUID().toString().substring(0, 8);   // Yields something like "C5d682a6f", which should be fine, yet breaks validation.
        //componentId = RandomStringUtils.randomAlphabetic(8);              // Yields something like "zxYBcUYM", which should be fine, yet breaks validation.
        //componentId = "C5d682a6f";                                        // Hard-coding the same exact kind of string generated by UUID#randomUUID() works fine.
        //componentId = "zxYBcUYM";                                         // Hard-coding the same exact kind of string generated by RandomStringUtils#randomAlphabetic(int) 

        HtmlInputText emailFieldComponent = (HtmlInputText)facesContext.getApplication().createComponent(
            facesContext,
            HtmlInputText.COMPONENT_TYPE,
            "javax.faces.Text"
        );
        emailFieldComponent.setId(componentId);
        emailFieldComponent.setValueExpression(
            "value",
            facesContext.getApplication().getExpressionFactory().createValueExpression(
                facesContext.getELContext(),
                "#{loginBean.email}",
                String.class
            )
        );

        // The following validators stop working if UUID#randomUUID() or
        // RandomStringUtils#randomAlphabetic(int) are used to generate componentId.
        emailFieldComponent.setRequired(true);
        emailFieldComponent.addValidator(new EmailValidator());

        HtmlMessage message = (HtmlMessage)facesContext.getApplication().createComponent(
            facesContext,
            HtmlMessage.COMPONENT_TYPE,
            "javax.faces.Message"
        );
        message.setFor(componentId);

        panelGrid = (HtmlPanelGrid)facesContext.getApplication().createComponent(
            facesContext,
            HtmlPanelGrid.COMPONENT_TYPE,
            "javax.faces.Grid"
        );
        panelGrid.setColumns(2);
        panelGrid.getChildren().add(emailFieldComponent);
        panelGrid.getChildren().add(message);
    }
}

对为什么会这样有什么想法吗?我只需要 componentId 是在运行时生成的任意字符串并符合以下约定(来自 UIComponent#setId(String) JavaDoc):

Component identifiers must obey the following syntax restrictions:

 Must not be a zero-length String.
 First character must be a letter or an underscore ('_').
 Subsequent characters must be a letter, a digit, an underscore ('_'), or a dash ('-').

Component identifiers must also obey the following semantic restrictions (note that this restriction is NOT enforced by the setId() implementation):

 The specified identifier must be unique among all the components (including facets) that are descendents of the nearest ancestor UIComponent that is a NamingContainer, or within the scope of the entire component tree if there is no such ancestor that is a NamingContainer.

我的开发环境是Mojarra 2.2.6-jbossorg-4 on Wildfly 8.1.0.Final.

编辑:

因此似乎任何在运行时创建组件 ID 的尝试都会导致验证不会发生。

    componentId = "C" + Long.toHexString(Double.doubleToLongBits(Math.random()));
    componentId = "C" + Long.toHexString(System.currentTimeMillis());
    componentId = "C" + Long.toHexString(new Date().getTime());
    componentId = "C" + new Date().hashCode();

而如果组件 ID 在编译时已知,验证就可以正常进行。

    componentId = "C" + Long.toHexString(Double.doubleToLongBits(Double.MAX_VALUE));

我真的很想明白为什么会这样。

编辑#2:

以下工作正常(谢谢 BalusC),componentId 在运行时生成,这正是我需要的:

    setId(facesContext.getViewRoot().createUniqueId());

我听从了 BalusC 的建议并查看了 UIViewRoot#createUniqueId(),它在引擎盖下看起来像这样:

public String createUniqueId() {
    return createUniqueId(getFacesContext(), null);
}

public String createUniqueId(FacesContext context, String seed) {
    if (seed != null) {
        return UIViewRoot.UNIQUE_ID_PREFIX + seed;
    } else {
        Integer i = (Integer) getStateHelper().get(PropertyKeys.lastId);
        int lastId = ((i != null) ? i : 0);
        getStateHelper().put(PropertyKeys.lastId,  ++lastId);
        return UIViewRoot.UNIQUE_ID_PREFIX + lastId;
    }
}

但我很困惑,因为上面的方法似乎没有在 JSF 视图状态中存储新的客户端 ID。它只会增加 lastId 并更新视图状态中的 lastId。

组件 ID 未存储在 JSF 视图状态中。它们本身就像组件,因此基本上是请求范围的。只有存储在 JSF 视图状态中的内容基本上是视图范围的。 IE。通过 getStateHelper() 方法组成 put/get 的东西。 getId()/setId() 方法不会那样做。

当 JSF 需要处理回发请求时,它将在恢复视图阶段重建视图(即所有组件实例将像 new UIComponent() 等一样重新创建),因此组件将按照您的方式获得不同的客户端 ID。此后,JSF 将使用来自 JSF 视图状态的数据恢复组件树。

然后,当JSF需要处理apply request values阶段时,它会使用client ID作为参数名从HTTP请求参数映射中提取请求参数。但是,由于此客户端 ID 已更改,JSF 无法找到最初提交的值。

这就是这里发生的事情。如何解决是一秒钟。一个好的起点是 UINamingContainer#createUniqueId().