javax.faces.application.ViewExpiredException:无法还原视图


175

我编写了具有容器管理的安全性的简单应用程序。问题是当我登录并打开另一个要注销的页面时,然后回到第一页,然后单击任何链接等或刷新页面,我得到了此异常。我想这是正常的(或者可能不是:)),因为我注销了,会话被破坏了。我应该怎么做才能将用户重定向到例如index.xhtml或login.xhtml并使他免于看到该错误页面/消息?

换句话说,注销后如何自动将其他页面重定向到索引/登录页面?

这里是:

javax.faces.application.ViewExpiredException: viewId:/index.xhtml - View /index.xhtml could not be restored.
    at com.sun.faces.lifecycle.RestoreViewPhase.execute(RestoreViewPhase.java:212)
    at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:101)
    at com.sun.faces.lifecycle.RestoreViewPhase.doPhase(RestoreViewPhase.java:110)
    at com.sun.faces.lifecycle.LifecycleImpl.execute(LifecycleImpl.java:118)
    at javax.faces.webapp.FacesServlet.service(FacesServlet.java:312)
    at org.apache.catalina.core.StandardWrapper.service(StandardWrapper.java:1523)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:343)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:215)
    at filter.HttpHttpsFilter.doFilter(HttpHttpsFilter.java:66)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:256)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:215)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:277)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:188)
    at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:641)
    at com.sun.enterprise.web.WebPipeline.invoke(WebPipeline.java:97)
    at com.sun.enterprise.web.PESessionLockingStandardPipeline.invoke(PESessionLockingStandardPipeline.java:85)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:185)
    at org.apache.catalina.connector.CoyoteAdapter.doService(CoyoteAdapter.java:325)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:226)
    at com.sun.enterprise.v3.services.impl.ContainerMapper.service(ContainerMapper.java:165)
    at com.sun.grizzly.http.ProcessorTask.invokeAdapter(ProcessorTask.java:791)
    at com.sun.grizzly.http.ProcessorTask.doProcess(ProcessorTask.java:693)
    at com.sun.grizzly.http.ProcessorTask.process(ProcessorTask.java:954)
    at com.sun.grizzly.http.DefaultProtocolFilter.execute(DefaultProtocolFilter.java:170)
    at com.sun.grizzly.DefaultProtocolChain.executeProtocolFilter(DefaultProtocolChain.java:135)
    at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:102)
    at com.sun.grizzly.DefaultProtocolChain.execute(DefaultProtocolChain.java:88)
    at com.sun.grizzly.http.HttpProtocolChain.execute(HttpProtocolChain.java:76)
    at com.sun.grizzly.ProtocolChainContextTask.doCall(ProtocolChainContextTask.java:53)
    at com.sun.grizzly.SelectionKeyContextTask.call(SelectionKeyContextTask.java:57)
    at com.sun.grizzly.ContextTask.run(ContextTask.java:69)
    at com.sun.grizzly.util.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:330)
    at com.sun.grizzly.util.AbstractThreadPool$Worker.run(AbstractThreadPool.java:309)
    at java.lang.Thread.run(Thread.java:619)

Answers:


353

介绍

ViewExpiredException每当将被抛出javax.faces.STATE_SAVING_METHOD被设置为server(默认)和终端用户经由发送视图上的HTTP POST请求<h:form><h:commandLink><h:commandButton><f:ajax>,而相关联的视图状态不是在会话中可用了。

视图状态被标识为的隐藏输入字段javax.faces.ViewState的值<h:form>。将状态保存方法设置为时server,它仅包含引用会话中已序列化视图状态的视图状态ID。因此,当会话由于某种原因而过期时(要么在服务器或客户端超时,要么由于某种原因在浏览器中或由于调用HttpSession#invalidate()服务器而不再维护会话cookie ,或者由于服务器特定的错误而将会话cookie作为在WildFly中是已知的),则会话中的序列化视图状态不再可用,最终用户将获得此异常。要了解会话的工作原理,另请参见servlet如何工作?实例化,会话,共享变量和多线程

JSF将在会话中存储的视图数量也受到限制。达到限制后,最近最少使用的视图将失效。另请参见com.sun.faces.numberOfViewsInSession与com.sun.faces.numberOfLogicalViews

将状态保存方法设置为时clientjavax.faces.ViewState隐藏的输入字段将包含整个序列化的视图状态,因此最终用户ViewExpiredException在会话过期时将不会获得。但是,它仍然可能在群集环境下(“错误:MAC未验证”是有症状的)和/或在配置的客户端状态上存在特定于实现的超时和/或在重新启动过程中服务器重新生成AES密钥时发生,另请参阅在状态保存方法设置为客户端且用户会话有效的解决方案下,在集群环境中获取ViewExpiredException

无论解决方案,确保你不会使用enableRestoreView11Compatibility。它根本不还原原始视图状态。它基本上从头开始重新创建视图和所有关联的视图作用域Bean,从而丢失所有原始数据(状态)。由于应用程序将以一种混乱的方式运行(“嘿,我的输入值在哪里。最好使用无状态视图,或者<o:enableRestorableView>改为使用无状态视图,这样您就可以仅在特定视图而不是所有视图上对其进行管理。

至于为什么 JSF需要保存视图状态,请转到以下答案:为什么JSF在服务器上保存UI组件的状态?

在页面导航上避免ViewExpiredException

为了避免ViewExpiredException在状态保存设置为时注销后导航(例如)server,仅在注销后重定向POST请求是不够的。您还需要指示浏览器不要缓存动态JSF页面,否则当您在服务器上发送GET请求时(例如,通过后退按钮),浏览器可能会从缓存中显示它们,而不是从服务器请求一个新的页面。

javax.faces.ViewState缓存页面的隐藏字段可能包含视图状态ID值,该值在当前会话中不再有效。如果您(ab)使用POST(命令链接/按钮)而不是GET(常规链接/按钮)进行页面间导航,然后在缓存的页面上单击这样的命令链接/按钮,则依次失败了ViewExpiredException

要在JSF 2.0中注销后触发重定向,请添加<redirect />到有<navigation-case>问题的(如果有)或添加?faces-redirect=trueoutcome值。

<h:commandButton value="Logout" action="logout?faces-redirect=true" />

要么

public String logout() {
    // ...
    return "index?faces-redirect=true";
}

要指示浏览器不要缓存动态JSF页面,请创建一个Filter,将其映射到的Servlet名称上,FacesServlet并添加所需的响应标头以禁用浏览器缓存。例如

@WebFilter(servletNames={"Faces Servlet"}) // Must match <servlet-name> of your FacesServlet.
public class NoCacheFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;

        if (!req.getRequestURI().startsWith(req.getContextPath() + ResourceHandler.RESOURCE_IDENTIFIER)) { // Skip JSF resources (CSS/JS/Images/etc)
            res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
            res.setHeader("Pragma", "no-cache"); // HTTP 1.0.
            res.setDateHeader("Expires", 0); // Proxies.
        }

        chain.doFilter(request, response);
    }

    // ...
}

在页面刷新时避免ViewExpiredException

为了避免ViewExpiredException在状态保存设置为时刷新当前页面server,您不仅需要确保仅通过GET(常规链接/按钮)执行页面到页面的导航,而且还需要确保您仅使用ajax提交表单。如果无论如何要同步提交表单(非ajax),则最好使视图变为无状态(请参阅后面部分),或者在POST之后发送重定向(请参阅前面的部分)。

有一个ViewExpiredException页面上的刷新是在默认配置中非常罕见的情况。仅当达到JSF将在会话中存储的视图数量限制时,才会发生这种情况。因此,只有在您手动将限制设置得太低或在“后台”中不断创建新视图时(例如,在同一页面中实施不正确的Ajax轮询或实施不正确的404),才会发生这种情况错误页面出现在同一页面的损坏图像上)。有关该限制的详细信息,另请参见com.sun.faces.numberOfViewsInSession与com.sun.faces.numberOfLogicalViews。另一个原因是运行时类路径中的重复JSF库相互冲突。我们的JSF Wiki页面中概述了正确安装JSF的过程。

处理ViewExpiredException

当你要处理一个无法回避的ViewExpiredException,其已经在某些浏览器标签/窗口打开,当你在另一个标签/窗口注销任意页面上的POST操作后,那么你想指定error-page用于在web.xml肚里转到“您的会话超时”页面。例如

<error-page>
    <exception-type>javax.faces.application.ViewExpiredException</exception-type>
    <location>/WEB-INF/errorpages/expired.xhtml</location>
</error-page>

如有必要,请在错误页面上使用元刷新标题,以防您打算实际进一步重定向到主页或登录页面。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Session expired</title>
        <meta http-equiv="refresh" content="0;url=#{request.contextPath}/login.xhtml" />
    </head>
    <body>
        <h1>Session expired</h1>
        <h3>You will be redirected to login page</h3>
        <p><a href="#{request.contextPath}/login.xhtml">Click here if redirect didn't work or when you're impatient</a>.</p>
    </body>
</html>

0in content代表重定向之前的秒数,0因此表示“立即重定向”,您可以使用例如3让浏览器等待3秒的重定向)

请注意,在ajax请求期间处理异常需要特殊的ExceptionHandler。另请参见会话超时和对JSF / PrimeFaces ajax request的ViewExpiredException处理。您可以在OmniFaces FullAjaxExceptionHandler展示页面上找到一个实时示例(该示例还涵盖了非ajax请求)。

另外请注意,您的“一般”的错误页面应该被映射<error-code>500,而不是一个<exception-type>如的java.lang.Exceptionjava.lang.Throwable,否则包裹在所有异常ServletException,如ViewExpiredException仍然会在一般的错误页面结束。另请参见web.xml中java.lang.Throwable error-page中显示的ViewExpiredException

<error-page>
    <error-code>500</error-code>
    <location>/WEB-INF/errorpages/general.xhtml</location>
</error-page>

无状态意见

完全不同的替代方法是在无状态模式下运行JSF视图。这样,将不会保存任何JSF状态,并且视图将永远不会过期,而只是在每个请求上从头开始重建。您可以通过设置打开无状态的观点transient的属性<f:view>true

<f:view transient="true">

</f:view>

这样,javax.faces.ViewState隐藏字段将"stateless"在Mojarra中获得固定值(此时尚未选中MyFaces)。请注意,此功能是Mojarra 2.1.19和2.2.0中引入的,而在较早的版本中不可用。

结果是您不能再使用视图范围的bean。现在,它们的行为类似于请求范围的Bean。缺点之一是您必须通过摆弄隐藏的输入和/或宽松的请求参数来自己跟踪状态。主要与输入字段那些形式renderedreadonly或者disabled其通过AJAX事件控制属性将受到影响。

请注意,<f:view>不一定在整个视图中都必须是唯一的和/或仅将其驻留在主模板中。重新声明并将其嵌套在模板客户端中也是完全合法的。然后,它基本上“扩展”了父级<f:view>。例如在主模板中:

<f:view contentType="text/html">
    <ui:insert name="content" />
</f:view>

并在模板客户端中:

<ui:define name="content">
    <f:view transient="true">
        <h:form>...</h:form>
    </f:view>
</f:view>

您甚至可以将<f:view>in 包装为<c:if>有条件的。请注意,它将应用于整个视图,而不仅适用于嵌套内容,例如<h:form>上例中的。

也可以看看


具体问题无关,使用HTTP POST进行纯页面到页面导航并不是非常友好的用户/ SEO。在JSF 2.0中,对于纯普通的页面到页面导航,您应该更喜欢<h:link><h:button>优于<h:commandXxx>那些。

因此,而不是例如

<h:form id="menu">
    <h:commandLink value="Foo" action="foo?faces-redirect=true" />
    <h:commandLink value="Bar" action="bar?faces-redirect=true" />
    <h:commandLink value="Baz" action="baz?faces-redirect=true" />
</h:form>

最好做

<h:link value="Foo" outcome="foo" />
<h:link value="Bar" outcome="bar" />
<h:link value="Baz" outcome="baz" />

也可以看看


如何在Java EE 6中使用隐式导航来做到这一点?我不使用faces-config。
l245c4l 2010年

1
哦,您在使用JSF 2.0?您应该在问题中提到这一点!添加?faces-redirect=true到中outcome。我已经相应地更新了答案。
BalusC 2010年

是的,我刚开始使用java ee :),并且在所有导航中都使用faces-redirect = true。仅当我有与之关联的动作时,才使用h:commandLink。例如注销链接...我有一个操作字符串logout(),在该会话中会话无效并重定向到登录,但是在我登录的页面上却不起作用,此刻我正在注销并抛出该异常:(
l245c4l 2010年

1
再次感谢,并为此感到抱歉:),但至少我很快就得到了专业的快速答复:p
l245c4l 2010年

1
@LS:但是,对于在过期的POST之后按下返回按钮并尝试在其上调用另一个POST请求的情况,仍然必须使用过滤器。否则,这会导致直觉上的异常。
BalusC 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.