这是由 Mojarra 中的一个错误引起的,或者可能是 JSF 规范中对复合组件的重定向方法表达式的疏忽。
解决方法如下ViewDeclarationLanguage。
public class FaceletViewHandlingStrategyPatch extends ViewDeclarationLanguageFactory {
private ViewDeclarationLanguageFactory wrapped;
public FaceletViewHandlingStrategyPatch(ViewDeclarationLanguageFactory wrapped) {
this.wrapped = wrapped;
}
@Override
public ViewDeclarationLanguage getViewDeclarationLanguage(String viewId) {
return new FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch(getWrapped().getViewDeclarationLanguage(viewId));
}
@Override
public ViewDeclarationLanguageFactory getWrapped() {
return wrapped;
}
private class FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch extends ViewDeclarationLanguageWrapper {
private ViewDeclarationLanguage wrapped;
public FaceletViewHandlingStrategyWithRetargetMethodExpressionsPatch(ViewDeclarationLanguage wrapped) {
this.wrapped = wrapped;
}
@Override
public void retargetMethodExpressions(FacesContext context, UIComponent topLevelComponent) {
super.retargetMethodExpressions(new FacesContextWithFaceletContextAsELContext(context), topLevelComponent);
}
@Override
public ViewDeclarationLanguage getWrapped() {
return wrapped;
}
}
private class FacesContextWithFaceletContextAsELContext extends FacesContextWrapper {
private FacesContext wrapped;
public FacesContextWithFaceletContextAsELContext(FacesContext wrapped) {
this.wrapped = wrapped;
}
@Override
public ELContext getELContext() {
boolean isViewBuildTime = TRUE.equals(getWrapped().getAttributes().get(IS_BUILDING_INITIAL_STATE));
FaceletContext faceletContext = (FaceletContext) getWrapped().getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
return (isViewBuildTime && faceletContext != null) ? faceletContext : super.getELContext();
}
@Override
public FacesContext getWrapped() {
return wrapped;
}
}
}
在faces-config.xml中安装如下:
<factory>
<view-declaration-language-factory>com.example.FaceletViewHandlingStrategyPatch</view-declaration-language-factory>
</factory>
我是怎么确定的?
我们已经确认问题在于,当在复合组件中调用操作而在另一个复合组件中声明操作本身时,方法表达式参数变为null。
<h:form>
<my:forEachComposite items="#{['one', 'two', 'three']}" />
</h:form>
<cc:interface>
<cc:attribute name="items" required="true" />
</cc:interface>
<cc:implementation>
<c:forEach items="#{cc.attrs.items}" var="item">
<h:commandButton id="regularButton" value="regular button" action="#{bean.action(item)}" />
<my:buttonComposite value="cc button" action="#{bean.action(item)}" />
</c:forEach>
</cc:implementation>
<cc:interface>
<cc:attribute name="action" required="true" targets="compositeButton" />
<cc:actionSource name=""></cc:actionSource>
<cc:attribute name="value" required="true" />
</cc:interface>
<cc:implementation>
<h:commandButton id="compositeButton" value="#{cc.attrs.value}" />
</cc:implementation>
我做的第一件事是找到负责在#{bean.action(item)} 后面创建MethodExpression 实例的代码。我知道它通常是通过ExpressionFactory#createMethodExpression() 创建的。我也知道所有 EL 上下文变量通常都是通过ELContext#getVariableMapper() 提供的。于是我在createMethodExpression()中放了一个调试断点。
在调用堆栈中,我们可以检查ELContext#getVariableMapper() 以及谁负责创建MethodExpression。在一个复合组件依次嵌套一个常规命令按钮和一个复合命令按钮的测试页面中,我们可以在ELContext 中看到以下差异:
常规按钮:
我们可以看到常规按钮使用DefaultFaceletContext 作为ELContext,并且VariableMapper 包含正确的item 变量。
复合按钮:
我们可以看到复合按钮使用标准的ELContextImpl 作为ELContext,并且VariableMapper 不包含正确的item 变量。所以我们需要在调用堆栈中返回一些步骤,看看这个标准 ELContextImpl 的来源以及为什么使用它而不是 DefaultFaceletContext。
一旦找到特定ELContext 实现的创建位置,我们可以发现它是从FacesContext#getElContext() 获得的。但这并不代表复合组件的 EL 上下文!这由当前的FaceletContext 表示。所以我们需要多退一步来弄清楚为什么FaceletContext没有被传递下去。
我们可以在这里看到CompositeComponentTagHandler#applyNextHander() 没有通过FaceletContext,而是通过FacesContext。这正是 JSF 规范中可能存在疏忽的部分。 ViewDeclarationLanguage#retargetMethodExpressions() 应该要求另一个参数,代表实际涉及的 ELContext。
但事实就是如此。我们现在无法即时更改规格。我们能做的最好的事情就是向他们报告问题。
上面显示的FaceletViewHandlingStrategyPatch 的工作原理如下,最终覆盖FacesContext#getELContext(),如下所示:
@Override
public ELContext getELContext() {
boolean isViewBuildTime = TRUE.equals(getWrapped().getAttributes().get(IS_BUILDING_INITIAL_STATE));
FaceletContext faceletContext = (FaceletContext) getWrapped().getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
return (isViewBuildTime && faceletContext != null) ? faceletContext : super.getELContext();
}
您看,它检查 JSF 当前是否正在构建视图以及是否存在 FaceletContext。如果是这样,则返回FaceletContext 而不是标准的ELContext 实现(注意FaceletContext just extends ELContext)。这样MethodExpression 将被创建,右ELContext 持有右VariableMapper。