内容


借助 Ajax 自动保存 JSF 表单

第 2 部分

在服务器端管理表单数据

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 借助 Ajax 自动保存 JSF 表单

敬请期待该系列的后续内容。

此内容是该系列的一部分:借助 Ajax 自动保存 JSF 表单

敬请期待该系列的后续内容。

简介

本系列的第 1 部分中描述了这样一个场景:其中,应用程序在服务器上自动保存表单数据,在用户关闭或重新打开浏览器后,表单可以恢复。这个解决方案即使在用户浏览器失效或用户没有在 Web 表单上单击 Submit 就离开应用程序之后也可以很好地工作。

本文所附的示例应用程序(请参见 下载)包含了一个名为 SupportForm.jsp的典型 JSF 表单,此表单的数据通过 AutoSaveScript.js文件的 JavaScript 函数定期提交给服务器。SupportForm.jspAutoSaveScript.js均在本系列 第 1 部分做过介绍,该部分还展示了如何使用 JSF 阶段侦听器在不干扰应用程序逻辑的前提下处理 Ajax 请求。

在本文中,您将学习如何构建线程安全的数据存储库来自动保存表单数据。您将看到如何选择数据结构、如何用用 JSF 组件树提取出的表单数据填充这些结构、如何恢复 JSF 组件的状态、如何限制数据存储库的内存资源以及如何实现其持久性。您还会了解到几个 Web 技巧,例如如何使用过滤器和浏览器 ID cookies。

跨会话识别用户

要在用户关闭和重新打开浏览器之后恢复 Web 表单,应用程序要能跨会话识别用户,如果用户做过身份认证,这一点并不难实现。如果应用程序使用标准方法进行用户认证,就可以调用 getUserPrincipal()方法,此方法在 HttpServletRequest接口中定义,然后再用 java.security.PrincipalgetName()方法获取用户名。

如何应用程序支持匿名用户的表单保存和表单恢复特性,您也可以设置浏览器 ID,这个 ID 与会话 ID 十分类似,只不过后者是在单一的会话中跟踪用户。实际上,当用户首次访问应用程序时,可以取得会话 ID cookie 的值并设置另一个名为 BROWSERID的 cookie。与会话开始生成、会话结束失效的会话 ID 不同,BROWSERIDcookie 只设置一次,且可以在很长一段时间(比如几年)后才失效。

使用 servlet 过滤器

servlet 过滤器非常适合设置 BROWSERIDcookie,因为当用户首次访问应用程序时,此过滤器可以截取每个 HTTP 请求,并将此 cookie 添加到 HTTP 响应。一旦浏览器通过第一个响应收到此 cookie,那么所有后续响应都将包含 BROWSERIDcookie,这样应用程序能够通过其浏览器 ID 来识别匿名用户。本文所附带的示例代码包含一个名为 BrowserIdFilter的类,它实现了 javax.servlet.Filter。此类还有一个名为 getBrowserId()的方法(参见 清单 1),它迭代请求对象的 cookie,返回 BROWSERIDcookie 的值或 null(如果这样的 cookie 不存在)。

清单 1. 获得浏览器 ID cookie
 package autosave; 
 ... 
 import javax.servlet.http.Cookie; 
 ... 
 public class BrowserIdFilter implements Filter { 
    public static String BROWSERID = "BROWSERID"; // cookie name 

    public static String getBrowserId(HttpServletRequest httpRequest) { 
        String browserId = null; 
        Cookie cookies[] = httpRequest.getCookies(); 
        if (cookies != null) 
            for (int i = 0; i < cookies.length; i++) { 
                if (BROWSERID.equals(cookies[i].getName())) { 
                    browserId = cookies[i].getValue(); 
                    break; 
                } 
            } 
        return browserId; 
    } 
    ... 
 }

清单 2给出了 BrowserIdFilterdoFilter()方法,此方法使用 getBrowserId()测试 cookie 是否尚未设置。如果 getBrowserId()返回 nulldoFilter()就会用 getSession()获得会话对象,用 getId()获得会话 ID。之后,doFilter()会创建 Cookie对象,设置 maxAgepath属性,并会将此 cookie 添加到响应对象。为确保对 HTTP 请求的合适处理,requestresponse对象会被传递给 chain参数的 doFilter()方法。

清单 2. 实现过滤器接口的 doFilter() 方法
 package autosave; 
 ... 
 import javax.servlet.Filter; 
 import javax.servlet.FilterChain; 
 ... 
 public class BrowserIdFilter implements Filter { 
    public static String BROWSERID = "BROWSERID"; // cookie name 
    public static int IDAGE = 3600 * 24 * 365 * 3; // three years 
    ... 
    public void doFilter(ServletRequest request, 
            ServletResponse response, FilterChain chain) 
            throws IOException, ServletException { 
        HttpServletRequest httpRequest = (HttpServletRequest) request; 
        if (getBrowserId(httpRequest) == null) { 
            // The BROWSERID cookie has not been found. This must be 
            // the first time the user accesses the application. 
            // Use the current session's ID as the value for 
            // the BROWSERID cookie. 
            HttpServletResponse httpResponse 
                = (HttpServletResponse) response; 
            String browserId = httpRequest.getSession().getId(); 
            Cookie browserCookie = new Cookie(BROWSERID, browserId); 
            browserCookie.setMaxAge(IDAGE); 
            browserCookie.setPath(httpRequest.getContextPath()); 
            httpResponse.addCookie(browserCookie); 
        } 
        chain.doFilter(request, response); 
    } 
    ... 
 }

配置 servlet 过滤器

清单 3 展示了如何在 web.xml中配置 BrowserIdFilter类,以便过滤器能够截获每个 HTTP 请求。

清单 3. 配置过滤器
 <web-app ...> 
    ... 
    <filter> 
        <filter-name>BrowserIdFilter</filter-name> 
        <filter-class>autosave.BrowserIdFilter</filter-class> 
    </filter> 

    <filter-mapping> 
        <filter-name>BrowserIdFilter</filter-name> 
        <url-pattern>/*</url-pattern> 
    </filter-mapping> 
    ... 
 </web-app>

浏览器 ID 的解决方案只能用在表单 包含任何敏感信息的情况下,原因是所有通过这个共享的计算机使用此应用程序的用户都会被视为同一个用户。 跨会话识别用户的惟一一种最安全的方案是使用基于用户名和密码的标准身份认证方法。但基于密码的身份认证要求用户必须登录到应用程序,这很不方便。在很多时候,安全性最为重要,但有时,与其注册到站点,用户更愿意保持匿名。浏览器 ID 提供了识别匿名用户的一种简便方式。

本系列所给出的表单自动保存特性适用于认证的和匿名的这两类用户。在接下来的章节中,您将看到如何存储和检索自动保存的表单数据以及如何在多线程的环境中使用表单数据。

选择数据存储库

首先,为了保存表单数据,必须选择数据结构和存储库。这一点十分重要,因为为了存储临时数据,经常需要访问存储库。在示例应用程序中,数据每隔 10 秒为每个表单实例自动保存一次,但在真正的应用程序中,如果具有大量的并发用户,可能将这个间隔提高到 10 分钟更为合理一些。

自动保存的表单数据自然要存储在内存中,原因是这些数据只会存储很短的一段时间,之后会被更新的数据所替代。在用户提交表单之前,每个表单实例都会定期保存数据。提交之后,任何与所提交表单相关的临时数据都会从内存清除。如果用户不能提交表单或没有单击提交按钮就放弃了此页面,那么最后保存的数据会尽可能久地被存储在内存。当用户再次返回此表单时,如果所保存的数据还在,他就可以选择恢复此表单。

保存和恢复 JSF 组件的值

每个表单实例的自动保存数据都保存在 Map<String, Object>实例中。这类数据地图的每个条目和元素都包含 JSF 输入组件的值,其 ID 是数据地图的键。这个结构与 javax.servlet.ServletRequest的参数地图类似,但却不完全相同,原因是请求参数地图包含字符串数组,而存储库数据地图则保存 JSF 视图的所有输入组件的转变和验证后的值。DataMapRepository类的 saveValues()方法是一种递归方法,可以遍历 JSF 组件树,用能实现 EditableValueHolder的输入组件的值填充数据地图(参见 清单 4)。

清单 4. 将 JSF 输入组件的值存储到数据地图
 package autosave; 
 ... 
 import javax.faces.component.EditableValueHolder; 
 import javax.faces.component.UIComponent; 
 ... 
 public class DataMapRepository ... { 
    ... 
    public static void saveValues(UIComponent comp, 
            Map<String, Object> dataMap) { 
        if (comp == null) 
            return; 
        if (comp instanceof EditableValueHolder) { 
            // Input component. Put its value into the data map 
            EditableValueHolder evh = (EditableValueHolder) comp; 
            dataMap.put(comp.getId(), evh.getValue()); 
        } 
        // Iterate over the children of the current component 
        Iterator children = comp.getChildren().iterator(); 
        while (children.hasNext()) { 
            UIComponent child = (UIComponent) children.next(); 
            // Recursive call 
            saveValues(child, dataMap); 
        } 
    } 
    ... 
 }

restoreValues()方法(如 清单 5所示)遍历此 JSF 组件树,恢复输入组件的值。此方法还清除了每个 EditableValueHolder组件的 submittedValue属性以便表单数据能恢复成地图数据,同时忽略所有的提交数据。saveValues()方法在本文稍后的部分会用到,restoreValues()会在本系列的第 3 部分用到。

清单 5. 恢复 JSF 输入组件的值
 package autosave; 
 ... 
 import javax.faces.component.EditableValueHolder; 
 import javax.faces.component.UIComponent; 
 ... 
 public class DataMapRepository ... { 
    ... 
    public static void restoreValues(UIComponent comp, 
            Map<String, Object> dataMap) { 
        if (comp == null || dataMap == null) 
            return; 
        if (comp instanceof EditableValueHolder) { 
            // Input component. Get its value from the data map 
            // and clear any submitted value 
            EditableValueHolder evh = (EditableValueHolder) comp; 
            evh.setValue(dataMap.get(comp.getId())); 
            evh.setSubmittedValue(null); 
        } 
        // Iterate over the children of the current component 
        Iterator children = comp.getChildren().iterator(); 
        while (children.hasNext()) { 
            UIComponent child = (UIComponent) children.next(); 
            // Recursive call 
            restoreValues(child, dataMap); 
        } 
    } 

 }

每个地图实例都有一个惟一的 ID,此 ID 由用户 ID 和 JSF 视图 ID 组成。因此,很自然地会将所有这些数据地图都放到一个存储库地图内。DataMapRepository类会扩展 java.util.LinkedHashMap,而且它有一个方法,名为 getDataMapId()(参见 清单 6),该方法利用给定的 faces 上下文(其方法返回包含所需用户和视图信息的对象)生成数据地图的 ID。

如果用户登录,getDataMapId()就会包含源自 Principal对象的用户名。否则,用户就是匿名的,而且 getDataMapId()使用的是浏览器 ID。JSF 视图 ID 是返回 ID 的一部分,对每个 JSF 页惟一。因此,此数据地图 ID 对每个用户和页面的组合而言也是惟一的。

清单 6. 生成数据地图的惟一 ID
 package autosave; 
 ... 
 import javax.faces.component.UIViewRoot; 
 import javax.faces.context.ExternalContext; 
 import javax.faces.context.FacesContext; 
 ... 
 import java.security.Principal; 
 ... 
 public class DataMapRepository ... { 
    ... 
    public static String getDataMapId(FacesContext ctx) { 
        UIViewRoot root = ctx.getViewRoot(); 
        if (root == null) 
            return null; 
        ExternalContext ectx = ctx.getExternalContext(); 
        String userId = null; 
        Principal principal = ectx.getUserPrincipal(); 
        if (principal != null) { 
            // Use the name of the authenticated user. 
            userId = principal.getName(); 
        } else { 
            // Use the browser ID of the anonymous user. 
            userId = BrowserIdFilter.getBrowserId( 
                    (HttpServletRequest) ectx.getRequest()); 
        } 
        if (userId == null) 
            return null; 
        // Concatenate the user ID and the JSF view ID 
        return userId + root.getViewId(); 
    } 
    ... 
 }

限制数据存储库的内存资源

由于 Java 堆有限且数据存储库也不应该消耗太多内存,因此有必要进行一些限制。在示例应用程序中,存储库所能存储的数据地图的实例是有限制的。当达到这个限值时,最旧的那个数据地图会从存储库中删除,以便进行垃圾收集。此机制已经构建到 java.util.LinkedHashMap类中 —只需重写 removeEldestEntry()方法以便在存储库具有超出所允许数量的条目时能够返回 true

清单 7给出了 DataMapRepository 类,它扩展了 LinkedHashMap,添加了 maxDataMaps属性并重写了 removeEldestEntry()方法,正如之前所解释的。此外,DataMapRepository还包含了一个构造函数以便创建存储库实例的副本,如果想要在运行应用程序时获取存储库快照,这种方法会十分有用。原始的存储库和其副本均包含同样的数据地图对象,这是因为数据地图是不能修改的,这一点在本文稍后的部分介绍。

清单 7. 数据存储库类
 package autosave; 
 ... 
 import java.util.LinkedHashMap; 
 import java.util.Map; 

 public class DataMapRepository 
        extends LinkedHashMap<String, Map<String, Object>> { 
    private static final int DEFAULT_MAX_DATA_MAPS = 1000; 
    private int maxDataMaps; 

    public DataMapRepository() { 
        maxDataMaps = DEFAULT_MAX_DATA_MAPS; 
    } 
    
    public DataMapRepository(DataMapRepository repository) { 
        maxDataMaps = repository.maxDataMaps; 
        putAll(repository); 
    } 
    
    public int getMaxDataMaps() { 
        return maxDataMaps; 
    } 

    public void setMaxDataMaps(int maxDataMaps) { 
        this.maxDataMaps = maxDataMaps; 
    } 

    protected boolean removeEldestEntry(Map.Entry eldest) { 
        return size() > maxDataMaps; 
    } 
    ... 
 }

getDataMap()setDataMap()方法(参见 清单 8)可用于访问存储库的数据地图。这两个方法都使用 getDataMapId()为给定的上下文生成 ID,然后再调用继承自 LinkedHashMap类的 get()put()remove()方法。

清单 8. 恢复和检索数据地图所需的方法
 package autosave; 
 ... 
 public class DataMapRepository ... { 
    ... 
    public Map<String, Object> getDataMap(FacesContext ctx) { 
        String id = getDataMapId(ctx); 
        if (id == null) 
            return null; 
        return get(id); 
    } 
    
    public void setDataMap(FacesContext ctx, Map<String, Object> dataMap) { 
        String id = getDataMapId(ctx); 
        if (id == null) 
            return; 
        if (dataMap != null) 
            put(id, dataMap); 
        else 
            remove(id); 
    } 
    ... 
 }

在多线程环境中工作

之前介绍过的 DataMapRepository类及其基类 LinkedHashMap都不是线程安全的。解决这个问题的第一种方法是使用 java.util.CollectionssynchronizedMap()方法,它返回线程安全的包装器地图。对表单数据存储库的情况而言,除了确保它在多线程的服务器环境中被安全使用之外,最好是构建一个定制的包装器类,由它控制如何访问存储库。

为数据存储库使用线程安全的包装器

清单 9给出了这个包装器类,在示例应用程序中称为 RepositoryWrapper。它具有一个名为 repository的字段,类型为 DataMapRepositorygetRepository() 方法返回专用 repository的副本,而 setRepository()也会创建一个新副本。这些副本都包含与原始存储库相同的数据地图对象,这一点没错,因为数据地图在用表单数据创建和填充之后不会被修改。

清单 9. 数据存储库的包装器类
 package autosave; 
 ... 
 public class RepositoryWrapper implements java.io.Serializable { 
    private DataMapRepository repository; 
    
    public RepositoryWrapper() { 
        repository = new DataMapRepository(); 
    } 
    
    public synchronized DataMapRepository getRepository() { 
        return new DataMapRepository(repository); 
    } 

    public synchronized void setRepository( 
            DataMapRepository repository) { 
        if (repository != null) 
            this.repository = new DataMapRepository(repository); 
        else 
            this.repository.clear(); 
    } 
    ... 
 }

RepositoryWrapper类包含线程安全的方法以便访问数据地图和包装了的存储库的 maxDataMaps属性(参见 清单 10)。getDataMap()setDataMap()方法可以为给定的 FacesContext检索和存储数据地图。若针对 ctx参数的数据地图已经存在,hasDataMap()方法返回 true,而且 clearDataMap()方法会从存储库删除数据地图。

清单 10. 用于访问数据存储库的线程安全的方法
 package autosave; 
 ... 
 import javax.faces.context.FacesContext; 
 ... 
 import java.util.Map; 

 public class RepositoryWrapper implements java.io.Serializable { 
    ... 
    public synchronized Map<String, Object> getDataMap( 
            FacesContext ctx) { 
        return repository.getDataMap(ctx); 
    } 
    
    public synchronized void setDataMap(FacesContext ctx, 
            Map<String, Object> dataMap) { 
        repository.setDataMap(ctx, dataMap); 
    } 
    
    public synchronized boolean hasDataMap(FacesContext ctx) { 
        return getDataMap(ctx) != null; 
    } 
    
    public synchronized void clearDataMap(FacesContext ctx) { 
        setDataMap(ctx, null); 
    } 
    
    public synchronized int getMaxDataMaps() { 
        return repository.getMaxDataMaps(); 
    } 

    public synchronized void setMaxDataMaps(int maxDataMaps) { 
        repository.setMaxDataMaps(maxDataMaps); 
    } 
    ... 
 }

将包装器配置成 JSF 受管 bean

所有包含表单数据的地图对象都保存在存储库实例中,该实例的包装器被配置成 faces-config.xml文件中的一个 JSF 受管 bean(参见 清单 11)。指定的 bean 的名称为 repositoryWrapper,作用域为 application。JSF 配置文件也可用于为数据存储库的 maxDataMaps属性提供值。

清单 11. 将存储库包装器配置成 JSF 受管 bean
 <faces-config> 
    ... 
    <managed-bean> 
        <managed-bean-name>repositoryWrapper</managed-bean-name> 
        <managed-bean-class>autosave.RepositoryWrapper</managed-bean-class> 
        <managed-bean-scope>application</managed-bean-scope> 
        ... 
        <managed-property> 
            <property-name>maxDataMaps</property-name> 
            <value>100</value> 
        </managed-property> 
    </managed-bean> 
    ... 
 </faces-config>

RepositoryWrapper类有两个静态方法,可以返回受管 bean 实例。这些方法(如 清单 12所示)可以使用 FacesContextServletContextapplication作用域检索包装器 bean。

清单 12. 获得受管 bean 实例
 package autosave; 

 import javax.faces.application.Application; 
 import javax.faces.context.FacesContext; 
 import javax.faces.el.ValueBinding; 

 import javax.servlet.ServletContext; 
 ... 
 public class RepositoryWrapper implements java.io.Serializable { 
    ... 
    public static RepositoryWrapper getManagedBean(FacesContext ctx) { 
        Application app = ctx.getApplication(); 
        ValueBinding vb = app.createValueBinding("#{repositoryWrapper}"); 
        return (RepositoryWrapper) vb.getValue(ctx); 
    } 

    public static RepositoryWrapper getManagedBean(ServletContext ctx) { 
        return (RepositoryWrapper) ctx.getAttribute("repositoryWrapper"); 
    } 

 }

修改 JSF 阶段侦听器

在本系列的 第 1 部分,介绍过一个名为 AutoSaveListener的类,它可用于处理 Ajax 请求、从 JSF 组件树检索所提交的数据。在本系列中,表单数据会存储于数据存储库中,而非直接打印出来。

将当前的视图数据保存于存储库

清单 13给出了 AutoSaveListenersaveCurrentView() 方法,它可将当前 JSF 视图的表单数据存储到存储库中。第一步是调用 getCurrentInstance(),它返回 faces 上下文,然后使用 getViewRoot()获得 JSF 视图的根元素。之后,saveCurrentView()会创建新的数据地图并调用 DataMapRepository类的 saveValues()方法以将 JSF 组件的值存储到数据地图。为了安全起见,该地图对象会传递给 Collections.unmodifiableMap(),而它反过来会返回包装器地图,此地图可在任何试图修改其状态时抛出异常。这个不可修改的地图用包装器对象的 setDataMap()方法即可存储到存储库。

清单 13. 将当前 JSF 视图的表单数据存储到存储库
 package autosave; 
 ... 
 import javax.faces.component.UIViewRoot; 
 import javax.faces.context.FacesContext; 
 ... 
 import java.util.Collections; 
 import java.util.HashMap; 
 import java.util.Map; 

 public class AutoSaveListener implements PhaseListener { 
    ... 
    public void saveCurrentView() { 
        // Get the faces context of the current request. 
        FacesContext ctx = FacesContext.getCurrentInstance(); 
        // Get the root component of the current view. 
        UIViewRoot root = ctx.getViewRoot(); 
        // Create a new data map. 
        Map<String, Object> dataMap = new HashMap<String, Object>(); 
        // Store the component values into the data map. 
        DataMapRepository.saveValues(root, dataMap); 
        // Make the data map unmodifiable. 
        dataMap = Collections.unmodifiableMap(dataMap); 
        // Get the managed bean instance wrapping the data repository. 
        RepositoryWrapper wrapper = RepositoryWrapper.getManagedBean(ctx); 
        // Store the data map into the repository. 
        wrapper.setDataMap(ctx, dataMap); 
        // Stop request processing. 
        ctx.responseComplete(); 
    } 
    ... 
 }

也许您还记得,在 第 1 部分中,表单的自动保存不应该干扰应用程序逻辑。数据模型必须要能更新,且自动保存之后不应调用任何动作。保存了输入组件的值之后,saveCurrentView()必须要停止请求处理以便自动保存数据(即部分填充了的表单的实际用户输入)不会存储到应用程序数据模型。因此,saveCurrentView()会调用 faces 上下文的 responseComplete()方法以表示 JSF 请求处理生命周期必须中断。

处理 JSF 阶段事件

正如第 1 部分所介绍的,表单数据必须在 JSF 验证阶段,即 JSF 组件的所有值都已经由 JSF 框架转变和验证之后才保存。除了检验 PhaseEvent事件的阶段 ID 之外,afterPhase() 方法(如 清单 14所示)会获得 Ajax-Request头以判断当前请求是否会自动保存表单数据。此头在 AutoSaveScript.js文件的 submitFormData()函数中设置,这在第 1 部分中已经介绍过了。如果此头的值为 Auto-Save,那么 afterPhase()方法就会调用 saveCurrentView()以将 JSF 组件的验证值存储到数据存储库。

如果标记此 Ajax 请求的头不存在,那么就表明用户一定是单击了 Web 表单的提交按钮。在这种情况下,afterPhase()就会检查 faces 上下文是否包含任何消息,如果没有消息就表明由用户提交的数据有效,原因是 JSF 验证阶段过后才会调用 afterPhase()。如果表单数据有效,针对当前用户和视图组合的任何之前保存的数据都会被 clearDataMap() 从存储库中清除。

清单 14. 侦听 JSF 阶段事件
 package autosave; 
 ... 
 import javax.faces.context.FacesContext; 
 import javax.faces.event.PhaseEvent; 
 import javax.faces.event.PhaseId; 
 import javax.faces.event.PhaseListener; 
 ... 
 public class AutoSaveListener implements PhaseListener { 
    ... 
    public void afterPhase(PhaseEvent e) { 
        if (!e.getPhaseId().equals(PhaseId.PROCESS_VALIDATIONS)) 
            return; 
        FacesContext ctx = e.getFacesContext(); 
        Map headers = ctx.getExternalContext().getRequestHeaderMap(); 
        if ("Auto-Save".equals(headers.get("Ajax-Request"))) { 
            // Auto-Save Request. Save data into the repository. 
            saveCurrentView(); 
        } else { 
            // The user must have clicked the Submit button. 
            if (!ctx.getMessages().hasNext()) { 
                // There are no error messages. 
                // This means the final submitted data is valid and 
                // the temporary auto-saved data is no longer needed. 
                RepositoryWrapper wrapper 
                    = RepositoryWrapper.getManagedBean(ctx); 
                wrapper.clearDataMap(ctx); 
            } 
        } 
    } 
    ... 
 }

实现存储库的持久性

在本节,我们会增强示例应用程序以在关机之前将存储库保存到服务器上并在服务器再启动时加以恢复。对象序列化是实现存储库持久性的一种最简单的方式,原因是 DataMapRepository类、所包含的数据地图以及它们的元素都是可序列化的。注意:数据地图包含 JSF 组件的值,而根据 JSP 规范,它们必须是可序列化的。

对象序列化有很多明显的缺陷,但在这种情况下,却是一个合理的持久性解决方案,原因是保存在存储库中的表单数据实例的数量是有限的。而且,即使存储库的临时数据在服务器由于某些与应用程序不相干的问题而当机的情况下丢失了,也不会造成任何严重后果。一种更为可靠的、基于关系型或对象数据库的解决方案将需要更多的 CPU 资源,其分配对于每 10 秒就更新一次的部分用户输入的存储而言很难做到。而且,存储库状态还应该能够不受服务器或应用程序重启的影响。

使用 ServletContextListener

示例应用程序的 DataMapPersistence类实现了 javax.servlet.ServletContextListener接口以便它能在应用程序开始和结束时获得通知。这些侦听器方法使用由 getDataFile()方法返回的 File对数据存储库进行序列化和反序列化处理(如 清单 15所示)。数据文件放在示例应用程序的 WEB-INF目录。

清单 15. 获得数据存储库的文件
 package autosave; 

 import javax.servlet.ServletContext; 
 import javax.servlet.ServletContextListener; 
 ... 
 import java.io.File; 

 public class DataMapPersistence implements ServletContextListener { 

    private File getDataFile(ServletContext sctx) { 
        String path = sctx.getRealPath("/WEB-INF/repository.ser"); 
        if (path == null) 
            return null; 
        return new File(path); 
    } 
    ... 
 }

加载数据存储库

servlet/JSP 容器会在应用程序初始化期间调用 contextInitialized() 方法。清单 16给出了此方法,它对存储库对象进行反序列化并设置名为 loadedRepository的上下文属性,此属性可使用 JSP/JSF EL 日后访问。servlet 上下文属性与保存在 application作用域内的 JSP/JSF 变量类似。所加载的存储库随后可用于设置 RepositoryWrapperbean 的 repository属性。

清单 16. 在应用程序初始化期间加载数据存储库
 package autosave; 

 import javax.servlet.ServletContext; 
 import javax.servlet.ServletContextEvent; 
 import javax.servlet.ServletContextListener; 
 ... 
 import java.io.BufferedInputStream; 
 import java.io.FileInputStream; 
 import java.io.ObjectInputStream; 

 public class DataMapPersistence implements ServletContextListener { 
    ...    
    public void contextInitialized(ServletContextEvent e) { 
        ServletContext sctx = e.getServletContext(); 
        File dataFile = getDataFile(sctx); 
        if (dataFile == null || !dataFile.exists()) 
            return; 
        try { 
            ObjectInputStream in = new ObjectInputStream( 
                    new BufferedInputStream( 
                    new FileInputStream(dataFile))); 
            try { 
                // Read the data repository from the file. 
                Object repository = in.readObject(); 
                // Store the loaded repository into the application scope. 
                sctx.setAttribute("loadedRepository", repository); 
            } finally { 
                in.close(); 
            } 
        } catch (Exception x) { 
            sctx.log("Loading Error", x); 
        } 
    } 
    ... 
 }

contextInitialized()调用之前,不会调用任何 servlet 或 JSP 页,这意味着 JSF 框架可能还尚未初始化。因而,contextInitialized()不能设置 RepositoryWrapperbean 的 repository属性,此 bean 受管于 JSF 框架。这种设定在 faces-config.xml文件完成,在此文件中,application作用域的 loadedRepository变量可完全被用于设置此受管 bean 的 repository属性(参见 清单 17)。

清单 17. 将所加载的数据存储到存储库包装器
 <faces-config> 
    ... 
    <managed-bean> 
        <managed-bean-name>repositoryWrapper</managed-bean-name> 
        <managed-bean-class>autosave.RepositoryWrapper</managed-bean-class> 
        <managed-bean-scope>application</managed-bean-scope> 
        <managed-property> 
            <property-name>repository</property-name> 
            <value>#{loadedRepository}</value> 
        </managed-property> 
        ... 
    </managed-bean> 
    ... 
 </faces-config>

保存数据存储库

在应用程序关闭后,servlet/JSP 容器将调用 contextDestroyed() 方法。在获得 RepositoryWrapper实例后,由 JSF 框架进行管理,contextDestroyed() 方法从包装器 bean 检索数据存储库的一个副本并将此存储库副本序列化到数据文件内(参见 清单 18)。

清单 18. 应用程序关闭前将文件中的存储库数据保存到文件中
 package autosave; 

 import javax.servlet.ServletContext; 
 import javax.servlet.ServletContextEvent; 
 import javax.servlet.ServletContextListener; 
 ... 
 import java.io.BufferedOutputStream; 
 import java.io.FileOutputStream; 
 import java.io.ObjectOutputStream; 

 public class DataMapPersistence implements ServletContextListener { 
    ...    
    public void contextDestroyed(ServletContextEvent e) { 
        ServletContext sctx = e.getServletContext(); 
        File dataFile = getDataFile(sctx); 
        if (dataFile == null) 
            return; 
        try { 
            ObjectOutputStream out = new ObjectOutputStream( 
                    new BufferedOutputStream( 
                    new FileOutputStream(dataFile))); 
            try { 
                // Get a copy of the data repository from the wrapper bean. 
                RepositoryWrapper wrapper 
                    = RepositoryWrapper.getManagedBean(sctx); 
                Object repository = wrapper.getRepository(); 
                // Serialize the data repository into the file. 
                out.writeObject(repository); 
            } finally { 
                out.close(); 
            } 
        } catch (Exception x) { 
            sctx.log("Saving Error", x); 
        } 
    } 

 }

清单 19显示了 DataMapPersistence类是如何作为 web.xml文件中的 servlet 上下文侦听器进行配置的。

清单 19. 配置 servlet 上下文侦听器
 <web-app ...> 
    ... 
    <listener> 
        <listener-class>autosave.DataMapPersistence</listener-class> 
    </listener> 
    ... 
 </web-app>

结束语

在本系列的第 2 部分中,您了解了如何跨会话识别用户、如何为表单数据实现线程安全的存储库、如何保存和恢复 JSF 输入组件的值以及如何实现数据存储库的持久性。在本系列的最后一部分即第 3 部分中,您将会看到如何用所存储的数据来填充 JSF 表单,以及如何在 JSF 应用程序中使用更多 JavaScript 技巧。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development, Java technology
ArticleID=260164
ArticleTitle=借助 Ajax 自动保存 JSF 表单: 第 2 部分
publish-date=10082007