1

我有一个比我将在此处描述的问题更大的问题,但由于难以解释和调试我的代码,我创建了一个更小(且愚蠢)的问题来重现相同的错误。

在我看来,我想打印一个字符串列表,这个列表是根据偏移量计算的。在我的 java 类中,我有一个巨大的静态字符串数组。为了计算我的字符串列表(将打印在页面中),我将静态数组中的值复制到我的列表中,从“偏移量”的值开始。

在我的页面中,我还有一个“增加偏移量”按钮,它通过 ajax 请求增加“偏移量”的值并更新结果(根据新偏移量重新计算列表)。

我的问题是:当我单击“增加偏移量”按钮时,执行服务器端代码,偏移量增加,但更新时出现问题:显示当前偏移量的文本使用“偏移量”的新值更新,但是字符串列表不会发生同样的情况,它会使用旧值进行更新,并且仅在我进行第二次请求时才考虑新值,在第二次请求中计算的值仅在第三次更新中考虑,使视图中的字符串列表总是延迟一个请求。

查看此图像(在新选项卡中将其打开以使其变大):在此处输入图像描述

让我们看一些代码...

按钮:

<h:form>
  <h:commandButton value="Increment offsets">
    <f:ajax listener="#{test.incrementOffset()}" render=":stringList" />
  </h:commandButton>
</h:form>

字符串列表:

<h:panelGroup id="stringList">
  <h1>Offsets increment = #{test.offset}</h1>
  <comp:stringPrinter offset="#{test.offset}" />
</h:panelGroup>

这就是我在标题中谈到的组件。页面不会计算或打印字符串列表。它将被计算并打印在一个复合组件中。

在查看复合组件的代码之前,这里是视图中使用的托管 bean“Test”:

@ManagedBean(name="test")
@ViewScoped
public class Test implements Serializable {
  private int offset;

  public int getOffset() {
    return offset;
  }

  public void incrementOffset(){
     offset++;
  }
}

复合组件:正如我所说的,它只是接收“偏移量”作为参数并打印字符串列表。查看下面的代码:

<h:body>
  <composite:interface componentType="stringPrinter">
    <composite:attribute name="offset" type="java.lang.Integer" required="true" />
  </composite:interface>
  <composite:implementation>
    <ul>
      <ui:repeat value="#{cc.list}" var="string">
        <li>#{string}</li>
      </ui:repeat>
    </ul>
  </composite:implementation>
</h:body>

这就是字符串列表的计算方式:

@FacesComponent(value="stringPrinter")
public class StringPrinter extends UINamingContainer implements Serializable {
  private ArrayList<String> list;

  private static String[] LIPSUM = "...".split(" "); // "..." is not the actual string, the string is irrelevant (it has more than 400 words separated by spaces).
  private static int ARRAY_SIZE = 12;

  private void generateList(){
    int offset = (Integer) getAttributes().get("offset");
    int position = offset;
    list = new ArrayList<String>();
    for (int i = 0; i < ARRAY_SIZE; i++){
      list.add(LIPSUM[position++ % LIPSUM.length]);
    }
  }

  public ArrayList<String> getList() {
    if (null == list) generateList();
      return list;
    }
  }
}

如果我不使用组件,问题就不会发生。当列表在父视图中计算并且组件没有附加 java 类时,问题不会发生。但在我真正的问题中,我不需要打印字符串,我真的需要由复合组件处理的东西,因此必须有一个专门的类。


一些调试:

ui:repeat 标签可能是问题所在。如果我需要打印列表中的第一个字符串而不是打印列表(这需要 ui:repeat),那么所有内容都会在正确的时间正确更新。查看不使用 ui:repeats 的复合组件的代码:

<h:body>
  <composite:interface componentType="stringPrinter">
    <composite:attribute name="offset" type="java.lang.Integer" required="true" />
  </composite:interface>
  <composite:implementation>
    <h:outputText value="#{cc.list.get(0)}" />
  </composite:implementation>
</h:body>

上面的代码有效,但这不是我需要做的,我真的需要使用 ui:repeat。我还尝试了 c:forEach,结果相同。

为了进一步调试,我给自己提出了一个问题:“组件中的方法 getList() 是否在页面中的方法 incrementOffset() 执行后被调用?”。这是我得到的日志:

*** GET LIST CALLED!
*** GET LIST CALLED!
*** OFFSET INCREMENTED!
*** GET LIST CALLED!
*** GET LIST CALLED!

我使用 System.out.flush() 来防止消息被缓冲。似乎 getList() 在方法执行后被调用,我看不出有任何理由用旧值更新视图。无论如何,在 incrementOffset() 执行之前和之后两次调用它的事实有点奇怪。

此问题影响 Mojarra 2.1.7、2.1.22、2.1.23 和 2.2.0。我没有测试其他版本。

我将通过进一步的调试来更新这篇文章。

下载 Maven 项目并自己测试: zip 文件

提前感谢您的任何回答。

4

1 回答 1

2

我发现问题出在哪里。我认为当一个组件被更新时,它会被完全重新处理,但在某些情况下不会发生。出于某种原因,似乎当使用 ui:repeat 时,JSF 试图延迟组件重置。

一种解决方案是在调用 getList() 时显式地重新计算列表:

以前是这样的:

  public ArrayList<String> getList() {
    if (null == list) generateList();
      return list;
    }
  }

如果我删除 null 的验证,它会起作用。

在我看来,这就是正在发生的事情:当组件更新时,列表仍然具有之前计算的值,它不是 null 并且没有进行重新计算。在一个请求和另一个请求之间的时间里,组件正在重建。重置组件后,list 等于 null,并且在 getList() 上完成了重新计算,从而使视图始终延迟一个请求。


编辑:更好的解决方案是在组件的视图中使用 f:event 而不是在每次获取时重新计算列表。

<f:metadata>
  <f:event type="preRenderComponent" listener="#{cc.generateList}" />
</f:metadata>
于 2013-06-26T23:31:23.540 回答