简介本系列的第 1 部分中描述了这样一个场景:其中,应用程序在服务器上自动保存表单数据,在用户关闭或重新打开浏览器后,表单可以恢复。这个解决方案即使在用户浏览器失效或用户没有在 Web 表单上单击 Submit 就离开应用程序之后也可以很好地工作。 本文所附的示例应用程序(请参见 下载)包含了一个名为 在本文中,您将学习如何构建线程安全的数据存储库来自动保存表单数据。您将看到如何选择数据结构、如何用用 JSF 组件树提取出的表单数据填充这些结构、如何恢复 JSF 组件的状态、如何限制数据存储库的内存资源以及如何实现其持久性。您还会了解到几个 Web 技巧,例如如何使用过滤器和浏览器 ID cookies。 跨会话识别用户developerWorks Ajax 资源中心请访问 Ajax 资源中心,这里几乎囊括了关于 Ajax 编程模型的所有信息,包括各种文章和教程、论坛、博客、wikis、活动和新闻。 要在用户关闭和重新打开浏览器之后恢复 Web 表单,应用程序要能跨会话识别用户,如果用户做过身份认证,这一点并不难实现。如果应用程序使用标准方法进行用户认证,就可以调用 如何应用程序支持匿名用户的表单保存和表单恢复特性,您也可以设置浏览器 ID,这个 ID 与会话 ID 十分类似,只不过后者是在单一的会话中跟踪用户。实际上,当用户首次访问应用程序时,可以取得会话 ID cookie 的值并设置另一个名为 用户会感谢您本文接下来将会介绍在 Ajax 应用程序中构建自动保存功能如何能让用户的 Web 体验更加方便和高效。与此同时,您还可以获得几个高级的 Web 技巧,比如使用过滤器和用户浏览器 ID cookie。 使用 servlet 过滤器servlet 过滤器非常适合设置 清单 1. 获得浏览器 ID cookiepackage 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给出了 清单 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 展示了如何在 清单 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 也可以用于通过身份认证的用户以便他们无需每次访问应用程序时都要登录。您可能也见过这样的站点,其中的登录屏幕上有标着 “Remember me on this computer” 的复选框。这样的复选框就是让此站点能够设置浏览器 ID 以便用户不必在返回该站点时还要再次登录。 浏览器 ID 的解决方案只能用在表单 不包含任何敏感信息的情况下,原因是所有通过这个共享的计算机使用此应用程序的用户都会被视为同一个用户。 跨会话识别用户的惟一一种最安全的方案是使用基于用户名和密码的标准身份认证方法。但基于密码的身份认证要求用户必须登录到应用程序,这很不方便。在很多时候,安全性最为重要,但有时,与其注册到站点,用户更愿意保持匿名。浏览器 ID 提供了识别匿名用户的一种简便方式。 本系列所给出的表单自动保存特性适用于认证的和匿名的这两类用户。在接下来的章节中,您将看到如何存储和检索自动保存的表单数据以及如何在多线程的环境中使用表单数据。 选择数据存储库首先,为了保存表单数据,必须选择数据结构和存储库。这一点十分重要,因为为了存储临时数据,经常需要访问存储库。在示例应用程序中,数据每隔 10 秒为每个表单实例自动保存一次,但在真正的应用程序中,如果具有大量的并发用户,可能将这个间隔提高到 10 分钟更为合理一些。 自动保存的表单数据自然要存储在内存中,原因是这些数据只会存储很短的一段时间,之后会被更新的数据所替代。在用户提交表单之前,每个表单实例都会定期保存数据。提交之后,任何与所提交表单相关的临时数据都会从内存清除。如果用户不能提交表单或没有单击提交按钮就放弃了此页面,那么最后保存的数据会尽可能久地被存储在内存。当用户再次返回此表单时,如果所保存的数据还在,他就可以选择恢复此表单。 保存和恢复 JSF 组件的值每个表单实例的自动保存数据都保存在 清单 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); } } ... }
清单 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 组成。因此,很自然地会将所有这些数据地图都放到一个存储库地图内。 如果用户登录, 清单 6. 生成数据地图的惟一 IDpackage 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 堆有限且数据存储库也不应该消耗太多内存,因此有必要进行一些限制。在示例应用程序中,存储库所能存储的数据地图的实例是有限制的。当达到这个限值时,最旧的那个数据地图会从存储库中删除,以便进行垃圾收集。此机制已经构建到 清单 7给出了 清单 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; } ... }
清单 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); } ... } 在多线程环境中工作之前介绍过的 为数据存储库使用线程安全的包装器清单 9给出了这个包装器类,在示例应用程序中称为 清单 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(); } ... }
清单 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所有包含表单数据的地图对象都保存在存储库实例中,该实例的包装器被配置成 清单 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>
清单 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 部分,介绍过一个名为 将当前的视图数据保存于存储库清单 13给出了 清单 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 部分中,表单的自动保存不应该干扰应用程序逻辑。数据模型必须要能更新,且自动保存之后不应调用任何动作。保存了输入组件的值之后, 处理 JSF 阶段事件正如第 1 部分所介绍的,表单数据必须在 JSF 验证阶段,即 JSF 组件的所有值都已经由 JSF 框架转变和验证之后才保存。除了检验 如果标记此 Ajax 请求的头不存在,那么就表明用户一定是单击了 Web 表单的提交按钮。在这种情况下, 清单 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); } } } ... } 实现存储库的持久性在本节,我们会增强示例应用程序以在关机之前将存储库保存到服务器上并在服务器再启动时加以恢复。对象序列化是实现存储库持久性的一种最简单的方式,原因是 对象序列化有很多明显的缺陷,但在这种情况下,却是一个合理的持久性解决方案,原因是保存在存储库中的表单数据实例的数量是有限的。而且,即使存储库的临时数据在服务器由于某些与应用程序不相干的问题而当机的情况下丢失了,也不会造成任何严重后果。一种更为可靠的、基于关系型或对象数据库的解决方案将需要更多的 CPU 资源,其分配对于每 10 秒就更新一次的部分用户输入的存储而言很难做到。而且,存储库状态还应该能够不受服务器或应用程序重启的影响。 使用 ServletContextListener示例应用程序的 清单 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 容器会在应用程序初始化期间调用 清单 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); } } ... } 在 清单 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 容器将调用 清单 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显示了 清单 19. 配置 servlet 上下文侦听器<web-app ...> ... <listener> <listener-class>autosave.DataMapPersistence</listener-class> </listener> ... </web-app> 结束语在本系列的第 2 部分中,您了解了如何跨会话识别用户、如何为表单数据实现线程安全的存储库、如何保存和恢复 JSF 输入组件的值以及如何实现数据存储库的持久性。在本系列的最后一部分即第 3 部分中,您将会看到如何用所存储的数据来填充 JSF 表单,以及如何在 JSF 应用程序中使用更多 JavaScript 技巧。 下载
|
|