browser back + viewscope beans

2019-01-24 05:06发布

问题:

What the problem is : What happens when clicking on the browser back button --> opens up a page whose viewscoped-managedbean is already destroyed --> submit a request from a commandButton from that page with grid-record-selections ?

What i expect : The associated viewscope-managebean is re-created, receives the grid-record-selections, and deal with them as if the browser back button is never involved.

What i experience : The associated viewscope-managebean is NOT re-created, doesnt receive the grid-record-selections. Have to reenter the URL, or F5 after clicking on the browser-back button for it to work properly again.

So here's the success scenario, all beans are viewscoped beans :

  1. GET page1.xhtml --> page1Bean created, querying data, etc in @PostConstruct
  2. check/select several records from a datatable, click on process button
  3. page1Bean's process method stores the selected records in the flash object, and redirect to the page2.xhtml
  4. page1Bean destroyed, page2Bean created, and in preRenderView listener method, fetches the selected records from the flash object, and deal with them
  5. click the "go to main page" commandButton to redirect to page1.xhtml, and page2Bean destroyed, page1Bean created again
  6. loop from no 2 - 5 is still doable

Now, this is the errornous scenario involving the browser back button (different stuffs happening starting from #6) :

  1. GET page1.xhtml --> page1Bean created, querying data, etc in @PostConstruct
  2. check/select several records from a datatable, click on process button
  3. page1Bean's process method stores the selected records in the flash object, and redirect to the page2.xhtml
  4. page1Bean destroyed, page2Bean created, and in preRenderView listener method, fetches the selected records from the flash object, and deal with them
  5. click the browser back button page2Bean is not destroyed, page1Bean is not created
  6. check/select several records from a datatable, click on process button
  7. the page1Bean method executes (strange, because the page1Bean should've been destroyed), but cannot see the record-selections made, and redirect to page2.xhtml
  8. page1Bean is not destroyed (no logging output), page2Bean is not created (since it's not been destroyed), executes the preRenderView listener as usual, but this time, no selected records in the flash object

Is it possible to have the normal experience (as if without the browser back button) with viewscope-beans with the browser back button ?

Here's my dependency :

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.faces</artifactId>
    <version>2.1.3</version>
    <scope>compile</scope>
</dependency>

Please share your ideas !

回答1:

The browser seems to have served the page from its cache instead of sending a fullworthy HTTP GET request to the server, while you have JSF state saving method set to server (which is the default).

There are 2 ways to solve this problem:

  1. Tell the browser to not cache the dynamic JSF pages. You can do this with help of a 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);
    }
    

    Map the filter on the FacesServlet or its same URL-pattern.

  2. Set the JSF state saving method to client, so that the entire view state is stored in a hidden field of the form instead of in the session in the server side.

    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <param-value>client</param-value>
    </context-param>
    

The filter way is preferable.



回答2:

The disadvantage of disabling the browser cache of a page is that the user will see an browsers error page if he use browser back to navigate to previous page. So another solutions is to identify if the page comes from the server or from the browser cache using javascript:

First create a simple backing bean which serves a unique id (in my case current system time):

@Named("browserCacheController")
@RequestScoped
public class BrowserCacheController implements Serializable {
private static final long serialVersionUID = 1L;
/**
 * Returns a unique increasing id for each request
 * @return
 */
public long getCacheID() {
    return System.currentTimeMillis();
}   
}

So now you can test if a page is served from the server or browser and redirecting the user if the current page comes from browser cache. See the following javascript code placed into a jsf page which should not be cached by browser:

<script type="text/javascript">
    // check for latestCacheID
    if (!isValidCacheID(#{browserCacheController.cacheID})) {
        //redirect to some page
        document.location="#{facesContext.externalContext.requestContextPath}/index.jsf";
    }

    // test cacheID if it comes from the server....
    function isValidCacheID(currentCacheID) {
        if (!('localStorage' in window && window['localStorage'] !== null))
            return true; // old browsers not supported
        var latestCacheID=localStorage.getItem("org.imixs.latestCacheID");
        if (latestCacheID!=null && currentCacheID<=latestCacheID) {
            return false; // this was a cached browser page!
        }
        // set new id
        localStorage.setItem("org.imixs.latestCacheID", currentCacheID);
        return true;
    }
</script>

The script can also be placed into facelet to make the jsf code more clean.