JSF отказывается обрабатывать глубоко вложенный составной компонент. Нет, правда: JSF1098: [] Это пустая трата процессорного времени []

Предпосылка довольно проста: у меня есть макет страницы, который использует вложенные составные компоненты (CC) для выполнения рендеринга, то есть одна страница JSF ссылается на CC, который сам ссылается на другой CC, который содержит вызов третьего CC. - Источник ниже.

Теперь, когда этот третий CC хочет выполнить метод компонента с использованием ajax, что приведет к полностью автономному частичному обновлению... ничего не происходит.*

Вложенные CC JSF: черный: CC, который выполняет вызов из атрибутов. Красный: внешний CC, который передает свои дочерние элементы синему. Синий: промежуточный, который вставляет дочерние элементы для красного.

Я тщательно искал SO и другие места и исследовал все точки проницательного поста BalusC здесь, но я придумал пустой. В конце концов, ведение журнала трассировки обнаружило следующее сообщение на этапах применения, проверки и рендеринга, что привело к «пустому» ответу: FINER [javax.enterprise.resource.webcontainer.jsf.context] ... JSF1098: следующие clientIds не были посещены после частичного обхода: fooForm:j_idt14:j_idt15:j_idt18 . Это пустая трата процессорного времени, которая может быть вызвана ошибкой на странице VDL.

*) Это происходит только при очень специфических обстоятельствах (которые, тем не менее, являются точным определением моего варианта использования):

  • Глубоко вложенные CC, как минимум два родительских уровня.
  • Вложение должно быть неявным, т. е. разные CC вызывают другой, а не просто вложенные теги непосредственно внутри вызывающей страницы.
  • «Высшие» CC передают потомков, которые вставляются в «нижний» CC с помощью <cc:insertChildren />.
  • CC, который выполняет вызов ajax и частичное обновление, содержит actions, динамически создаваемых из атрибутов CC или clientId. (Но даже не обязательно в том же вызове, просто внутри одного и того же компонента.)

Все они должны выполняться одновременно: если я использую самый внутренний CC выше в иерархии (или вложение, включая вызов последнего CC, находится внутри вызывающей страницы), все работает. Если я использую фасеты, нет проблем. Если я удалю или жестко запрограммирую параметр action, все будет хорошо.

(В настоящее время тестируется на EE6, EAP 6.4, но то же самое в проекте EE7, работающем на EAP 7.0)

Страница вызова:

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

Самый внутренний 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 находится достаточно глубоко в иерархии, они просто пропускаются.

Я в недоумении, и решения или обходные пути приветствуются.


person Antares42    schedule 18.02.2018    source источник


Ответы (1)


Это вызвано ошибкой в ​​Mojarra, связанной с генерацией идентификатора клиента составного компонента, вложенного через <cc:insertChildren>. Если вы назначаете составным компонентам фиксированный идентификатор, как в:

<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 в соответствии с ожиданием (см. также ответ на этот связанный вопрос Как узнать идентификатор клиента компонента для обновления/рендеринга ajax? Не удается найти компонент с выражением foo, на которое ссылается панель ).

Это было ключом к поиску ошибки в обходе дерева. Сообщение журнала, которое вы получили, на самом деле вводит в заблуждение.

JSF1098: следующие идентификаторы clientId не были посещены после частичного обхода: form:level1:level3:button. Это пустая трата процессорного времени и может быть вызвана ошибкой на странице VDL.

Первая часть верна, это действительно техническая проблема, но предположение о потенциальной причине, как подразумевается "Это пустая трата процессорного времени и может быть связано с ошибкой на странице VDL", неверно. неправильно. Теоретически это могло произойти только в том случае, если посещение привело к ошибке переполнения стека, что происходит только на глубине около 1000 уровней. Здесь это далеко не так.

Возвращаясь к первопричине проблемы с неправильным идентификатором клиента, к сожалению, это непросто обойти без исправления самой базовой реализации JSF (я сообщил об этом как выпуск 4339). Однако вы можете обойти это, предоставив пользовательский контекст посещения, который предоставляет правильные идентификаторы поддерева для посещения. Вот:

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(), он получает пустой набор, потому что строка идентификатора компонента level2 отсутствует в строке идентификатора клиента form:level1:level3:button. Нам нужно переопределить и манипулировать getSubtreeIdsToVisit() таким образом, чтобы он «правильно» возвращал form:level1:level3:button при передаче <my:level2>. Это можно сделать, разбив идентификатор клиента на части form, level1, level3 и button и попытавшись найти его как прямой. дочерний элемент данного компонента.

Чтобы запустить его, зарегистрируйте его, как показано ниже, в faces-config.xml:

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

Сказав это, убедитесь, что вы не злоупотребляете составными компонентами ради шаблонов. Вместо этого лучше использовать файлы тегов. См. также Когда использовать ‹ui: include›, файлы тегов, составные компоненты и/или пользовательские компоненты?

person BalusC    schedule 26.02.2018
comment
Сладкий. Да, я копался в исходном коде и нашел, где в PartialViewContextImpl создается сообщение, но нашел время, чтобы проследить, где оно на самом деле пренебрегает посещением частей дерева. И действительно, основываясь на вашем связанном посте, я уже реорганизовал некоторые из своих компонентов в файлы тегов. Как всегда, спасибо за понимание и советы! - person Antares42; 26.02.2018