用环境变量定制应用部署
有时,当我们编写J2EE Web应用时,我们想要为应用部署人员提供一些灵活性。例如,或许你想要提供几个不同的应用版本,每一个版本满足特定的用户的需要。或许代码的有些部分需要主机名称和端口信息,这些只有在部署时才知道。或许,你只想在如何显示数据方面给应用部署人员一些灵活性。 你可以用环境变量增加这种灵活性。环境变量是可以在组件的部署描述符文件中定义的参数。应用组件通过JNDI按名称查找环境变量,用环境变量的值定制应用的行为或表示。 所有类型的应用组件都可以使用环境变量。Servlet、企业Bean、JSP 页面和用户tag都可以使用环境变量。环境变量必须在组件的适当部署描述符文件中定义,例如,Web组件在web.xml文件中定义,企业Bean在ejb-jar.xml文件中定义。 例如,假定你要为电子商务应用编写一个servlet。这个servlet发送email给客户,通知客户收到了一个订单。你的servlet需要授权的SMTP服务器的主机明、端口、登录名和口令。作为组件(servlet)开发者,你不必知道这些信息,只要部署者知道就行了。但是eamil功能要求这些信息,如何向servle提供这些信息呢? 一个方法是使用servlet部署描述符文件(web.xml)中的环境变量。首先,为主机名、端口、登录名、口令定义环境变量。然后,编写代码通过JNDI从环境中得到这些环境变量的值,在代码中使用这些环境变量的值。部署者在部署时利用部署工具为这些环境变量填上适当的值。程序运行时提取部署者设置的这些值,使用这些值访问要访问的服务器。 定义环境变量 用XML在组件的部署描述符文件中定义环境变量。如果使用部署工具(例如J2EE参考实现所带的部署工具程序),你就可以用GUI方式确定部署描述符。但是下面我们还是假定用文本编辑器手工编辑部署描述符。 环境变量有四个部分: · 描述: 定义在scription> tag 内的一个串。 · 名称:定义在<env-entry-name> tag内的一个串。 · 值:定义在<env-entry-value> tag内的一个值。 · 类名:定义在<env-entry-type> tag内的环境变量的类型 描述是可选的文字描述,出现在部署工具的用户界面上。它告诉部署者在确定环境实体引用时做什么。换句话说,它是一个可读描述,告诉部署者如何填写其他值。它也告诉部署者这个环境变量是否是可选的。 环境变量的名称是相对于JNDI上下文名“jndi:comp/env”的,组件用名称查找环境变量。所有环境变量都由它们的容器在JNDI上下文中注册。 环境变量的值是环境变量应取的值,格式是字符串。除了表示单个字符的类型java.lang.Character以外,允许作为环境变量的所有类型都有以串作为参数的构造函数。Env-entry-value tag包含了用于值的构造函数的串。 环境变量的类型是环境变量值的类型类名。必须是下面的类型之一: l java.lang.Boolean l java.lang.Byte l java.lang.Character l java.lang.Double l java.lang.Float l java.lang.Integer l java.lang.Long l java.lang.Short l java.lang.String
SMTP主机例子中的环境变量可以象下面这样: <env-entry> <description> Enter the host name for sending email </description> <env-entry-name>SMTP Host Name</env-entry-name> <env-entry-value> homer.springfield.ma.us </env-entry-value> <env-entry-type>java.lang.String</env-entry-type> </env-entry>
<env-entry> <description>SMTP port number for email </description> <env-entry-name>SMTP Port</env-entry-name> <env-entry-value>2101</env-entry-value> <env-entry-type>java.lang.Integer</env-entry-type> </env-entry>
<env-entry> <description> User authentication for SMTP server </description> <env-entry-name>SMTP User</env-entry-name> <env-entry-value>bart</env-entry-value> <env-entry-type>java.lang.String</env-entry-type> </env-entry>
<env-entry> <description> Password for SMTP user </description> <env-entry-name>SMTP Password</env-entry-name> <env-entry-value>D'oh!</env-entry-value> <env-entry-type>java.lang.String</env-entry-type> </env-entry>
使用环境变量 要在代码中使用环境变量很简单,用JNDI查找环境变量就行了。注意要使方法Context.lookup的结果与适当的类型相配。如下所示:
try { InitialContext ic = new InitialContext(); Context ctx = ic.lookup("java:comp/env"); String hostname = (String)(ctx.lookup("SMTP Host")); Integer port = (Integer)(ctx.lookup("SMTP Port")); String user = (String)(ctx.lookup("SMTP User")); String password = (String)(ctx.lookup("SMTP Password"));
sendEmail( emailText, port, hostname, user, password); } catch (NamingException nex) { ... } 环境变量与servlet初始化参数 在Web应用中,可以用servlet初始化参数代替环境变量定制servlet的行为。Servlet开发者在web.xml中用init-param tag定义servlet初始化参数,在servlet代码中用方法javax.servlet.GenericServlet.getInitParameter访问servlet初始化参数。servlet初始化参数的使用范围是定义它的servlet。 那么,对于具体的定制来说,如何在环境变量和servlet初始化参数之间做出选择呢?这个问题的答案依赖于定制的自然范围。就象全局变量的作用范围是程序的名称空间一样,环境变量的作用范围是JNDI名称空间。这将会导致组件之间的不必要的依赖。当定制只影响一个servlet时,servlet初始化参数是最好的选择。当定制涉及多个组件时,考虑使用环境变量。 代码示例 这个技巧的代码示例有两个部分。第一部分是servlet,打印应用的所有环境变量。这个servlet的最后用Context.listBindings方法列出了所有绑定在JNDI上下文java:com/env中的环境变量。下面的代码片断摘选于这个servlet的源代码: public void printEnvEntries(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
res.setContentType("text/html"); PrintWriter out = res.getWriter();
try { InitialContext ic = new InitialContext();
NamingEnumeration ne = ic.listBindings("java:comp/env"); out.println( "<HTML><HEAD><TITLE>Environment Entries</TITLE></HEAD>"); out.println( "<BODY><TABLE BORDER=1><TR><TH>Entry</TH>" + "<TH>Value</TH></TR>"); while (ne.hasMore()) { Binding ncp = (Binding)ne.next(); String objName = ncp.getName(); Object objObj = ncp.getObject(); out.println("<TR><TD>" + objName + "</TD>"); out.print( "<TD>" + objObj.toString() + "</TD></TR>"); } out.println("</TABLE></BODY></HTML>"); } catch (Exception e) { throw new ServletException(e); } } 这个方法对java:com/env中的每个对象迭代,用table的形式打印出每个环境变量的名称和文字表示。试一试部署这个应用,看一看定义在部署描述符文件中的环境变量。“运行示例代码”一节指导你如何部署应用。 这个技巧的代码示例的第二部分是一个定制tag,DateTag.java。说明了如何利用环境变量使组件(本例中是定制tag)可以定制。 DataTag是一个简单的tag,页面开发者可以用这个tag打印服务器的日期和时间。单独使用时(“<t:date/>” ),它用标准格式打印出日期和时间。如果用tag的格式属性定义了格式,打印时就使用所定义的格式。(标准类SimpleDateFormat定义了格式语法)。 部署者可以通过符号名用环境变量定义一列日期/时间格式。如果DateTag的格式属性值以$开始,那么,这个tag就查找这个属性命名的环境变量值的格式。例如,下面的环境变量就是在web.xml中定义的: <env-entry> <env-entry-name>LongTimeDateFormat</env-entry-name> <env-entry-value> 'Date:' EEEE, d MMMM yyyy', Time:' kk:mm:ss z </env-entry-value> <env-entry-type>java.lang.String</env-entry-type> </env-entry> 示例JSP包含了下面的文字: The server date in "Obtuse" format is <mytags:date format="$ObtuseTimeDateFormat"/>.
运行时显示的是: The server date in "Obtuse" format is 20030511-23:05:04EST.
这意味著部署者可以在应用的部署描述符文件中定义一列共用日期格式。应用开发者就可以在JSP文件中用DateTag引用这些日期格式。只要在部署描述符文件中改变格式的定义,应用中的日期/时间值的外观就改变了。 这是如何实现的呢?方法DateTag.setFormat检查格式是否以$开始,如果是的话,就装载相应的环境变量: public void setFormat(String sFormat) { if (sFormat.length() > 0 && sFormat.charAt(0) == '$') {
// Remove $ sFormat = sFormat.substring(1);
// Look up environment entry String format = null; try { InitialContext ic = new InitialContext(); format = (String)ic.lookup( "java:comp/env/" + sFormat); } catch (NamingException nex) { format = "[" + nex.toString() + "]"; } // If we found something if (format != null) { sFormat = format; } else { sFormat = "[" + sFormat + ": environment entry not set]"; } } _sFormat = sFormat; } 要部署和测试示例代码,请参照“运行实例代码”一节。试一试定义自己的格式,在应用的JSP文件中使用你定义的格式。 进一步可以让tag支持国际化。Java的Pet Store 示例应用(http://java.sun.com/blueprints/code/index.html - java_pet_store_demo)中的Data Access Object提供了一个如何国际化数据源的例子。
对Web层属性范围的讨论
Web层容器上下文接口(例如,ServletContext、HttpSession、和PageContext等)都提供了属性。属性是命名对象,在处理服务请求时可以放在运行环境中,并从运行环境对它进行访问。程序员可以使用属性把数据与容器内的各种对象联系起来。这个技巧介绍了属性范围的概念,描述了在不同的上下文中它是如何工作的。 什么是属性的范围? 属性范围是Web层中上下文对象的属性的生命期和可见性。有四种属性范围,它们是application 范围, session 范围, request 范围,和 page 范围。类PageContext提供了一组方法,从这些范围的每一个中按名称获得数据。它还提供了一个方法,在指定的范围中找到变量的值。JSP页面可以用PageContext接口中的方法访问指定范围的属性。在描述如何使用这些方法之前,让我们回顾一下这四种范围的语义。 在每一种范围中,设置和读取属性的方法分别称为setAttribute和getAttribute。用来访问属性的接口确定了属性的范围。 Application范围 用ServletContext接口设置和读取Application范围属性。Application范围对象的值在应用实例的生命期内有效。当重新部署应用或重新启动应用容器时,所有应用属性值才失效。Application范围的属性对应用的所有Web层组件是可见的。存储在Application范围的一个典型对象是对全局id服务器的同步化引用,这个对象被所有应用组件所共用。 Session范围 用HttpSession接口设置和读取Session范围。Session范围的属性在session期间有效。因此,一个请求设置的Session范围属性可以被多个后续的请求使用。Session范围属性的值在session超时或重新部署session应用或重新启动Web容器时失效。Session范围的属性只对同一个session的HTTP客户端是可见的,但是,多个请求可以共用这种属性。存储在session范围的一个典型对象是电子商务应用中的用户购物卡。 Request范围 用HttpServletRequest接口设置和读取Request范围的属性。这些属性不同于请求的POST或GET参数(这两种参数可以用HttpServletRequest.getParameter获取)。Request范围的属性只在HTTP请求服务期间才有效。如果一个Web层组件处理一个请求,把这个请求传递给另一个组件,那么这种属性对第二个组件及所有后续的组件是可见的。Request范围的属性在HTTP响应传送到客户端之前失效。这种属性只对提供单个请求服务的应用组件是可见的。Request范围的一个例子是这样的一个对象,servlet将这个对象传递给JSP页面实现格式化。 Page 范围 用PageContext接口设置和读取Page范围的属性。这种属性只在单个JSP页面上下文中可见,直到这个页面的全部输出处理完毕后就失效。特别地,这种属性对请求的传递或对其他Web资源的内容的包含是不可见的。Page范围属性的一个例子是一个JSP页面上的所有元素的具体Style。 下图用图形的方式说明了上述四种范围。
获取具体范围的属性 在JSP页中,PageContext属性有一对方法setAttribute/getAttribute,设置和读取属性。它还为方便地处理其他范围的属性提供了重载方法: public Object getAttribute(String name, int scope); public void setAttribute( String name, Object value, int scope); 这两个方法设置和获取指定范围的对象。这两个方法的参数列表中的scope的值必须是下面的在PageContext中已定义的final int值之一: · PageContext.APPLICATION_SCOPE · PageContext.SESSION_SCOPE · PageContext.REQUEST_SCOPE · PageContext.PAGE_SCOPE 另一个PageContext方法在所有范围中搜索指定名称的属性: public Object findAttribute(String name, int scope) 这个方法按顺序(从小范围到大范围)在每个范围内搜索指定名称的属性,首先在Page范围内搜索,然后在Request范围内搜索,接著在Session范围内搜索,最后在Application范围内搜索。它返回所找到的第一个属性的值。在较高级别设置属性的缺省值,在较低级别覆盖这个属性的值时,这种机制非常有用。例如,开发者定义了Application属性,color.background,这个属性为应用中的所有页面定义了背景颜色。每一个相继的范围,直到单个的Page范围,都可以为了具体的上下文重置color.background的值。 如果你需要知道具体属性所属的最小范围,使用: public int getAttributesScope(String name) 这将返回上面列出的final int之一。 下面的方法: Enumeration getAttributeNamesInScope(int scope) 返回指定范围内定义的所有属性的名称。
如何使用适当的范围 Application、Session、Request、和Page范围适用于不同的设计情况。 Application范围最适合应用组件之间的共享资源。Application范围内的数据应当是只读的。如果Application范围的状态是可写的,多个组件就可能会同时对它进行写操作,从而引起冲突。在分布式应用中,每个虚拟机维护自己的Application范围内存空间。虚拟机之间的Application范围内存空间不要求同步,因此,应用组件写入Application范围内存空间的任何数据通常只对本虚拟机内的组件可见。 Session范围是针对用户Session的。要记住的是保存在Session范围内的数据对扩展有影响。这是因为Session范围的状态所占的资源与用户Session的数量成比例(这一点与其他范围的状态不同)。如果应用是分布式的,Session范围状态的数据必须是可以序列化的。如果要照顾到扩展性,考虑把Session范围的状态保存在有状态会话Bean中,而不是保存在Web层。 Request范围适合于特定的单个请求中的数据。多个组件(例如,Servlet、JSP页面和定制tag)都可以访问Request范围的变量。Request范围的状态对Session范围的状态有较小的影响,这是因为在响应完成后它就失效了。 Page范围适合于单个页面中的组件用来互相通讯的数据。例如,迭代tag一般为其体内要用到的tag存储了Page范围的数据。Page范围不能用来在一个页面与它所包含的页面之间传递数据。 一般来说,考虑到合理使用资源,应尽可能使用较小的范围。还要避免混淆了组件之间的名称空间。总之: l 如果数据只在一个页面用到,就用Page范围。 l 如果数据在多个页面用到,就用Request范围。 l 如果数据在多个请求中用到,就用Session范围。 l 如果数据在多个Session中用到,就用Application范围。 l 如果数据对于多个Session来说是可写的,就要考虑使用实体Bean或消息。 示例代码 这个技巧的示例代码包括一个JSP页面(ShowAllAttrs.jsp)和一个定制tag(ShowAttrsTag.java)。ShowAttrsTag打印tag范围属性表示的范围内所有属性的名称和值组成的表。如果没有指定范围属性,就打印从findAttribute()得到的所有属性/值对组成的表。它还用颜色区分属性表示属性所属的不同范围。 JSP页面使你可以在各种范围内设置属性进行测试,观察它们的可见性和生命期。这个页面上方的Form允许你设置或移去任何范围内的属性/值对。填好这个Form后点击Submit运行JSP页面中的脚本代码,设置或移去所要求的元素。JSP页面的其他部分首先分别打印每个范围的所有属性,然后在页面的下方用findAttribute列出全部属性。JSP页面使用tag打印表,因此,JSP页面中的代码是简单的: <H2>Application Scope Attributes</H2> <mytags:showattrs scope="application"/> <p> <H2>Session Scope Attributes</H2> <mytags:showattrs scope="session"/> <p> <H2>Request Scope Attributes</H2> <mytags:showattrs scope="request"/> <p> <H2>Page Scope Attributes</H2> <mytags:showattrs scope="page"/> <hr> <H2>All Attributes</H2> <mytags:showattrs/> 对示例代码作如下测试: 1. 设置一个Application范围的属性。重新打开浏览器,访问JSP页面。你能看到所设的属性吗? 2. 设置一个Application范围的属性,然后在Session范围内用不同的值设置同一个属性。你能看到两个不同的值吗?findAttribute返回的是什么值?Session范围的属性能够存在于多个请求吗?再打开一个浏览器,访问JSP页面。Session范围的属性设置了吗? 3. 在Application范围和Session范围用不同的值设置同一个属性。然后在Request范围设置同一个属性。FindAttribute返回的是什么值?确认Attribute Name文本框是空的,点击Submit按钮。Request属性有什么变化?findAttribute返回的值有什么变化?删除这个Session属性,对Request属性重作这个测试,同样地对Page属性重作这个测试。 4. 修改这个程序和Form使得一次可以设置多个属性。更进一步可以为所设置或移去的每个属性指定不同的范围。 花几分钟作如上的测试,观察不同范围的属性的行为,可以帮助你了解属性范围。关于属性范围的更多指导,请看“Designing Enterprise Applications with the J2EE Platform, Second Edition”中的“Web-Tier State”一节(http://java.sun.com/blueprints/guidelines/designing_enterprise_applications_2e/web-tier/web-tier5.html - 1083750)。 运行示例代码 上述两个技巧的归档文件可以在网上下载(http://java.sun.com/developer/EJTechTips/download/ttnov2003.ear)。应用上下文的root是/ttnov2003。下载的EAR文件还包含了全部源代码。 你可以在J2EE参考实现中用如下部署工具程序部署应用归档文件(ttnov2003.ear): $J2EE_HOME/deploytool -deploy ttnov2003.ear localhost 用服务器所在的主机名代替localhost。如果服务器在本机,主机名就是localhost。就可以用http://localhost:8000/ttnov2003访问应用。 对于其他的与J2EE兼容的实现,使用你的J2EE服务器部署工具将应用部署到你用的平台上。 关于如何运行应用,看index.html欢迎页面。 欢迎页面就象下面这样: |