为什么getter被渲染属性调用了这么多次?

mar*_*zzz 18 getter jsf facelets jsf-2

与前面的示例相关,我试图在服务器上监视我的get/set方法(当它们被调用时,以及多久).所以,我的实际看起来像这样:

@ManagedBean(name="selector")
@RequestScoped
public class Selector {
    @ManagedProperty(value="#{param.profilePage}")
    private String profilePage;

    public String getProfilePage() {
        if(profilePage==null || profilePage.trim().isEmpty()) {
            this.profilePage="main";
        }

        System.out.println("GET "+profilePage);

        return profilePage;
    }
    public void setProfilePage(String profilePage) { 
        this.profilePage=profilePage; 
        System.out.println("SET "+profilePage); 
    }
}
Run Code Online (Sandbox Code Playgroud)

并且唯一可以调用此方法的页面(它只调用渲染时的get方法)是:

<!DOCTYPE html>
<ui:composition
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:ui="http://java.sun.com/jsf/facelets">

    <h:panelGroup layout="block" id="profileContent">
        <h:panelGroup rendered="#{selector.profilePage=='main'}">
            // nothing at the moment
        </h:panelGroup>
    </h:panelGroup>
</ui:composition>
Run Code Online (Sandbox Code Playgroud)

当我看到服务器日志时,我的昏迷,我看到:

SET null
GET main
GET main
GET main
GET main
GET main
GET main
GET main
Run Code Online (Sandbox Code Playgroud)

什么?它调用七次getProfilePage()方法?(也是1次setProfilePage())我想知道为什么这个行为:)

谢谢

添加了一个例子

@ManagedBean(name="selector")
@RequestScoped
public class Selector {
    @ManagedProperty(value="#{param.profilePage}")
    private String profilePage;

    @PostConstruct
    public void init() {
        if(profilePage==null || profilePage.trim().isEmpty()) {
            this.profilePage="main";
        }
    }

    public String getProfilePage() { return profilePage; }
    public void setProfilePage(String profilePage) { this.profilePage=profilePage; }
}
Run Code Online (Sandbox Code Playgroud)

profile.xhtml

<h:panelGroup layout="block" id="profileContent">
    <h:panelGroup layout="block" styleClass="content_title">
        Profilo Utente
    </h:panelGroup>

    <h:panelGroup rendered="#{selector.profilePage=='main'}">
        <ui:include src="/profile/profile_main.xhtml" />
    </h:panelGroup>

    <h:panelGroup rendered="#{selector.profilePage=='edit'}">
        <ui:include src="/profile/profile_edit.xhtml" />
    </h:panelGroup>
</h:panelGroup>

// profile_main.xhtml
<h:form id="formProfileMain" prependId="false">
    <h:panelGroup layout="block" styleClass="content_span">
        <h:outputScript name="jsf.js" library="javax.faces" target="head" />

        <h:panelGroup layout="block" styleClass="profilo_3">
            <h:commandButton value="EDIT">
                <f:setPropertyActionListener target="#{selector.profilePage}" value="edit" />
                <f:ajax event="action" render=":profileContent"/>
            </h:commandButton>
        </h:panelGroup>
    </h:panelGroup>
</h:form>

// profile_edit.xhtml
<h:form id="formProfileEdit" prependId="false">
    <h:panelGroup layout="block" styleClass="content_span">
        <h:outputScript name="jsf.js" library="javax.faces" target="head" />

        <h:panelGroup layout="block" styleClass="profilo_3">
            <h:commandButton value="Edit">
                <f:setPropertyActionListener target="#{selector.profilePage}" value="editProfile" />
                <f:ajax event="action" render=":profileContent"/>
            </h:commandButton>

            <h:commandButton value="Back">
                <f:setPropertyActionListener target="#{selector.profilePage}" value="main" />
                <f:ajax event="action" render=":profileContent"/>
            </h:commandButton>
        </h:panelGroup>
    </h:panelGroup>
</h:form>      
Run Code Online (Sandbox Code Playgroud)

在这个例子中,我调用profile_main(默认情况下); 之后(例如)我调用profile_edit(通过点击编辑); 之后,我通过单击Back返回profile_main.现在,如果我想重新加载profile_edit(EDIT),我需要在该命令按钮上多次单击.为什么?

Bal*_*usC 35

EL(表达式语言,那些#{}东西)不会缓存调用的结果左右.它只是直接访问bean中的数据.如果getter只返回数据,这通常不会造成伤害.

setter调用由@ManagedProperty.完成.它基本上做了以下事情:

selector.setProfilePage(request.getParameter("profilePage"));
Run Code Online (Sandbox Code Playgroud)

getter调用都是rendered="#{selector.profilePage == 'some'}"在渲染响应阶段完成的.当它false第一次评估时,UIComponent#encodeAll()则不再进行任何调用.在评估时true,它将按以下顺序重新评估六次:

  1. UIComponent#encodeBegin() - 找到组件开头的渲染器.
  2. Renderer#encodeBegin() - 渲染组件的开头.
  3. UIComponent#encodeChildren() - 找到组件子项的渲染器.
  4. Renderer#encodeChildren() - 呈现组件的子项.
  5. UIComponent#encodeEnd() - 找到用于组件结尾的渲染器.
  6. Renderer#encodeEnd() - 渲染组件结束.

组件及其渲染器在每个步骤中验证是否允许渲染.在表单提交期间,如果输入或命令组件或其任何父组件具有rendered属性,则还将在应用请求值阶段期间对其进行评估,作为防止篡改/黑客请求的一部分.

没错,这看起来像笨拙和低效.根据规范问题941,它被认为是JSF的致命弱点.有人建议删除所有这些重复检查并坚持完成UIComponent#encodeAll(),或者isRendered()每个阶段进行评估.在EG讨论期间,很明显问题的根源在于EL,而不是在JSF中,并且使用CDI可以大大提高性能.因此没有必要从JSF规范方面解决它.


如果您关注的是托管属性在设置后只应检查一次,如果它为null或为空,则考虑将其移动到注释为的方法中@PostConstruct.在bean的构造和所有依赖注入之后,将直接调用这样的方法.

@PostConstruct
public void init() {
    if (profilePage == null || profilePage.trim().isEmpty()) {
        profilePage = "main";
    }
}
Run Code Online (Sandbox Code Playgroud)

也可以看看:

  • 我没有仔细观察JSF 2.0中的行为,因为它特别便宜所以不值得努力,所以微优化它,但你可能想要在getter中添加一个`Thread.dumpStack();`以了解这个调用的位置发起了一个`FacesContext.getCurrentInstance().getPhaseId()`的sysout,以了解它现在处于哪个阶段. (2认同)