JSF拒绝处理深层嵌套的复合组件。不,真的:“JSF1098:[...]这浪费了处理器时间[...]”

问题描述 投票:3回答:1

前提非常简单:我有一个依赖嵌套复合组件(CC)进行渲染的页面布局,即一个JSF页面引用CC,CC本身引用另一个CC,其中包含对第三个CC的调用。 - 来源如下。

现在,当第三个CC想要使用ajax执行bean方法时,这将导致完全自包含的部分更新......没有任何反应。*

Nested JSF CCs: Black: CC that executes a call from attributes. Red: Outer CC that passes its children to blue. Blue: Intermediate that inserts children for red.

我已经广泛搜索了SO和其他地方,并调查了BalusC的有见地的帖子here的所有要点,但我已经空了。跟踪日志记录最终在应用,验证和呈现阶段发出以下消息,导致“空”响应:FINER [javax.enterprise.resource.webcontainer.jsf.context] ... JSF1098:未访问以下clientIds部分遍历后:fooForm:j_idt14:j_idt15:j_idt18。这是浪费处理器时间,可能是由于VDL页面中的错误。

*)这只发生在非常特殊的情况下(虽然这是我的用例的确切定义):

  • 深度嵌套的CC,至少两个父级。
  • 嵌套必须是隐式的,即不同的CC调用另一个,而不仅仅是直接在调用页面内的嵌套标签。
  • “更高”的CC传递了使用<cc:insertChildren />插入“较低”CC的子节点。
  • 执行ajax调用和部分更新的CC包含从CC的属性或clientId动态创建的actions。 (但即使在同一个调用中,也不一定只在同一个组件中。)

所有这些都必须同时满足:如果我在层次结构中使用最里面的CC(或者包括对最终CC的调用的嵌套都在调用页面内),那么一切都有效。如果我使用facet,没问题。如果我删除或硬编码action参数,一切都很好。

(目前在EE6,EAP 6.4上进行测试,但在EAP 7.0上运行的EE7项目中是相同的)

致电页面:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:my="http://java.sun.com/jsf/composite/components/nestedcomponents">

    <h:head>
        <title>Nested composite component test</title>
    </h:head>

    <h:body>
        <h:form id="fooForm">           

        <h2>Works</h2>
        <my:randomString saveBean="#{util}" saveAction="doSomething" />


        <h2>Doesn't</h2>
        <my:containerInsertingAnotherUsingInsertChildren>
            <my:randomString saveBean="#{util}" saveAction="doSomething" />
        </my:containerInsertingAnotherUsingInsertChildren>

        </h:form>
    </h:body>
</html>

Innermost CC:(<my:randomString>,黑色框架; #{util}是一个请求范围的bean,带有单线程虚拟方法)

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

<cc:interface>
    <cc:attribute name="someValue" />
    <!--cc:attribute name="someAction" method-signature="void action()" /-->
    <!--cc:attribute name="someAction" targets="btn" targetAttributeName="action" /-->
    <cc:attribute name="saveBean" />
    <cc:attribute name="saveAction" />
</cc:interface>

<cc:implementation>
    <h:panelGroup layout="block" id="box" style="border: 1px solid black; margin: 3px; padding: 3px;">
        <h:outputText value="#{cc.attrs.id} / #{cc.clientId} / #{util.getRandomString()} " />

        <h:commandLink id="btn" value="save!" action="#{cc.attrs.saveBean[cc.attrs.saveAction]}" >
            <f:ajax render="box" immediate="true" />
        </h:commandLink>
    </h:panelGroup>
</cc:implementation>

</html>

外包装CC:(<my:containerInsertingAnotherUsingInsertChildren>,红框)

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:my="http://java.sun.com/jsf/composite/components/nestedcomponents">

<cc:interface>
</cc:interface>

<cc:implementation>
    <h:panelGroup layout="block" style="border: 1px solid red; margin: 3px; padding: 3px;">
        <my:containerUsingInsertChildren>
            <cc:insertChildren />
        </my:containerUsingInsertChildren>
    </h:panelGroup>
</cc:implementation>

</html>

中级CC:(<my:containerUsingInsertChildren>,蓝框)

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:cc="http://java.sun.com/jsf/composite"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui">

<cc:interface>
</cc:interface>

<cc:implementation>
    <h:panelGroup layout="block" style="border: 1px solid blue; margin: 3px; padding: 3px;">
        <cc:insertChildren />
    </h:panelGroup>
</cc:implementation>

</html>

正如我所写,硬编码调用按预期工作并更新附加的小框。一旦bean方法涉及CC的参数(属性),并且CC在层次结构中足够深,它们就会被跳过。

我很茫然,欢迎提供解决方案或解决方法。

jsf composite-component tree-traversal clientid
1个回答
3
投票

这是由Mojarra中与生成通过<cc:insertChildren>嵌套的复合组件的客户端ID相关的错误引起的。如果为复合组件分配固定ID,如下所示:

<h:form id="form">
    <my:level1 id="level1">
        <my:level3 id="level3" beanInstance="#{bean}" methodName="action" />
    </my:level1>
</h:form>

由此level1.xhtml实施为:

<cc:implementation>
    <my:level2 id="level2">
        <cc:insertChildren />
    </my:level2>
</cc:implementation>

并且level2.xhtml为:

<cc:implementation>
    <cc:insertChildren />
</cc:implementation>

并且level3.xhtml为:

<cc:implementation>
    <h:commandButton id="button" value="Submit #{component.clientId}"
        action="#{cc.attrs.beanInstance[cc.attrs.methodName]}">
        <f:ajax />
    </h:commandButton>
</cc:implementation>

然后你会注意到提交按钮中的#{component.clientId}按照预期说form:level1:level3:button而不是form:level1:level2:level3:button(另见这个相关问题How to find out client ID of component for ajax update/render? Cannot find component with expression "foo" referenced from "bar"的答案)。

这是在树遍历中发现错误的线索。您获得的日志消息实际上是误导性的。

JSF1098:部分遍历后,未访问以下clientId:form:level1:level3:按钮。这是浪费处理器时间,可能是由于VDL页面中的错误。

第一部分是正确的,这确实是技术问题,但是“这是浪费处理器时间并且可能是由于VDL页面中的错误”暗示的潜在原因的假设是不正确的。理论上,只有在访问导致堆栈溢出错误时才会发生这种错误,该错误仅发生在1000左右的深度。这远非如此。

回到错误的客户端ID问题的根本原因,遗憾的是,如果不修复核心JSF实现本身(我已将其报告为issue 4339),这是不容易的。但是,您可以通过提供自定义访问上下文来解决此问题,该上下文提供了要访问的正确子树ID。这里是:

public class PartialVisitContextPatch extends VisitContextWrapper {
    private final VisitContext wrapped;
    private final Pattern separatorCharPattern;

    public PartialVisitContextPatch(VisitContext wrapped) {
        this.wrapped = wrapped;
        char separatorChar = UINamingContainer.getSeparatorChar(FacesContext.getCurrentInstance());
        separatorCharPattern = Pattern.compile(Pattern.quote(Character.toString(separatorChar)));
    }

    @Override
    public VisitContext getWrapped() {
        return wrapped;
    }

    @Override
    public Collection<String> getSubtreeIdsToVisit(UIComponent component) {
        Collection<String> subtreeIdsToVisit = super.getSubtreeIdsToVisit(component);

        if (subtreeIdsToVisit != VisitContext.ALL_IDS) {
            FacesContext context = getFacesContext();
            Map<String, Set<String>> cachedSubtreeIdsToVisit = (Map<String, Set<String>>) context.getAttributes()
                .computeIfAbsent(PartialVisitContextPatch.class.getName(), k -> new HashMap<String, Set<String>>());

            return cachedSubtreeIdsToVisit.computeIfAbsent(component.getClientId(context), k ->
                getIdsToVisit().stream()
                    .flatMap(id -> Arrays.stream(separatorCharPattern.split(id)))
                    .map(childId -> component.findComponent(childId))
                    .filter(Objects::nonNull)
                    .map(child -> child.getClientId(context))
                    .collect(Collectors.toSet())
            );
        }

        return subtreeIdsToVisit;
    }

    public static class Factory extends VisitContextFactory {
        private final VisitContextFactory wrapped;

        public Factory(VisitContextFactory wrapped) {
            this.wrapped = wrapped;
        }

        @Override
        public VisitContextFactory getWrapped() {
            return wrapped;
        }

        @Override
        public VisitContext getVisitContext(FacesContext context, Collection<String> ids, Set<VisitHint> hints) {
            return new PartialVisitContextPatch(getWrapped().getVisitContext(context, ids, hints));
        }
    }
}

默认情况下,当Mojarra在树遍历期间访问<my:level2>并调用getSubtreeIdsToVisit()时,它将获得一个空集,因为组件ID字符串level2不存在于客户端ID字符串form:level1:level3:button中。我们需要覆盖和操纵getSubtreeIdsToVisit(),以便在传入form:level1:level3:button时“正确”返回<my:level2>。这可以通过将客户端ID分解为部分formlevel1level3button并尝试将其作为直接命令来完成给定组件的孩子。

为了让它运行,请在faces-config.xml中将其注册如下:

<factory>
    <visit-context-factory>com.example.PartialVisitContextPatch$Factory</visit-context-factory>
</factory>

说,确保你没有为了模板而滥用复合材料组件。更好地使用标记文件。另见When to use <ui:include>, tag files, composite components and/or custom components?

© www.soinside.com 2019 - 2024. All rights reserved.