我遇到的问题比我在这里描述的问题要大,但由于解释和调试我的代码很困难,因此我创建了一个较小(和愚蠢)的问题来重现同样的错误。
在我看来,我想打印一个字符串列表,这个列表是根据偏移量计算的。在我的java类中,我有一个巨大的字符串静态数组。要计算我的字符串列表(将在页面中打印),我将静态数组中的值复制到我的列表中,从“offset”的值开始。
在我的页面中,我还有一个“递增偏移”按钮,它通过ajax请求递增“offset”的值并更新结果(根据新的偏移量重新计算列表)。
我的问题是:当我单击“递增偏移”按钮时,执行服务器端代码,偏移量会增加但更新时会出现问题:显示当前偏移量的文本会更新为新值“offset”,但字符串列表不会发生同样的情况,使用旧值更新,并且如果我执行第二个请求,则会考虑新值,第二个请求中计算的值仅在第三次更新,使视图中的字符串列表总是一个请求迟到。
查看此图片(在新标签中将其打开以使其更大):
让我们看一些代码......
按钮:
<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++;
}
}
复合组件:简单如我所说,它只是接收“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),我需要在列表中打印第一个String,所有内容都会在正确的时间内正确更新。请参阅复合组件的代码,而不使用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 file
提前感谢您的回答。
答案 0 :(得分: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>