javax.faces.application.ViewExpiredException: la vue n'a pas pu être restaurée

175

J'ai écrit une application simple avec une sécurité gérée par conteneur. Le problème est que lorsque je me connecte et que j'ouvre une autre page sur laquelle je me déconnecte, je reviens à la première page et je clique sur n'importe quel lien, etc.ou actualise la page, j'obtiens cette exception. Je suppose que c'est normal (ou peut-être pas :)) parce que je me suis déconnecté et que la session est détruite. Que dois-je faire pour rediriger l'utilisateur vers, par exemple, index.xhtml ou login.xhtml et l'empêcher de voir cette page / message d'erreur?

En d'autres termes, comment puis-je rediriger automatiquement d'autres pages vers la page d'index / de connexion après ma déconnexion?

C'est ici:

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)
l245c4l
la source

Réponses:

354

introduction

Le ViewExpiredExceptionsera lancé chaque fois que le javax.faces.STATE_SAVING_METHODest défini sur server(par défaut) et que l'utilisateur final envoie une requête HTTP POST sur une vue via <h:form>avec <h:commandLink>, <h:commandButton>ou <f:ajax>, alors que l'état de vue associé n'est plus disponible dans la session.

L'état d'affichage est identifié comme la valeur d'un champ d'entrée masqué javax.faces.ViewStatedu <h:form>. Avec la méthode d'enregistrement d'état définie sur server, il contient uniquement l'ID d'état d'affichage qui fait référence à un état d'affichage sérialisé dans la session. Ainsi, lorsque la session est expirée pour une raison quelconque (soit expiré côté serveur ou côté client, soit le cookie de session n'est plus conservé pour une raison quelconque dans le navigateur, ou en appelant le HttpSession#invalidate()serveur, ou en raison d'un bogue spécifique au serveur avec les cookies de session comme connu dans WildFly ), l'état de la vue sérialisée n'est plus disponible dans la session et l'utilisateur final obtiendra cette exception. Pour comprendre le fonctionnement de la session, voir également Comment fonctionnent les servlets? Instanciation, sessions, variables partagées et multithreading .

Il existe également une limite au nombre de vues que JSF stockera dans la session. Lorsque la limite est atteinte, la vue la moins récemment utilisée expire. Voir aussi com.sun.faces.numberOfViewsInSession vs com.sun.faces.numberOfLogicalViews .

Avec la méthode d'enregistrement d'état définie sur client, le javax.faces.ViewStatechamp de saisie masqué contient à la place l'ensemble de l'état de la vue sérialisée, de sorte que l'utilisateur final n'obtiendra pas de ViewExpiredExceptionlorsque la session expire. Cela peut cependant se produire dans un environnement de cluster ("ERREUR: MAC n'a pas vérifié" est symptomatique) et / ou lorsqu'il y a un délai d'expiration spécifique à l'implémentation sur l'état côté client configuré et / ou lorsque le serveur recrée la clé AES lors du redémarrage , voir également Obtenir ViewExpiredException dans un environnement en cluster tandis que la méthode d'enregistrement d'état est définie sur client et que la session utilisateur est valide pour la résoudre.

Quelle que soit la solution, assurez-vous de ne pas l' utiliser enableRestoreView11Compatibility. il ne restaure pas du tout l'état d'affichage d'origine. Il recrée essentiellement la vue et tous les beans étendus de vue associés à partir de zéro et perd ainsi toutes les données d'origine (état). Comme l'application se comportera de manière déroutante ("Hey, où sont mes valeurs d'entrée .. ??"), c'est très mauvais pour l'expérience utilisateur. Mieux vaut utiliser des vues sans état ou à la <o:enableRestorableView>place pour pouvoir la gérer sur une vue spécifique uniquement plutôt que sur toutes les vues.

En ce qui concerne la raison pour laquelle JSF doit enregistrer l'état d'affichage, dirigez-vous vers cette réponse: Pourquoi JSF enregistre l'état des composants de l'interface utilisateur sur le serveur?

Éviter ViewExpiredException lors de la navigation dans les pages

Pour éviter, ViewExpiredExceptionpar exemple, de revenir en arrière après la déconnexion lorsque la sauvegarde d'état est définie sur server, il ne suffit pas de rediriger la requête POST après la déconnexion. Vous devez également indiquer au navigateur de ne pas mettre en cache les pages JSF dynamiques, sinon le navigateur peut les afficher à partir du cache au lieu d'en demander une nouvelle au serveur lorsque vous envoyez une requête GET dessus (par exemple par le bouton Précédent).

Le javax.faces.ViewStatechamp masqué de la page mise en cache peut contenir une valeur d'ID d'état d'affichage qui n'est plus valide dans la session en cours. Si vous (ab) utilisez POST (liens / boutons de commande) au lieu de GET (liens / boutons normaux) pour la navigation de page à page, et cliquez sur un tel lien / bouton de commande sur la page en cache, alors cela sera à son tour échouer avec un ViewExpiredException.

Pour déclencher une redirection après la déconnexion dans JSF 2.0, ajoutez <redirect />à la <navigation-case>question en question (le cas échéant) ou ajoutez ?faces-redirect=trueà la outcomevaleur.

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

ou

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

Pour demander au navigateur de ne pas mettre en cache les pages JSF dynamiques, créez un Filterqui est mappé sur le nom de servlet du FacesServletet ajoutez les en-têtes de réponse nécessaires pour désactiver le cache du navigateur. Par exemple

@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);
    }

    // ...
}

Éviter ViewExpiredException lors de l'actualisation de la page

Afin d'éviter l' ViewExpiredExceptionactualisation de la page actuelle lorsque l'enregistrement de l'état est défini sur server, vous devez non seulement vous assurer que vous effectuez une navigation de page à page exclusivement par GET (liens / boutons réguliers), mais vous devez également vous assurer que vous utilisez exclusivement ajax pour soumettre les formulaires. Si vous soumettez le formulaire de manière synchrone (non-ajax) de toute façon, alors vous feriez mieux de rendre la vue sans état (voir la section suivante), ou d'envoyer une redirection après POST (voir la section précédente).

Avoir une ViewExpiredExceptionactualisation sur la page est dans la configuration par défaut un cas très rare. Cela ne peut se produire que lorsque la limite du nombre de vues que JSF stockera dans la session est atteinte. Donc, cela ne se produira que lorsque vous aurez défini manuellement cette limite trop bas, ou que vous créerez continuellement de nouvelles vues en "arrière-plan" (par exemple par un sondage ajax mal implémenté dans la même page ou par un 404 page d'erreur sur les images cassées de la même page). Voir aussi com.sun.faces.numberOfViewsInSession vs com.sun.faces.numberOfLogicalViews pour plus de détails sur cette limite. Une autre cause est le conflit entre les bibliothèques JSF dupliquées dans le chemin de classe d'exécution. La procédure correcte pour installer JSF est décrite dans notre page wiki JSF .

Gestion de ViewExpiredException

Lorsque vous souhaitez gérer un inévitable ViewExpiredExceptionaprès une action POST sur une page arbitraire qui était déjà ouverte dans un onglet / une fenêtre de navigateur alors que vous êtes déconnecté dans un autre onglet / fenêtre, vous souhaitez spécifier un error-pagepour celui dans web.xmllequel va vers une page "Votre session a expiré". Par exemple

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

Utilisez si nécessaire un en-tête de méta-actualisation dans la page d'erreur au cas où vous auriez l'intention de rediriger vers la page d'accueil ou de connexion.

<!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>

(le 0in contentreprésente le nombre de secondes avant la redirection, 0signifie donc "rediriger immédiatement", vous pouvez utiliser par exemple 3pour laisser le navigateur attendre 3 secondes avec la redirection)

Notez que la gestion des exceptions lors des requêtes ajax nécessite un fichier spécial ExceptionHandler. Voir aussi Délai d'expiration de session et gestion de ViewExpiredException sur la requête ajax JSF / PrimeFaces . Vous pouvez trouver un exemple en direct sur la page de présentation d' OmniFacesFullAjaxExceptionHandler (cela couvre également les demandes non-ajax).

Notez également que votre page d'erreur "générale" doit être mappée sur <error-code>de 500au lieu d'un <exception-type>de par exemple java.lang.Exceptionou java.lang.Throwable, sinon toutes les exceptions encapsulées dans ServletExceptiontelles ViewExpiredExceptionqu'elles finiraient toujours dans la page d'erreur générale. Voir aussi ViewExpiredException affiché dans la page d'erreur java.lang.Throwable dans web.xml .

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

Vues apatrides

Une alternative complètement différente consiste à exécuter des vues JSF en mode sans état. De cette façon, rien de l'état JSF ne sera enregistré et les vues n'expireront jamais, mais seront simplement reconstruites à partir de zéro à chaque demande. Vous pouvez activer les vues sans état en définissant l' transientattribut de <f:view>sur true:

<f:view transient="true">

</f:view>

De cette façon, le javax.faces.ViewStatechamp caché obtiendra une valeur fixe de "stateless"dans Mojarra (je n'ai pas vérifié MyFaces à ce stade). Notez que cette fonctionnalité a été introduite dans Mojarra 2.1.19 et 2.2.0 et n'est pas disponible dans les anciennes versions.

La conséquence est que vous ne pouvez plus utiliser les beans à portée de vue. Ils se comporteront désormais comme des beans à portée de requête. L'un des inconvénients est que vous devez suivre l'état vous-même en manipulant des entrées cachées et / ou des paramètres de requête lâches. Principalement ces formes avec des champs d'entrée avec rendered, readonlyou disabledattributs qui sont contrôlés par les événements ajax seront affectés.

Notez que le <f:view>n'a pas nécessairement besoin d'être unique dans la vue et / ou de résider uniquement dans le modèle principal. Il est également tout à fait légitime de le redéclarer et de l'imbriquer dans un modèle de client. Il "étend" fondamentalement le parent <f:view>alors. Par exemple, dans le modèle principal:

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

et dans le client modèle:

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

Vous pouvez même envelopper le <f:view>dans un <c:if>pour le rendre conditionnel. Notez qu'il s'appliquerait à la vue entière , pas seulement au contenu imbriqué, comme dans l' <h:form>exemple ci-dessus.

Voir également


Sans rapport avec le problème concret, l'utilisation de HTTP POST pour une navigation pure de page à page n'est pas très conviviale pour l'utilisateur / le référencement. Dans JSF 2.0, vous devriez vraiment préférer <h:link>ou <h:button>sur <h:commandXxx>ceux pour la navigation simple de page à page.

Donc au lieu de par exemple

<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>

mieux faire

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

Voir également

BalusC
la source
Comment puis-je le faire avec la navigation implicite dans java ee 6? Je n'utilise pas faces-config.
l245c4l
1
Oh, vous utilisez JSF 2.0? Vous auriez dû le mentionner dans votre question! Ajouter ?faces-redirect=trueau outcome. J'ai mis à jour la réponse en conséquence.
BalusC le
Oui, je viens de commencer avec java ee :) et j'utilise faces-redirect = true dans toutes mes navigations. J'utilise h: commandLink uniquement lorsque j'ai des actions associées. Par exemple Lien de déconnexion ... J'ai une action String logout () où j'invalide la session et la redirige vers la connexion, mais cela ne fonctionne pas sur la page où j'étais connecté et étant pour le moment déconnecté et lève cette exception :(
l245c4l
1
Merci encore et désolé pour cela :) mais au moins j'ai eu une réponse rapide et professionnelle en un rien de temps: p
l245c4l
1
@LS: Le filtre est cependant toujours obligatoire pour le cas chaque fois que l'on appuie sur le bouton retour après un POST expiré et essaie d'appeler une autre requête POST sur celui-ci. Sinon, cela entraînerait de manière involontaire cette exception.
BalusC
56

Avez-vous essayé d'ajouter des lignes ci-dessous à votre web.xml?

<context-param>
   <param-name>com.sun.faces.enableRestoreView11Compatibility</param-name>
   <param-value>true</param-value>
</context-param>

J'ai trouvé cela très efficace lorsque j'ai rencontré ce problème.

Mike GH
la source
1
il a travaillé pour moi aussi. Merci d'avoir répondu. Quel est le but de cela?
MartK
2
Je ne me souviens pas exactement, mais j'ai trouvé cette solution sur le site Web ICEFaces.
Mike GH
Je suis peut-être un peu en retard à la fête, mais cela a fonctionné pour moi aussi. Merci!
stellarossa
4
Est-ce défini uniquement pour JSF 1.2 ou JSF 2?
SRy
17
Cela arrêtera de lever l'exception lorsque la vue est expirée et continuera simplement la demande, mais JSF ne pourra toujours pas restaurer l'état de la vue ni trouver les beans d'étendue de vue associés. Cette transaction se comportera comme un JSF sans état et vous devrez restaurer vous-même l'état d'affichage en fonction des paramètres de la requête POST pour éviter "wtf?" l'expérience de l'utilisateur final lors du traitement de l'envoi du formulaire a répondu de manière inattendue. Si vous souhaitez l'appliquer uniquement à des pages JSF spécifiques, utilisez plutôt OmniFaces <o:enableRestorableView>au lieu d'un paramètre de contexte à l'échelle de l'application.
BalusC
5

Avant de changer web.xml, vous devez d'abord vous assurer que votre ManagedBean implements Serializable:

@ManagedBean
@ViewScoped
public class Login implements Serializable {
}

Surtout si vous utilisez MyFaces

ZuzEL
la source
3

Évitez les formulaires en plusieurs parties dans Richfaces:

<h:form enctype="multipart/form-data">
    <a4j:poll id="poll" interval="10000"/>
</h:form>

Si vous utilisez Richfaces, j'ai constaté que les demandes ajax à l'intérieur des formulaires en plusieurs parties renvoient un nouvel ID de vue à chaque demande.

Comment déboguer:

À chaque demande ajax, un ID de vue est renvoyé, ce qui est très bien tant que l'ID de vue est toujours le même. Si vous obtenez un nouvel ID de vue à chaque demande, il y a un problème et doit être corrigé.

tak3shi
la source
Soyez prudent lorsque vous jouez avec les sondages, cela peut empêcher l'expiration des sessions de vos utilisateurs.
Jonathan Simas
0

Vous pouvez utiliser votre propre AjaxExceptionHandler personnalisé ou primefaces-extensions

Mettez à jour votre faces-config.xml

...
<factory>
  <exception-handler-factory>org.primefaces.extensions.component.ajaxerrorhandler.AjaxExceptionHandlerFactory</exception-handler-factory>
</factory>
...

Ajoutez le code suivant dans votre page jsf

...
<pe:ajaxErrorHandler />
...
myset
la source
0

J'obtenais cette erreur: javax.faces.application.ViewExpiredException.Lorsque j'utilise différentes demandes, j'ai trouvé celles ayant le même JsessionId, même après le redémarrage du serveur. Cela est donc dû au cache du navigateur. Fermez simplement le navigateur et essayez, cela fonctionnera.

rahul
la source
0

Lorsque notre page est inactive pendant x durée, la vue expirera et lancera javax.faces.application.ViewExpiredException pour éviter que cela ne se produise, une solution consiste à créer CustomViewHandler qui étend ViewHandler et remplace la méthode restoreView.Toutes les autres méthodes sont déléguées au Parent

import java.io.IOException;
import javax.faces.FacesException;
import javax.faces.application.ViewHandler;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.servlet.http.HttpServletRequest;

public class CustomViewHandler extends ViewHandler {
    private ViewHandler parent;

    public CustomViewHandler(ViewHandler parent) {
        //System.out.println("CustomViewHandler.CustomViewHandler():Parent View Handler:"+parent.getClass());
        this.parent = parent;
    }

    @Override 
    public UIViewRoot restoreView(FacesContext facesContext, String viewId) {
    /**
     * {@link javax.faces.application.ViewExpiredException}. This happens only  when we try to logout from timed out pages.
     */
        UIViewRoot root = null;
        root = parent.restoreView(facesContext, viewId);
        if(root == null) {
            root = createView(facesContext, viewId);
        }
        return root;
    }

    @Override
    public Locale calculateLocale(FacesContext facesContext) {
        return parent.calculateLocale(facesContext);
    }

    @Override
    public String calculateRenderKitId(FacesContext facesContext) {
        String renderKitId = parent.calculateRenderKitId(facesContext);
        //System.out.println("CustomViewHandler.calculateRenderKitId():RenderKitId: "+renderKitId);
        return renderKitId;
    }

    @Override
    public UIViewRoot createView(FacesContext facesContext, String viewId) {
        return parent.createView(facesContext, viewId);
    }

    @Override
    public String getActionURL(FacesContext facesContext, String actionId) {
        return parent.getActionURL(facesContext, actionId);
    }

    @Override
    public String getResourceURL(FacesContext facesContext, String resId) {
        return parent.getResourceURL(facesContext, resId);
    }

    @Override
    public void renderView(FacesContext facesContext, UIViewRoot viewId) throws IOException, FacesException {
        parent.renderView(facesContext, viewId);
    }

    @Override
    public void writeState(FacesContext facesContext) throws IOException {
        parent.writeState(facesContext);
    }

    public ViewHandler getParent() {
        return parent;
    }

}   

Ensuite, vous devez l'ajouter à votre faces-config.xml

<application>
    <view-handler>com.demo.CustomViewHandler</view-handler>
</application>

Merci pour la réponse originale sur le lien ci-dessous: http://www.gregbugaj.com/?p=164

user4924157
la source
Cette approche ne restaure pas les beans à portée de vue.
BalusC
-2

Veuillez ajouter cette ligne dans votre web.xml Cela fonctionne pour moi

<context-param>
        <param-name>org.ajax4jsf.handleViewExpiredOnClient</param-name> 
        <param-value>true</param-value>     
    </context-param>
harshal
la source
4
Votre réponse sera plus utile si vous incluez une explication et un contexte sur ce que fait le code.
Palpatim
1
Même si c'est la bonne réponse, StackOverflow décourage des réponses comme celle-ci sans explication. Il serait utile à la communauté d'ajouter plus d'informations sur les raisons pour lesquelles cela fonctionne.
RacerNerd
-2

Je suis tombé sur ce problème moi-même et j'ai réalisé que c'était à cause d'un effet secondaire d'un filtre que j'avais créé qui filtrait toutes les demandes sur l'application. Dès que j'ai modifié le filtre pour ne sélectionner que certaines demandes, ce problème ne s'est pas produit. Il peut être utile de vérifier ces filtres dans votre application et de voir comment ils se comportent.

javshak
la source
-3

J'ajoute la configuration suivante à web.xml et elle a été résolue.

<context-param>
    <param-name>com.sun.faces.numberOfViewsInSession</param-name>
    <param-value>500</param-value>
</context-param>
<context-param>
    <param-name>com.sun.faces.numberOfLogicalViews</param-name>
    <param-value>500</param-value>
</context-param>
Mohamed Ali
la source
6
Terrible conseil. Ne faites cela que lorsque vous avez beaucoup de mémoire et que vos utilisateurs finaux ont réellement ouvert jusqu'à 500 onglets de navigateur et appuyez sur le bouton de retour du navigateur jusqu'à 500 fois par rapport aux publications synchrones précédentes.
BalusC