ui:repeat 对每个 ajax 请求计算的值表达式

ui:repeat value expression evaluated on every ajax request

我有一个表单非常简单的页面,它提交 ajax 仅针对同一表单中的组件的请求。在同一个页面中(但在表单之外),还有一个 ui:repeat 迭代从 request scoped managed bean 返回的数组(假设有一个产品类别列表) .这个 bean 没有绑定到表单中的属性,并且除了 ui:repeat 标记的 value 属性之外不能以任何其他方式访问。我不明白为什么 JSF 需要在每个 ajax 回发时重新创建请求范围的 bean,只是 就好像 我要求呈现这个外部 ui:repeat (它有与表单无关)以及表单中的某些组件。

这是一个错误吗?或者这是一种预期的行为?当然,我可以将 bean 注释为 ViewScoped,但我看不出将类别存储在视图范围中的原因,因为它们在回发之间是完全静态的。

我发现的另一个 solution/workaround 是仅在非 ajax 请求的情况下呈现 ui:repeat

<ul>
    <ui:repeat value="#{someRequestScopedBean.categories}" var="category" rendered="#{not facesContext.partialViewContext.ajaxRequest}">
        <li>#{category.name}</li>
    </ui:repeat>
</ul>

不过不知道会不会出问题,看起来不是很清楚

测试用例

index.xhtml:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:f="http://xmlns.jcp.org/jsf/core">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        <h1>Test page</h1>
        <p>Random jokes</p>
        <ul>
            <ui:repeat value="#{oneLiners.list}" var="oneliner">
                <li>#{oneliner}</li>
            </ui:repeat>
        </ul>
        <h:form>
            <h:selectOneMenu value="#{backingBean.greeting}" hideNoSelectionOption="true">
                <f:selectItem value="#{null}" itemLabel="Select a greeting..." noSelectionOption="true"/>
                <f:selectItems value="#{backingBean.greetings}"/>
                <f:ajax render="@this btn"/>
            </h:selectOneMenu>
            <h:commandButton id="btn" value="Say Hello!" disabled="#{empty backingBean.greeting}">
                <f:ajax render="otxt"/>
            </h:commandButton>
            <h:outputText id="otxt" value="#{backingBean.greeting}, Maurizio!" style="display: #{empty backingBean.greeting ? 'none' : 'block'}"/>
        </h:form>
    </h:body>
</html>

请求作用域 bean:

package testuirepajax;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;

/**
 *
 * @author maurizio
 */
@ManagedBean
@RequestScoped
public class OneLiners {
    private String[] list;

    public OneLiners() {
        System.out.println("testuirepajax.OneLiners.<init>()");
        list = new String[] {
            "Life is wonderful. Without it we'd all be dead.",
            "Daddy, why doesn't this magnet pick up this floppy disk?",
            "Daddy, what does FORMATTING DRIVE C mean?",
            "Never forget: 2 + 2 = 5 for extremely large values of 2.",
            "C:\ is the root of all directories."
        };
    }

    public String[] getList() {
        System.out.println("testuirepajax.OneLiners.getList()");
        return list;
    }
}

表单支持 bean:

package testuirepajax;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

/**
 *
 * @author maurizio
 */
@ManagedBean
@ViewScoped
public class BackingBean {

    private String[] greetings;
    private String greeting;

    public BackingBean() {
        System.out.println("testuirepajax.BackingBean.<init>()");
        greetings = new String[] {
          "Hello", "Hi", "Good morning", "Good evening", "Good night"
        };
    }

    public String[] getGreetings() {
        return greetings;
    }

    public void setGreeting(String greeting) {
        this.greeting = greeting;
    }

    public String getGreeting() {
        return greeting;
    }
}

检查容器的输出。使用 Payara 服务器(搭载 Mojarra 2.2.12),我看到如下行:

Informazioni:   testuirepajax.OneLiners.<init>()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.<init>()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()
Informazioni:   testuirepajax.OneLiners.getList()

从菜单中选择元素或单击 "Say Hello!" 按钮时。

我在 getList() 方法上放置了一个断点,并在回发期间 "unnecessarily" 命中时检查了调用堆栈,以了解谁和为什么:

Daemon Thread [http-nio-8088-exec-5] (Suspended (breakpoint at line 23 in OneLiners))   
    owns: NioChannel  (id=83)   
    OneLiners.getList() line: 23    
    NativeMethodAccessorImpl.invoke0(Method, Object, Object[]) line: not available [native method]  
    NativeMethodAccessorImpl.invoke(Object, Object[]) line: 62  
    DelegatingMethodAccessorImpl.invoke(Object, Object[]) line: 43  
    Method.invoke(Object, Object...) line: 497  
    BeanELResolver.getValue(ELContext, Object, Object) line: 97 
    DemuxCompositeELResolver._getValue(int, ELResolver[], ELContext, Object, Object) line: 176  
    DemuxCompositeELResolver.getValue(ELContext, Object, Object) line: 203  
    AstValue.getValue(EvaluationContext) line: 169  
    ValueExpressionImpl.getValue(ELContext) line: 184   
    TagValueExpression.getValue(ELContext) line: 109    
    UIRepeat.getValue() line: 279   
    UIRepeat.getDataModel() line: 255   
    UIRepeat.visitTree(VisitContext, VisitCallback) line: 727   
    HtmlBody(UIComponent).visitTree(VisitContext, VisitCallback) line: 1700 
    UIViewRoot(UIComponent).visitTree(VisitContext, VisitCallback) line: 1700   
    PartialViewContextImpl.processComponents(UIComponent, PhaseId, Collection<String>, FacesContext) line: 403  
    PartialViewContextImpl.processPartial(PhaseId) line: 266    
    UIViewRoot.processDecodes(FacesContext) line: 927   
    ApplyRequestValuesPhase.execute(FacesContext) line: 78  
    ApplyRequestValuesPhase(Phase).doPhase(FacesContext, Lifecycle, ListIterator<PhaseListener>) line: 101  
    LifecycleImpl.execute(FacesContext) line: 198   
    FacesServlet.service(ServletRequest, ServletResponse) line: 658 
    ApplicationFilterChain.internalDoFilter(ServletRequest, ServletResponse) line: 291  
    ApplicationFilterChain.doFilter(ServletRequest, ServletResponse) line: 206  
    WsFilter.doFilter(ServletRequest, ServletResponse, FilterChain) line: 52    
    ApplicationFilterChain.internalDoFilter(ServletRequest, ServletResponse) line: 239  
    ApplicationFilterChain.doFilter(ServletRequest, ServletResponse) line: 206  
    StandardWrapperValve.invoke(Request, Response) line: 212    
    StandardContextValve.invoke(Request, Response) line: 106    
    FormAuthenticator(AuthenticatorBase).invoke(Request, Response) line: 502    
    StandardHostValve.invoke(Request, Response) line: 141   
    ErrorReportValve.invoke(Request, Response) line: 79 
    AccessLogValve(AbstractAccessLogValve).invoke(Request, Response) line: 616  
    StandardEngineValve.invoke(Request, Response) line: 88  
    CoyoteAdapter.service(Request, Response) line: 521  
    Http11NioProcessor(AbstractHttp11Processor<S>).process(SocketWrapper<S>) line: 1096 
    Http11NioProtocol$Http11ConnectionHandler(AbstractProtocol$AbstractConnectionHandler<S,P>).process(SocketWrapper<S>, SocketStatus) line: 674    
    NioEndpoint$SocketProcessor.doRun() line: 1500  
    NioEndpoint$SocketProcessor.run() line: 1456    
    ThreadPoolExecutor(ThreadPoolExecutor).runWorker(ThreadPoolExecutor$Worker) line: 1142  
    ThreadPoolExecutor$Worker.run() line: 617   
    TaskThread$WrappingRunnable.run() line: 61  
    TaskThread(Thread).run() line: 745  

有趣的是上面的那几行FacesServlet。 class/method 个名字已经不言自明了。

因此发生在应用请求值阶段,此时部分请求需要处理组件的解码。访问组件树是为了找到由 <f:ajax execute>(默认为 @this)中指定的客户端 ID 标识的组件。由于 <ui:repeat> 在感兴趣的组件之前,因此首先对其进行检查。 visitTree() 触发完整迭代,因为感兴趣的客户端 ID 仅在迭代期间可用。

当我将 <ui:repeat> 移动到 <h:form> 下方时,它不再被调用。那时已经找到了所有感兴趣的组件。

不幸的是,这种行为是 "by design"。你的工作很好。最好检查 #{not facesContext.postback},因为这也涵盖非 ajax 回发。