J2EE和XML开发——用户接口(一) 作者 KURT A. GABRICK DAVID B. WEISS 出处 J2EE and XML Development第五章 地址 <http://www.manning.com/gabrick>
一. 引语 通常,为J2EE应用创建健壮的表示层是富有挑战性的尝试。这是因为绝大多数J2EE应用是基于Web的瘦客户端应用。在本文中,我们将检测一些在J2EE应用的用户接口设计中十分常见的问题并讨论如何使用XML技术来克服这些问题的方法。 我们首先探讨瘦客户端开发的特征以及你在构建和维护表示层当中可能遇到的挑战。然后我们通过使用纯J2EE技术构建这样一个表示层的例子来说明当前的J2EE架构的缺点。 本文余下的部分将集中讨论通过使用XSLT技术克服使用纯J2EE的局限性。首先我们开发了一个基于XSLT的表示层,然后我们通过介绍一个Web发布框架的使用解释使用第三方API的好处和不足。 本文的目标不是告诉你某种架构比另一种更好,而是希望你了解更多的选择,看它们如何使用并且理解这些选择积极的和消极的方面。
二. 构建瘦客户端用户接口 本文我们的焦点是基于Web的,瘦客户端分布式应用。在讨论如何克服有关这些应用的问题的细节之前,我们应该花些时间讨论这些问题的内容以及为什么为它们构造接口如此的困难。 如果你从前开发过基于Web的应用,那么毫无疑问你不会对瘦客户端体系感到陌生。直到最近,为应用在服务器端生成用户接口不是很困难。但是,最近的两个相关的要求使得开发和维护你的表示层部件更具挑战。
2.1 服务不同的设备 第一个挑战是关于不断发展的嵌入式Web设备。好象现在所有的电子设备都有Web浏览器这些智能设备包括手机、PDA和冰箱(听起来像个笑话)。问题是它们中的有些识别HTML的子集,而另一些又需要完全独立的标记语言。例如嵌入了WAP的手机需要使用无线标记语言(WML)才能请求和浏览Web页。 使用传统的J2EE(或者ASP、ColdFusion)创建一个能处理符合各种不同设备要求的独立的用户接口是十分困难的。为了在J2EE的Web应用的服务Web浏览器和客户手机,你必须开发和维护两个独立的表示层。一个可能用于生成HTML,另一个则为WML。对于应用升级可能使这两层都需要更新。
2.2 服务多个地区 另一个讨厌的关注点是为不同地区的用户提供服务。明显的挑战是决定用户喜欢的语言以及使用该语言提交Web页面。这就意味着应用中需要一些机制在不同的自然语言中转换。所有的内容都需要被翻译,包括图形的和JSP中静态的文字信息。 其次,可能不那么明显的是不同地区的文化差异。不仅仅是页面中的文字内容,它可能有必要为新的区域改变用户界面的整个结构。你可能想要改变颜色,页面布置甚至界面的导航。这些因素涉及到心理学、市场学或其他的主题。 如果你尝试过使用纯J2EE有效的开发出表示层为多语言或地域服务,你已经感受过了这种挑战的大小。如果没有,我们会在本文余下的部分突出这些可怕的特征。使用传统方法尝试解决这些需求是导致产生大量的冗余,通常JSP和Servlet的误用将导致维护和扩展的噩梦。
2. 3 本文的例子 为了真实的了解问题的核心和XML的解决之道,让我们通过一个例子来说明。处于普遍性和真实性,我在应用中使用简单的用户接口和功能。这是一个股票交易网站并且这里的功能是向用户提交一份股价的信息报表。 这里是本文例子应用的一些需求定义: ·股价页面必须可以在PC的浏览器和WAP手机上显示 ·股价页面能在两个区域提交,美国和英国,每个区域需要各自的特定信息。 ·股价页面中显示的股票价格必须使用各自货币形式表达,USD和GBP。 你可以发现上面的需求使得我们必须提供四个独立的页面。 图5.1是在PC浏览器下显示的区域为美国的页面
 图5.2是在PC浏览器下显示的区域为英国的页面
 图5.3是在手机WAP下显示的区域为美国的页面
 图5.4是在手机WAP下显示的区域为英国的页面
 在本文余下的部分我们使用多种技术创建和重创建这四个页面。在三中我们仅使用J2EE 并看看在什么地方会出现阻力。而在四和五中,我们将引入XML技术为页面创建可重用和统一的接口并能够支持多种设备和语言。
三. 纯J2EE解决方案 在我们讨论任何新的用于用户接口创建的基于XML的体系结构模型前,我们有必要理解为什么我们需要它们。在这个部分我们首先探讨纯J2EE方案和开发我们例子系统时的局限性。 3.1 J2EE表示层开发工具 J2EE表示层组件包括Applets、Servlets、JSP、JavaBeans、JSP定制标记和Filters,除了Applet外,所有的这些组件在J2EE服务器端的Web容器中C/S。这些应用几乎都是瘦客户端且基于Web的。在这种体系结构下的J2EE服务器端组件需要利用MVC模式设计并与其他组件合作。成功实现了MVC模式后,各个组件就相互独立了。在J2EE表示层中,Servlets通常作为控制器。它们接收客户的请求,操作模型然后返回适当的视图。在一些简单和设计不良的系统中,servlet自己可能用来提交视图。这种servlet代码中包含了静态的HTML内容,所有的开发人员都不喜欢这种风格。 更高级的解决方案是让你的控制器选择合适的JSP作为视图并且将请求转发给它。JSP更易维护而且可以利用定制标记和JavaBeans将应用逻辑从静态页面中分离出来。这种使用JSP的表示层应用模型被称为Model 2。图5说明了整个的规则。 3.2 J2EE与MVC架构中的问题 J2EE框架是采用MVC瘦客户应用的适配器,在大多数案例中,它并不能完全的将表示层逻辑从你的应用数据中分离出。在这部分文章中,我们一个一个的探索一些对所有的纯J2EE表示层实现很常见的目标和挑战。
3.2.1 强制分离逻辑和内容 Servlets和JSP可以使用许多方法实现。在图5中,我们见到了Model 2架构,其中JavaBeans被用做模型,Servlets作为控制器,JSP作为视图。
 考虑下面的请求-响应流程: 1. Web请求被入Filter拦截并做预处理,如认证或记录然后重定向到一个Servlet。 2. 这个Servlet作一些轻量处理并且与应用的商业逻辑层交互,最后转发请求到JSP提交。 3. JSP联合使用JavaBeans和定制标记来提交合适的表示法并且将请求转发给出Filter做响应用户之前的后续处理。 MVC模式的成功实现依赖于组件间清晰的角色划分。当严格的执行这些划分时,各组件的可重用性和可维护性能大大提高。但是如果没有分清它们的角色,这样的MVC架构的使用将使原本就很复杂的系统更加难以维护和扩展。
3.2.2 模板语言的局限性 JSP是动态页面的模板,它包含逻辑和数据的集合体。它们和其他流行的动态模板语言(ASP和ColdFusion)类似。在这种模板语言中完全分离应用逻辑和数据是非常困难的,有时甚至是不可能的。例如,下面这三行JSP就混合了页面结构,静态内容以及代码。 <html> <h1>Hello <%=request.getParameter(uName)%> </h1> </html> 当JSP最初开发出来时,它就被广泛的批评,因为它将Java代码直接嵌入HTML中。导致HTML页面作者和JSP开发者需要在同一份原文件上工作。作为企业应用,通常有明显的需要将UI设计者和代码编写者的角色分开。为了解决这个问题,JSP加入了定制标记和JavaBeans。这些附加功能使JSP将大多数的Java代码封装到了包含复杂显示逻辑的定制标记和数据结构的JavaBeans中。XML标记现在已经替代了大多数的陈旧的JSP代码。 虽然JSP中的代码减少了,但是整体的复杂度却增加了。特别是标记总是同时包含显示标记和Java代码,使得一些页面更难维护。页面设计师不希望HTML的一点变化就需要修改和重编译标记文件;而且找到那些需要改变的代码也十分烦人。
3.2.3 代码冗余 我们在前面说过,J2EE应用的多设备支持和国际化需求使得纯J2EE方案更加多变。在下一步部分中,建造我们的股票例子页面需要2到4个JSP页。当我们再增加地区和设备时,页面数量还要翻番。考虑考虑要开发和维护100个页面而仅仅为了满足5个功能将是一个怎样的担子啊!再想想,要在20个不同的JSP页面中做同样改动,单独测试它们,还要在应用中重复这些测试,多可怕的一件事。如果是商务逻辑的改变,你庆幸将它们封装到了标记或JavaBean中。如果是显示逻辑的改变,那么你可要遭受许多创伤了。
3.3 使用J2EE构件我们的例子 理论已经够多了,现在我们将理论化为实际。仅使用J2EE服务我们的例子,需要用到下面的组件: ·一个Sevlet接收客户请求并与应用逻辑层交互获取股价信息。 ·每种设备一个JSP用来提交股价列表。 3.3.1 处理请求 我们要求Servlet接收股票列表的用户请求,获得这个列表,并且选择一个适当的JSP页提交请求。在仅使用J2EE的场景中,我们的Servlet将转发股价表到4个JSP之一,这取决于用户的设备种类和地区。 首先,Servlet将需要提取用户的股票引用列表,我们假设我们的应用在Http会话中使用字符串存储用户的标识。获取用户身份后,我们调用方法从应用逻辑层获取JDOM值对象形式的股票列表。 HttpSession session = request.getSession(false); ListBuilder builderInterface = ... // ... validate session object exists String userId = (String) session.getAttribute("userId"); org.jdom.Document quoteList = builderInterface.getWatchList(userId); 然后我们使用JavaBean对象包装这个quoteList并且在请求对象中存储它,以便稍后可以被JSP组件获取。我们稍后在讨论JavaBean的代码。 XMLHelperBean helper = new XMLHelperBean(quoteList); session.setAttribute(helper, helper); 最后一步是决定哪一个JSP页面用来提交。要做到这一点,我们需要确定用户使用的设备类型和地区。首先我们编写一个方法来找出用户的设备信息,可以利用User-Agent HTTP头信息。 private String getOutputType(HttpServletRequest request) { String userAgent = request.getHeader("User-Agent"); // compare to list of WAP User-Agents // for simplicity, we'll only try one here if (userAgent.indexOf("UP.Browser") >= 0) return "wml"; return "html"; } 在真实世界场景中,这个Servlet将需要维护一个所有user-agent和它们MIME类型的字典。例如,我们仅输出WML如果某人使用UP.Browser电话浏览器。调用下述方法将设置输出格式。现在我们需要一个方法选择一个基于输出格式和用户区域的JSP。为了得到它,我们使用Accept-Language HTTP头,该头信息包含了一个按顺序列出的用户浏览器设置的语言列表。 private String getForwardURL( HttpServletRequest request, String outputType) { String result = null; if (outputType.equals("html")) result = "/watchlist/watchlist.html.en_US.jsp"; else result = "/watchlist/watchlist.wml.en_US.jsp"; Enumeration locales = request.getHeaders("Accept-Language"); while (locales.hasMoreElements()) { String locale = (String) locales.nextElement(); if (locale.equalsIgnoreCase("en_GB")) if (outputType.equals("html")) return "/watchlist/watchlist.html.en_GB.jsp"; else return "/watchlist/watchlist.wml.en_GB.jsp"; } return result; } 上面的方法将从四个JSP页中选择合适的那个。现在只差把这些方法在我们的ServletD的请求方法中联合起来啦。 String outputType = getOutputType(request); String forwardURL = getForwardURL(request, outputType); context.getRequestDispatcher(forwardURL).forward(request, response); 列表1显示了我们的新Servlet的完整代码 列表1 The watch list JSP servlet import org.jdom.*; import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; /** * The stock watchlist servlet with JSP */ public class WatchListJSPServlet extends HttpServlet { private ListBuilder builderInterface = new ListBuilder(); private ServletConfig config; private ServletContext context; public WatchListJSPServlet() { super(); } public void init(ServletConfig config) throws ServletException { this.config = config; this.context = config.getServletContext(); Listin} public void doGet(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { // get userid from HttpSession HttpSession session = request.getSession(false); if (session == null) { context.getRequestDispatcher("/login.jsp") .forward(request, response); return; } String userId = (String) session.getAttribute("userId"); Document quoteList = builderInterface.getWatchList(userId); XMLHelperBean helper =new XMLHelperBean(quoteList); request.setAttribute("helper", helper); String outputType = getOutputType(request); String forwardURL =getForwardURL(request, outputType); context.getRequestDispatcher(forwardURL) .forward(request, response); } private String getOutputType(HttpServletRequest request) { String userAgent = request.getHeader("User-Agent"); // compare to list of WAP User-Agents // for simplicity, we'll only compare one here if (userAgent.indexOf("UP.Browser") >= 0) return "wml"; return "html"; } private String getForwardURL(HttpServletRequest request, String outputType) { String result = null; if (outputType.equals("html")) result = "/watchlist/watchlist.html.en_US.jsp"; else result = "/watchlist/watchlist.wml.en_US.jsp"; Enumeration locales = request.getHeaders("Accept-Language"); while (locales.hasMoreElements()) { String locale = (String) locales.nextElement(); if (locale.equalsIgnoreCase("en_GB")) if (outputType.equals("html")) return "/watchlist/watchlist.html.en_GB.jsp"; else return "/watchlist/watchlist.wml.en_GB.jsp"; } return result; } public void doPost(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } 3.3.2 获取和使用XML数据 我们的Watch List Servlet利用了ListBuilder类,代码可以从本文网站上下载(地址 <http://www.manning.com/gabrick>)ListBuilder返回一个JDOM文档,它包含了XML格式的股票信息列表。列表2说明我们例子使用的XML数据集。它包括三个股票价格,注意多个price节点包含在对应的quote元素中,每个价格都有不同的货币表示。这使我们可以在页面生成时采用用户本地货币显示价格。 列表2 股票引用XML数据集 <?xml version="1.0"?> <quote-list date="Nov. 4, 2001" time="9:32 AM EST"> <customer first-name="Kurt" last-name="Gabrick" id="9999"/> <quote symbol="SRMC" name="Sierra Monitor Corporation"> <price amount="2.00" currency="USD"/> <price amount="1.05" currency="GBP"/> <price amount="4000.00" currency="MXP"/> </quote> <quote symbol="IBM" name="International Business Machines"> <price amount="135.00" currency="USD"/> <price amount="67.75" currency="GBP"/> <price amount="230000.00" currency="MXP"/> </quote> <quote symbol="ORCL" name="Oracle Corporation"> <price amount="15.00" currency="USD"/> <price amount="7.75" currency="GBP"/> <price amount="30000.00" currency="MXP"/> </quote> </quote-list> 在获得JDOM文档后,我们用JavaBean组件把它包装起来。这么做的原因是保持XML操作代码在JSP文件之外。通过在HttpRequest对象中存储这个JavaBean,使它在任何JSP提交视图时都可以使用。此Bean的代码如列表3所示。注意这个Bean同时还是一个定制标记,扩展了类javax.servlet.jsp.tagext.TagSupport并且实现了doStartBody方法。这样我们就可以在Bean中动态生成股价信息表的显示标记并减少了JSP中的代码。
列表3 XMLHelper JavaBean/Tag import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.jsp.*; import javax.servlet.jsp.tagext.*; import java.io.*; import java.util.*; import org.jdom.*; public class XMLHelperBean extends TagSupport { //JavaBean属性 private String firstName; private String lastName; private String quoteTime; private String quoteDate; private Vector quotes = new Vector(); //定制标记属性 private boolean useLinks=false; private String currency = "USD";
public XMLHelperBean(Document doc) { Element root = doc.getRootElement(); this.quoteTime = root.getAttributeValue("time"); this.quoteDate = root.getAttributeValue("date"); Element customer = root.getChild("customer"); this.firstName = customer.getAttributeValue("first-name"); this.lastName = customer.getAttributeValue("last-name"); // build quote list List quoteElements = root.getChildren("quote"); Iterator it = quoteElements.iterator(); while (it.hasNext()) { Element e = (Element) it.next(); Quote quote = new Quote(); quote.symbol = e.getAttributeValue("symbol"); quote.name = e.getAttributeValue("name"); List priceElements = e.getChildren("price"); Iterator it2 = priceElements.iterator(); while (it2.hasNext()) { Element pe = (Element) it2.next(); quote.prices.put( pe.getAttributeValue("currency"), pe.getAttributeValue("amount")); } } } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getQuoteTime() { return quoteTime; } public String getQuoteDate() { return quoteDate; } // supplied only for consistency with // JavaBeans component contract - inoperative public void setFirstName(String s) {} public void setLastName(String s) {} public void setQuoteTime(String s) {} public void setQuoteDate(String s) {} public void setUseLinks(String yesno) { if (yesno.equalsIgnoreCase("yes")) useLinks = true; else useLinks = false; } public boolean getUseLinks() { return useLinks; } public void setCurrency(String currency) { this.currency = currency; } public String getCurrency() { return currency; } public int doStartTag() { //在HTML或WML中动态创建表格 try { JspWriter out = pageContext.getOut(); out.print("<tr><th>Stock Symbol</th>"); out.print("<th>Company Name</th><th>Last Price</th>"); if (useLinks) out.print("<th>Easy Actions</th>"); out.print("</tr>"); for (int i = 0; i < quotes.size(); i++) { Quote q = (Quote) quotes.get(i); out.print("<tr><td>"); out.print(q.symbol); out.print("</td><td>"); out.print(q.name); out.print("</td><td>"); out.print(q.prices.get(currency)); out.print(" "); out.print(currency); out.print("</td>"); if (useLinks) { out.print("<td><a href=\"http://www.exampleco.com/ buyStock?symbol="); out.print(q.symbol); out.print("\">buy</a> "); out.print("<a href=\"http://www.exampleco.com/sellStock?symbol="); out.print("\">sell</a></td>"); } out.print("</tr>"); } } catch(IOException ioe) { System.out.println("Error in XMLHelperBean.doStartTag(): " + ioe); } return(SKIP_BODY); } private class Quote { Hashtable prices = new Hashtable(); String symbol; String name; } } 使用列表3的类,我们可以将JSP中的Java代码全部分离出来。希望这个小的例子带给你的设计和开发必要的灵感以便使你的JSP代码无关。 3.3.3 提交输出 现在我们需要使用JSP了,我们可以开发一个包含许多控制结构的复杂JSP。这样的页面能处理两种输出格式和两个区域,但是它想必太复杂和难于维护,也难以独立于数据之外。 我们也可以会开发两个JSP而不是一个。其中一个产生HTML另一个则是WML。如果页面布置不会因区域改变而改变,我们可以将所有与本地文字和图象相关的逻辑放到我们的定制标记代码中。但是,我们将仍然不得不在分支语句中处理本地化。 所以我决定开发四个独立而简单的JSP页,每个相对于一个设备和地区。列表4到7,分别说明了这四个JSP页,每个JSP都从请求对象中获得XMLHelper JavaBean的引用用来提交各自的输出。
列表4 <jsp:useBean name="helper" scope="page" class="examples.chapter5.XMLHelperBean"/> <%@ taglib uri="example.tld" prefix="helperTag" %> <html> <head><title>Your Watch List</title></head> <body> <h1>Your Stock Price Watch List</h1> <h3> Hello, <jsp:getProperty name="helper" property="firstName"/>! </h3> <h3> Here are the latest price quotes for your watch list stocks. </h3> <p><i> Price quotes were obtained at <jsp:getProperty name="helper" property="quoteTime"/> on <jsp:getProperty name="helper" property="quoteDate"/>. </i></p> <table cellpadding="5" cellspacing="0" border="1"> <helperTag:printData useLinks="yes" currency="USD"/>// #1 </table> </body> </html> #1处我们提供useLinks=yes,我们的定制标记打印HTML表格,它包括四列,每列都是一个超连接指向购买和抛售各个股票的页。设置currency=USD将用美圆作为价格的货币单位。列表4显示的是美国地区HTML版本的JSP,列表5给出的是英国地区的同一版本。请注意两者的相似和不同之处。美国 页面使用不那么正式的语言并用USD描述价格。英国版本则语言更加正式并使用英镑描述价格。但是,它们都使用同一数据源且都没有包含Java代码。
列表5 <jsp:useBean name="helper" scope="page" class="examples.chapter5.XMLHelperBean"/> <%@ taglib uri="example.tld" prefix="helperTag" %> <html> <head><title>Your Watch List</title></head> <body> <h1>Your Stock Price Watch List</h1> <h3> Greetings, Mr.<jsp:getProperty name="helper" property="lastName"/>! </h3> <h3> Here are the latest prices on your stocks of interest. </h3> <p><i> Price quotes as of <jsp:getProperty name="helper" property="quoteTime"/> On <jsp:getProperty name="helper" property="quoteDate"/>. </i></p> <table cellpadding="5" cellspacing="0" border="1"> <helperTag:printData useLinks="yes" currency="GBP"/> </table> </body> </html> 现在余下的部分是开发WML版本的页面。这些页面包含的文字信息稍微少一些而且没有直接的指向购买页的连接。如果你需要回忆一下这些页面是怎么样的,跳到图1到4去看看。
列表6 美国地区WML版本Watch List JSP <jsp:useBean name="helper" scope="page" class="examples.chapter5.XMLHelperBean"/> <%@ taglib uri="example.tld" prefix="helperTag" %> <wml> <card id="main" title="Your Watch List"> <do type="accept" name="do-back" label="Back"> <go href="http://www.exampleco.com/home.wml" /> </do> <do type="accept" name="do-buy" label="Buy Shares"> <go href="http://www.exampleco.com/buyShares" /> </do> <do type="accept" name="do-sell" label="Sell Shares"> <go href="http://www.exampleco.com/sellShares" /> </do> <p><b> Hello,<jsp:getProperty name="helper" property="firstName"/>! </b> </p> <p>Here are your latest watch list price quotes:</p> <table columns="3"> <helperTag:printData useLinks="no" currency="USD"/> </table> </card> </wml>
列表7 英国地区WML版本Watch List JSP <jsp:useBean name="helper" scope="page" class="examples.chapter5.XMLHelperBean"/> <%@ taglib uri="example.tld" prefix="helperTag" %> <wml> <card id="main" title="Your Watch List"> <do type="accept" name="do-back" label="Back"> <go href="http://www.exampleco.com/home.wml" /> </do> <do type="accept" name="do-buy" label="Buy Shares"> <go href="http://www.exampleco.com/buyShares" /> </do> <do type="accept" name="do-sell" label="Sell Shares"> <go href="http://www.exampleco.com/sellShares" /> </do> <p><b>Greetings, Mr. <jsp:getProperty name="helper" property="lastName"/>! </b></p> <p>Here are the latest prices on your stocks of interest.</p> <table columns="3"> <helperTag:printData useLinks="no" currency="GBP"/> </table> </card> </wml> 3.3.4 结果分析 我们认为你会承认,使用J2EE实现多设备,多地区的表示层不是一件简单的事情。设想一下如果要扩展我们的例子以便支持更多的地区和设备需要多少努力。这是一个基于XML的表示层能发挥它作用的问题领域。当实质上地为一些应用的不同的视图提供服务总是一种挑战的时候,那些XML工具的出现会使你的开发过程多少更容易一点。
导读 本文的第二部分将介绍如何使用J2EE联合XML技术(XSLT)来更优雅的解决上述股票的例子并且介绍了流行的XML Web发布框架Cocoon。
更多信息 1. <http://www.theserverside.com/> 2. <http://www.javaworld.com/>
声明
本文由starchu1981保留版权,如果需要转贴请写明作者和出处。 
|