如何在JSF 1.x中实现页面加载重定向

30
我有一个Web应用程序,用户可以直接访问某些特定页面(例如,查看或编辑项目的页面)。为了实现这一点,我们提供了一个特定的URL。这些URL位于当前Web应用程序之(即它们可以存在于另一个Web应用程序中,也可以存在于电子邮件中)。
URL看起来像这样:http://myserver/my-app/forward.jsf?action=XXX&param=YYY,其中:
- action表示用户将被重定向到的页面。您可以将其视为faces-config.xml中任何JSF操作中navigation-casefrom-outcome。 - actionParam是上一个操作的参数(通常是一个项目ID)。
例如,我可以拥有这些类型的URL:
- http://myserver/my-app/forward.jsf?action=viewItem&actionParam=1234 - http://myserver/my-app/forward.jsf?action=editItem&actionParam=1234 当然,我有一个Java类(bean),将检查一些安全性约束(即用户是否允许查看/编辑相应的项目?),然后将用户重定向到正确的页面(例如edit.xhtmlview.xhtmlaccess-denied.xhtml)。

目前的实现

目前,我们有一种基本的方式来完成转发。当用户单击链接时,将调用以下XHTML页面:

<html>
    <body id="forwardForm">
        <h:inputHidden id="myAction" binding="#{forwardBean.hiddenAction}"/>
        <h:inputHidden id="myParam" binding="#{forwardBean.hiddenActionParam}"/>
        <h:commandButton id="forwardBtn" actionListener="#{forwardBean.doForward}" style="display: none;"/>
    </body>
    <script type="text/javascript">
        document.getElementById('forwardForm:forwardBtn').click();
    </script>
</html>

如您所见,我在我的Java bean中绑定了两个<h:inputHidden>组件。它们将用于存储actionactionParam请求参数的值(使用FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("actiontParam");)。 我还提供了doForward方法,该方法将在页面呈现时立即调用,并将用户重定向(再次)到真正的页面。 方法如下:

public void doForward(ActionEvent evt) {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    String redirect = // define the navigation rule that must be used in order to redirect the user to the adequate page...
    NavigationHandler myNav = facesContext.getApplication().getNavigationHandler();
    myNav.handleNavigation(facesContext, null, redirect);
}

这个解决方案可以工作,但我有两个问题:

  • 我不喜欢它的实现方式。我相信我可以用更简单的方法(使用Servlet?)。
  • 这个解决方案使用了Javascript,而我不能使用Javascript(因为旧版黑莓用户可能会使用该功能,而这些用户不支持Javascript)。

所以我的问题是如何重构这个重定向/转发功能?

技术信息

Java 1.6,JSF 1.2,Facelets,Richfaces


我不确定这样做是否最好,但是你正在给用户提供一个链接并使用中间的xhtml和JavaScript点击,为什么不将其指向一个servlet,然后您可以处理此场景,我的意思是,为什么不使用servlet而不是中间的xhtml。我认为在jsf环境中添加自己的servlet并不是坏事。 - jmj
@org: 这当然可以不使用servlet来完成。使用servlet只会增加额外的维护负担,因为它无法立即访问导航案例。 - BalusC
@BalusC 谢谢,我想知道怎么做?我们可以在xhtml文件中直接使用jsp脚本吗? - jmj
@org:我有一个理论,但我只发布那些我可以从自己的经验中确定正确性的答案。而且我现在手头没有JSF 1.2游乐场环境。也许如果我有更多时间的话。无论视图技术如何,都不鼓励使用脚本片段。 - BalusC
@org scriptlet 超出了我的词汇量 ;) 如果我使用 Servlet 创建解决方案,我将无法受益于 JSF 的导航案例。 - Romain Linsolas
@BalusC 不要犹豫,分享你的想法,即使它们没有经过测试 ;) - Romain Linsolas
5个回答

30
将GET查询参数设置为faces-config.xml中的托管属性,这样就不需要手动收集它们:
<managed-bean>
    <managed-bean-name>forward</managed-bean-name>
    <managed-bean-class>com.example.ForwardBean</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
    <managed-property>
        <property-name>action</property-name>
        <value>#{param.action}</value>
    </managed-property>
    <managed-property>
        <property-name>actionParam</property-name>
        <value>#{param.actionParam}</value>
    </managed-property>
</managed-bean>

这样,请求forward.jsf?action=outcome1&actionParam=123将允许JSF将actionactionParam参数设置为ForwardBeanactionactionParam属性。

创建一个小视图forward.xhtml(足够小,适合默认响应缓冲区(通常是2KB),以便可以通过导航处理程序重置它,否则必须在servlet容器的配置中增加响应缓冲区大小),并在f:viewbeforePhase上调用bean方法:

<!DOCTYPE html>
<html xmlns:f="http://java.sun.com/jsf/core">
    <f:view beforePhase="#{forward.navigate}" />
</html>
ForwardBean 可以长这样:
public class ForwardBean {
    private String action;
    private String actionParam;

    public void navigate(PhaseEvent event) {
        FacesContext facesContext = FacesContext.getCurrentInstance();
        String outcome = action; // Do your thing?
        facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, null, outcome);
    }

    // Add/generate the usual boilerplate.
}

navigation-rule 可以说是自解释的(注意其中的 <redirect /> 条目,它们会在底层执行 ExternalContext#redirect() 而不是 ExternalContext#dispatch()):

<navigation-rule>
    <navigation-case>
        <from-outcome>outcome1</from-outcome>
        <to-view-id>/outcome1.xhtml</to-view-id>
        <redirect />
    </navigation-case>
    <navigation-case>
        <from-outcome>outcome2</from-outcome>
        <to-view-id>/outcome2.xhtml</to-view-id>
        <redirect />
    </navigation-case>
</navigation-rule>

另外一种选择是使用forward.xhtml作为

<!DOCTYPE html>
<html>#{forward}</html>

更新navigate()方法,使其在@PostConstruct上被调用(该注解将在bean的构建和所有受管属性设置之后被调用):

@PostConstruct
public void navigate() {
    // ...
}    

这种方法具有相同的效果,但视图方面实际上并不是自我说明的。它基本上只打印 ForwardBean#toString()(如果尚未存在,则隐式构建bean)。


对于JSF2用户,请注意使用<f:viewParam>传递参数的更清晰的方法,并使用< f:event type =“preRenderView”>更健壮地处理重定向/导航。 另请参见:


我喜欢你使用<f:view beforePhase=#{...}"/>的想法。然而,我的第一次测试并不成功,因为beforePhase方法没有被调用。我不明白为什么... - Romain Linsolas
我仍然不明白为什么第一个解决方案不起作用(也许我会为此创建一个单独的帖子),但第二个解决方案确实有效!非常感谢! - Romain Linsolas
PhaseEvent参数是否存在?JSF实现/版本确切是什么?我必须承认,我在使用faces-config声明为1.2的2.1-nightly上进行了测试(以便2.x功能不起作用)。顺便说一下,该属性是在1.2中引入的(因此在1.1或更早版本中根本无法工作)。但是@PostConstruct也是如此。 - BalusC

5
FacesContext context = FacesContext.getCurrentInstance();
HttpServletResponse response = (HttpServletResponse)context.getExternalContext().getResponse();
response.sendRedirect("somePage.jsp");

不回答我的问题。你想让我把这段代码放在哪里?而且它怎么能在没有任何用户交互或JavaScript调用的情况下被调用? - Romain Linsolas

2

你应该使用action而不是actionListener:

<h:commandLink id="close" action="#{bean.close}" value="Close" immediate="true" 
                                   />

在close方法中,你需要写入类似以下的代码:
public String close() {
   return "index?faces-redirect=true";
}

其中index是你的页面之一(index.xhtml)

当然,所有这些内容都应该写在我们原始的页面中,而不是在中间页面中。 在close()方法内部,您可以使用参数动态选择重定向的位置。


使用 actionactionListener 对我的当前问题没有任何影响。 - Romain Linsolas
而且,OP正在使用的是JSF 1.x版本,而不是2.x版本。此外,重定向将导致请求参数丢失。 - BalusC

1

编辑2

最终我通过实现我的前向操作找到了一个解决方案:

private void applyForward() {
    FacesContext facesContext = FacesContext.getCurrentInstance();
    // Find where to redirect the user.
    String redirect = getTheFromOutCome();

    // Change the Navigation context.
    NavigationHandler myNav = facesContext.getApplication().getNavigationHandler();
    myNav.handleNavigation(facesContext, null, redirect);

    // Update the view root
    UIViewRoot vr = facesContext.getViewRoot();
    if (vr != null) {
        // Get the URL where to redirect the user
        String url = facesContext.getExternalContext().getRequestContextPath();
        url = url + "/" + vr.getViewId().replace(".xhtml", ".jsf");
        Object obj = facesContext.getExternalContext().getResponse();
        if (obj instanceof HttpServletResponse) {
            HttpServletResponse response = (HttpServletResponse) obj;
            try {
                // Redirect the user now.
                response.sendRedirect(response.encodeURL(url));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

它能够工作(至少在我的第一次测试中),但我仍然不喜欢它的实现方式... 有更好的想法吗?


编辑 此解决方案不起作用。实际上,当调用doForward()函数时,JSF生命周期已经启动,因此无法重新创建新请求。


一个解决这个问题的想法,但我并不是很喜欢,就是在setBindedInputHidden()方法中强制执行doForward()操作:
private boolean actionDefined = false;
private boolean actionParamDefined = false;

public void setHiddenActionParam(HtmlInputHidden hiddenActionParam) {
    this.hiddenActionParam = hiddenActionParam;
    String actionParam = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("actionParam");
    this.hiddenActionParam.setValue(actionParam);
    actionParamDefined = true;
    forwardAction();
}

public void setHiddenAction(HtmlInputHidden hiddenAction) {
    this.hiddenAction = hiddenAction;
    String action = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap().get("action");
    this.hiddenAction.setValue(action);
    actionDefined = true;
    forwardAction();
}

private void forwardAction() {
    if (!actionDefined || !actionParamDefined) {
        // As one of the inputHidden was not binded yet, we do nothing...
        return;
    }
    // Now, both action and actionParam inputHidden are binded, we can execute the forward...
    doForward(null);
}

这个解决方案不涉及任何Javascript调用,并且并不起作用。


1
这确实很尴尬 :) handleNavigation() 在这里完全是不必要的(它已经被 response.sendRedirect() 覆盖了),而且 ExternalContext 有一个 redirect() 方法。 - BalusC

0
假设foo.jsp是您的JSP文件。以下代码是您想要重定向的按钮。
<h:commandButton value="Redirect" action="#{trial.enter }"/>  

现在我们将检查在您的Java(服务)类中进行指导的方法

 public String enter() {
            if (userName.equals("xyz") && password.equals("123")) {
                return "enter";
            } else {
                return null;
            }
        } 

现在这是 faces-config.xml 文件的一部分。

<managed-bean>
        <managed-bean-name>'class_name'</managed-bean-name>
        <managed-bean-class>'package_name'</managed-bean-class>
        <managed-bean-scope>request</managed-bean-scope>
    </managed-bean>


    <navigation-case>
                <from-outcome>enter</from-outcome>
                <to-view-id>/foo.jsp</to-view-id>
                <redirect />
            </navigation-case>

这并没有回答 OP 实际提出的问题。 - BalusC

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接