通过前面章节的学习,我们了解到Velocity主要用作MVC框架View组件的脚本语言。Servlets是Java下MVC框架里用得最多的编程方式。通过把Servlets用作控制器(controller),Velocity用作显示层(View),把Servlets或JavaBeans作用模型(model),这样就创建了一个理想的Web开发全面解决方案。在这一章里,我们将学习如何在Servlets里使用Velocity。
使用Servlets
在开始学习这章内容之前,你必须充分理解Servlets是什么?以及它是如何工作的?如果对此你已经很熟悉了,那么你可以直接跳到“用VelocityServlet扩展Servlets”节进行深入学习。
在动态Web页面开发的早期,服务端语言,比如ASP、JSP等主要用于在HTML页面里嵌入服务器端执行的代码。当浏览器请求这个页面的时候,服务器立即对所请求的ASP/JSP页面进行解析,执行里面的语句,并向用户返回一个纯粹的HTML页面,有时还要附带返回客户端执行的JavaScript或VBScript脚本。混合HTML和服务器端代码并不容易,而且,服务器端脚本语言还要受到一些限制,比如服务器平台支持、服务器支持等比较有限。
Servlets把代码从HTML中提取出来,把它放到服务器上,这样,你就可以使用全功能的Java语言进行Servlets逻辑开发。Servlets也能进行分布式处理,而且可以使用模板来向用户提供信息显示。
Servlets的通用格式
在进行深入学习之前了解传统的Servlets结构是非常有必要的。Listing 12.1显示了一个简单的Servlets。
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.naming.*;
public class ViewAccount extends HttpServlet {
public void init() throws ServletException {
}
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<HTML>");
out.println("</HTML>");
} catch (SQLException e) {
e.printStackTrace();
}
}
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
Listing 12.1 传统servlet代码
当有一个web浏览器请求达到时,一般情况下,请求的方式为POST或GET。在上面的代码中你可以看到,其中有一些方法将用于处理这两种请求,分别为doPost()和doGet()方法,其中doPost()方法只是简单委派给了doGet()方法。在doGet()方法内部有两个重要的变量:request和response。request对象用于把用户信息转换成servlet能够理解的信息。特别的,用户在HTML窗体里的输入将作为文本进行处理。response对象主要负责向用户的浏览器返回标准的HTML页面。response对象返回的信息包括HTML、XML和图片。response对象也可以和PrintWriter对象一起协同工作,以用于写入信息并返回给用户。
Servlets不能使用普通的Web服务器作为主机,只能使用应用程序服务器(application servers)作为主机。可以作为Servlets主机的软件有Tomcat、JBOSS、Resin、WEBlogic、apusic等。在很多情况下,应用程序服务器被用于和Web服务器一起协同工作,比如IIS或Apache等。当Web服务器得到一个servlet请求时,这个请求被移交给应用程序服务器执行。应用程序服务器使用java编译器来创建一个可执行的Servlets映像,这个Servlets映像将在JVM里执行。我们将在下一章演示这个话题的示例。
用VelocityServlet扩展Servlets
在某种程度上,你可以使用Velocity引擎和servlet,通过Velocity模板语言书写代码来为用户创建信息输出。为了让Servlets的使用更容易,我们将通过使用一个名叫VelocityServlet的基类和名叫handleRequest()的方法来代替传统Servlets的HttpServlet和doGet()/doPost()方法。
handleRequest()方法传递三个参数HttpServletRequest、HttpServletResponse和Context来响应GET/POST请求。HttpServletRequest、HttpServletResponse对象和doGet()、doPost()方法接受的是同一个对象(代码见Listing 12.1)。上下文对象是一个用于在Servlets里使用的Velocity引擎的上下文。我们将在上下文对象里为用于向用户显示响应的Velocity模板放置信息。
handleRequest()方法返回一个模板对象,该方法自动把传递进来的上下文对象进行合并。因而,在handleRequest()方法里的代码,主要完成设置上下文和合并返回的Velocity对象等操作。如果返回的模板对象为null,这些代码就不能完成合并操作。在这种情况下,和传统的Servlets代码一样,这些代码将向用户浏览器返回一些放置在response对象里的PrintWriter对象里的东西。
基本的Velocity Servlet代码
handleRequest()方法示例代码见Listing 12.2,用于向浏览器简单输出字符串。
import java.util.Vector;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.Template;
import org.apache.velocity.context.Context;
import org.apache.velocity.servlet.VelocityServlet;
import org.apache.velocity.exception.*;
public class VelocityServletExample extends VelocityServlet {
public Template handleRequest( HttpServletRequest request,
HttpServletResponse response,
Context context ) {
Vector v = new Vector();
v.add("one");
v.add("two");
v.add("three");
context.put("list", v);
Template template = null;
try {
template = getTemplate("displaylist.vm");
} catch( Exception e ) {
PrintWriter out = response.getWriter();
out.println("Error getting template");
}
return template;
}
}
Listing 12.2 A Velocity servlet example.
当浏览器调用Listing 12.2里的代码时,初始化一个Velocity对象,并在里面放置了三个字符串对象。Velocity对象通过下面的语句被放入上下文中。
context.put("list", v);
注意,我们在上下文里放置的是Vector对象实体(java.util.Vector)。接着,声明了一个模板对象,并使用getTemplate(String)方法来从服务器的磁盘上定位和加载模板。如果在获取模板文件的过程中出现错误时,response对象里的PrintWriter被获得,并向其中写入错误信息。最后一个操作是返回模板对象。如果某些错误导致这个对象为null,系统将不能完成合并操作;否则,从服务器读入的模板将和上下文一起被合并。Listing 12.3为这个示例所使用的Velocity模板。
<?xml version="1.0" encoding="ISO-8859-1" ?>
<list>
#foreach( $value in $list )
<number>$value</number>
#end
</list>
Listing 12.3 The displaylist.vm Velocity code.
Listing 12.3里的模板不是HTML格式的,我们使用XML来演示Velocity模板并不仅限于使用HTML格式。了解这个示例是如何执行的非常重要。还记得“list”关键字和Vector对象关联,在模板里的Velocity模板语言代码通过#foreach循环指令使用Vector。回想一下在List对象(如Vector)里使用#foreach指令的定义我们就可以发现,迭代器(iterator)自动提取并用于获取所有在List(在这种情况下还可以是Vector)里的独立对象。
所有来自Vector的每一个对象都被提出并放置到引用$value处。既然字符串不是混合对象,我们就可以简单地输出它的值(在<number>元素标记里)。Figure 12.1演示了执行结果。
Figure 12.1 Output from the Velocity template.
HttpServletRequest和HttpServletResponse对象被传递给Servlets代码,同时被放置在两个上下文常量对象中:
■ VelocityServlet.REQUEST—Stored as req
■ VelocityServlet.RESPONSE—Stored as res
每一个对象都能通过直接调用引用名(req和res)的方式用于Velocity模板中。
#set($username = $req.getParameter('username'))
创建一个MVC应用程序
我们的第一个Velocity示例相当简单,现在我们来搞一个稍复杂一点的示例。在这一节里,你将使用Servlets来构建一个MVC应用程序,其中:Servlets用作控制器(controller)、Velocity模板作用视图(view)、JavaBeans作用模型(model)。这个应用程序是一个具有以下四个功能的CD数据库:
■增加一个CD,同时返回一个thank-you响应
■为特定的CD增加歌曲track,同时返回一个thank-you响应
■通过特定的艺术家返回其所有的CD
■获取特定CD的全部歌曲track
Figure 12.2显示了增加一个新CD时的情况;Figure 12.3显示了当你使用特定的艺术家搜索CD时的情况;Figure 12.4显示了一个特定CD的所有歌曲时的情况。
Figure 12.2 Adding a new CD.
Figure 12.3 Listing all CDs for an artist.
数据库结构
这个应用程序使用了一个数据库来保存CD和所有歌曲的信息。在这个测试环境,我们选择MYSQL数据库来存在数据,当然,你也可以选择其他类型的数据库。下面的SQL建表命令显示了CD和歌曲表的数据结构。
create table cd (
id int not null primary key auto_increment,
title varchar(128),
artist varchar(64),
tracks int);
create table tracks(
id int not null primary key auto_increment,
cd_id int,
name varchar(64),
length varchar(16));
Figure 12.4 Listing all tracks on a CD.
数据库访问
我们的模型组件打算使用实体EJBs来访问CD表和歌曲(tracks)表的数据,这些bean和Servlets被用于向用户提供服务。在这个测试环境里,我们使用了Resin应用程序服务器。EJBs通过JNDI(Java名字和目录接口)资源引用来访问数据库。Listing 12.4显示了在应用程序服务器的配置文件中增加的 <resource-ref>元素。除了数据库驱动和URL外,这个元素使用的是标准规范。如果你使用的是另外数据库,你需要改变<init-param>元素以满足需要。
<resource-ref>
<res-ref-name>jdbc/ProductsDB</res-ref-name>
<res-type>javax.sql.ConnectionPoolDataSource</res-type>
<init-param driver-name="org.gjt.mm.mysql.Driver"/>
<init-param url="jdbc:mysql://localhost:3306/products"/>
<init-param user=""/>
<init-param password=""/>
<init-param max-connections="20"/>
<init-param max-idle-time="30"/>
<init-param max-active-time="1"/>
<init-param max-pool-time="1"/>
<init-param connection-wait-time="1"/>
</resource-ref>
Listing 12.4 The resin.config resource text.
模型(Model)代码
在这里,你已经创建了一个数据库,同时包含了JNDI引用,现在,通过JNDI引用,你就可以访问数据库了。是时候考虑实体beans了,我们将把它用于访问数据库表里的数据。因为在我们的有两个数据库表,因此就需要构建两个EJB。为了达到目的,让我们利用Resin的container-managed persistence (CMP)模式来创建bean,同时主要把精力集中在实现EJB本地实例上。意思是我们只处理CMP的本地接口,而不处理远程接口,因此只需要很少的代码。由于本书是关于Velocity的,故我们只展示这两个bean的代码,不作更多解释,你可以在站点http://www.wiley.com/compbooks/gradecki处下载这个完整的示例代码。下面就让我们来看一看EJB文件的定义(Listing 12.5)。
<ejb-jar>
<enterprise-beans>
<entity>
<ejb-name>CDRecordBean</ejb-name>
<local-home>cd.CDRecordHome</local-home>
<local>cd.CDRecord</local>
<ejb-class>cd.CDRecordBean</ejb-class>
<prim-key-class>int</prim-key-class>
<primkey-field>id</primkey-field>
<persistence-type>Container</persistence-type>
<reentrant>True</reentrant>
<abstract-schema-name>CDTable</abstract-schema-name>
<sql-table>cd</sql-table>
<cmp-field><field-name>id</field-name></cmp-field>
<cmp-field><field-name>title</field-name></cmp-field>
<cmp-field><field-name>artist</field-name></cmp-field>
<cmp-field><field-name>tracks</field-name></cmp-field>
<query>
<query-method>
<method-name>findByArtist</method-name>
</query-method>
<ejb-ql>SELECT o FROM CDTable o WHERE o.artist like
?1</ejb-ql>
</query>
</entity>
<entity>
<ejb-name>TracksRecordBean</ejb-name>
<local-home>cd.TracksRecordHome</local-home>
<local>cd.TracksRecord</local>
<ejb-class>cd.TracksRecordBean</ejb-class>
<prim-key-class>int</prim-key-class>
<primkey-field>id</primkey-field>
<persistence-type>Container</persistence-type>
<reentrant>True</reentrant>
<abstract-schema-name>TrackTable</abstract-schema-name>
<sql-table>tracks</sql-table>
<cmp-field><field-name>id</field-name></cmp-field>
<cmp-field><field-name>cd_id</field-name></cmp-field>
<cmp-field><field-name>name</field-name></cmp-field>
<cmp-field><field-name>length</field-name></cmp-field>
<query>
<query-method>
<method-name>findByCdID</method-name>
</query-method>
<ejb-ql>SELECT o FROM TrackTable o WHERE o.cd_id=?1</ejb-ql>
</query>
</entity>
</enterprise-beans>
</ejb-jar>
Listing 12.5 The CDRecordBean EJB file.
Listing 12.5里的EJB文件展示了这两个bean是如何定义的,其中包含了主键和每一个表的字段。这两个实体bean都包含了一个<query>元素,它允许数据从表中取出并用于除主键以外的字段。为了让你了解在Resin应用程序服务器里是如何书写实体bean的,让我们来看一下Listing 12.6里的CDRecordBean类。
package cd;
import javax.ejb.*;
public abstract class CDRecordBean
extends com.caucho.ejb.AbstractEntityBean {
public abstract String getTitle();
public abstract String getArtist();
public abstract int getTracks();
public abstract int getId();
public abstract void setTitle(String title);
public abstract void setArtist(String artist);
public abstract void setTracks(int tracks);
public abstract void setId(int id);
public int ejbCreate(String title, String artist, int tracks)
throws CreateException {
setId(0);
setTitle(title);
setArtist(artist);
setTracks(tracks);
return 1;
}
public void ejbPostCreate(String title, String artist, int
tracks) {
// since there are no relations, this is empty.
}
}
Listing 12.6 The CDRecordBean class.
Listing 12.6里的CDRecordBean类继承自AbstractEntityBean类,AbstractEntityBean类是Resin制造商定义的一个助手类。这个助手类提供了所有的用于让实体bean实现空body的通用方法。事实上,把该助手类用于你的bean时,你只需要提供一个方法,从而减少代码量和代码混杂。另外,bean里剩余的代码被用于定义与这个bean相关联的表字段。Listing 12.7显示了TracksRecordBean类。
package cd;
import javax.ejb.*;
import java.sql.*;
public abstract class TracksRecordBean
extends com.caucho.ejb.AbstractEntityBean {
public abstract int getId();
public abstract int getCd_id();
public abstract String getName();
public abstract String getLength();
public abstract void setId(int id);
public abstract void setCd_id(int cd_id);
public abstract void setName(String name);
public abstract void setLength(String tracks);
public int ejbCreate(int cd_id, String name, String length)
throws CreateException {
setCd_id(cd_id);
setName(name);
setLength(length);
return 1;
}
public void ejbPostCreate(int cd_id, String name, String length) {
// since there are no relations, this is empty.
}
}
Listing 12.7 The TracksRecordBean class.
我们在之前曾经提及,你可以为这些实体bean下载剩余的文件。所有的实体文件被放置在Resin 应用程序服务器主机的/classes目录下。
The View Code
现在,你需要考虑一下如何使用Velocity作为你的脚本语言来进行输出。这里将是你施展魔法的地方,为用户提供令人激动的输出。对这个应用程序来说,我们需要三个Velocity模板:
■ thanks.vm—显示thank-you信息的普通页面
■ displaycd.vm—向用户显示CD列表的页面
■ displaytracks.vm—显示特定CD内所有歌曲的页面
首先,让我们来看一下thanks.vm模板,见Listing 12.8
<HTML>
<HEAD>
<TITLE></TITLE>
<link rel="stylesheet" type="text/css" href="defaultpage.css">
</HEAD>
<BODY BGCOLOR="#F79C19" link="ffffff" alink="999999"
vlink="ffffff" topmargin="0" leftmargin="0" marginheight="0"
marginwidth="0">
<BR>
#if ($message)
$message
#end
$thanks
<img src="/images/tune_big.gif" height="250" width=283></td></tr>
</table>
</BODY>
</HTML>
Listing 12.8 The thanks.vm template.
这个thanks.vm模板事实上是web框架集的一部分,它提供了所有可见的边框。这个模板将被放置在main处,或放在body里,是web框架集的一部分。在这里,为了让页面更美观,你需要提供适当的背景颜色和图片。在这个模板的顶部是页面背景的信息,后跟实际的Velocity元素。
第一个元素为带有$message引用的#if指令。$message引用用于显示一个信息(当一个错误出现并且你需要让用户知道这个错误的时候)。这个信息只有在错误发生时才会被写入上下文中,因此,程序没有发生错误的时候,这个引用在上下文里是不存在的。如果我们不用#if指令对$message进行测试,那么在输出中就有可能出现文本字符串“$message”,这看起来不太好,因此我们使用$if指令对$message引用是否包含有值进行测试,如果$message引用包含有值(意思是在上下文里可以找到这个引用),那么就显示$message引用的值。
在发生错误的情况下,你仍旧想感谢用户辛苦输入新的CD或歌曲。这个输出就$thanks引用来产生。
显示CD
我们的应用程序允许用户通过特定的艺术家搜索数据库里所有的CD,并得到一个这些CD的列表。在每一次显示CD列表的时候都将显示一个按钮,以方便用户可以单击来列出特定CD的歌曲列表。Listing 12.9里的displaycd.vm模板用于处理这些显示任务。
<HTML>
<HEAD>
<TITLE></TITLE>
<link rel="stylesheet" type="text/css" href="defaultpage.css">
</HEAD>
<BODY BGCOLOR="#F79C19" link="ffffff" alink="999999"
vlink="ffffff" topmargin="0" leftmargin="0" marginheight="0"
marginwidth="0">
<BR><BR>
<font color="ffffff">
#foreach($value in $cds)
<form action="http://localhost:8080/cd/cdVelocityHandler"
method="post">
<b> Title: $value.title </b>
<input type="hidden" name="id" value="$value.id">
<input type="submit" name="submit" value="tracks">
</form>
#end
<BR>
<img src="/images/tune_big.gif" height="250" width="150">
</BODY>
</HTML>
Listing 12.9 The displaycd.vm template.
和thanks.vm模板一样,在这个模板的开头部分包含了一些HTML标记。所有CD的输出将被显示在框架集的body内。伴随HTML而来的是Velocity代码。现在让我们回忆一下需要列表显示的两个部分:显示某艺术家的所有CD标题和显示一个用于列出每个CD的所有歌曲的按钮。
假如你正和你的web开发者一起工作,你决定让代码执行一个操作,把从数据库里提出来的CD放到一个名叫$cds的Collection对象里,并将这个$cds对象放入上下文中。这个Collection对象将包含许多基于某特定艺术家所有CD的CDRecordBean对象。
正如你已经看到的,#foreach指令用于从可用的对象里提取一个迭代器(iterator),Collection类就是这样的一个对象。因此,每一次循环,$value引用将得到一个CDRecordBean实体对象。利用bean的getter方法,适当的值将被提取出来。
许多工作都发生在循环内部,在这里将从数据库提取用于显示的CD标题。注意,一定要用HTML的<form>标记包围这个标题,这个<form>标记用于显示一个让用户单击就可以显示所有这张CD歌曲的按钮。
在讨论完应用程序的需求后,你和web开发者决定从CD表里提取ID,用于链接歌曲表。为了实现这个想法,你在<form>元素里创建了一个隐藏的<input>元素。注意,这个隐藏输入按钮的值用的是$value.id,意思是你可以向控制器传递每一个CD的ID。
显示歌曲(tracks)
当用户单击displaycd.vm模板输出的Tracks按钮时,将显示歌曲的名称和歌曲长度。Listing 12.10显示的displaytracks.vm模板将用于完成这些任务。这个模板再次包含了HTML标记,以便更美观地显示这个模板。在HTML标记之后,你将看到另一个#foreach循环。
<HTML>
<HEAD>
<TITLE></TITLE>
<link rel="stylesheet" type="text/css" href="defaultpage.css">
</HEAD>
<BODY BGCOLOR="#F79C19" link="ffffff" alink="999999"
vlink="ffffff" topmargin="0" leftmargin="0" marginheight="0"
marginwidth="0">
#if ($message)
$message
#end
<BR>
#foreach($value in $tracks)
<b> Track: $value.name - Length: $value.length</b><BR>
#end
<br>
<img src="/images/tune_big.gif" height="250" width=283></td></tr>
</table>
</BODY>
</HTML>
Listing 12.10 The displaytracks.vm template.
在这里,你的web开发者已经指出了你所需要的另外一个Collection对象,用于存储某张CD上所有的歌曲。这个Collection对象被命名为$tracks,它通过控制器组件放置在上下文里。
控制器代码
你目前已经拥有了模型和视图组件,现在你需要一个控制器组件来把这两个组件联系在一起。Listing 12.11显示了这个Velocity Servlets,它将完成这个工作。
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.velocity.Template;
import org.apache.velocity.context.Context;
import org.apache.velocity.servlet.VelocityServlet;
import org.apache.velocity.exception.*;
import javax.naming.*;
import javax.ejb.*;
import cd.*;
import org.apache.velocity.app.Velocity;
public class cdVelocityHandler extends VelocityServlet {
private CDRecordHome cdHome = null;
private TracksRecordHome tracksHome = null;
protected Properties loadConfiguration(ServletConfig config )
throws IOException, FileNotFoundException {
Properties p = new Properties();
String path =
config.getServletContext().getRealPath("/");
if (path == null) {
System.out.println("
Unable to get the current webapp root");
path = "/";
}
p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH,
path );
return p;
}
public void init() throws ServletException {
try {
javax.naming.Context cmp = (javax.naming.Context)
new InitialContext().lookup("java:comp/env/cmp");
cdHome = (CDRecordHome) cmp.lookup("CDRecordBean");
tracksHome = (TracksRecordHome)
cmp.lookup("TracksRecordBean");
} catch (NamingException e) {
e.printStackTrace();
}
}
public Template handleRequest( HttpServletRequest req,
HttpServletResponse res, Context context ) {
Template template = null;
if (req.getParameter("submit").equals("new")) {
try {
if (cdHome == null) {
context.put("message", "Sorry we had an error");
} else {
int tracks =
Integer.parseInt(req.getParameter("tracks"));
CDRecord cd =
cdHome.create(req.getParameter("title"),
req.getParameter("artist"), tracks);
if (cd != null) {
context.put("thanks",
"Thank you for the new CD<BR>");
} else {
context.put("thanks",
"We are sorry but your request failed<BR>");
}
try {
template = getTemplate("thanks.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
} else if (req.getParameter("submit").equals("obtain")) {
try {
if (cdHome == null) {
context.put("message", "Sorry we had an error");
} else {
Collection cds =
cdHome.findByArtist(req.getParameter("artist"));
context.put ("cds", cds);
try {
template = getTemplate("displaycds.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
} else if (req.getParameter("submit").equals("tracks")) {
try {
if (tracksHome == null) {
context.put("message", "Sorry we had an error");
} else {
int id = Integer.parseInt(req.getParameter("id"));
Collection tracks = tracksHome.findByCdID(id);
context.put ("tracks", tracks);
try {
template = getTemplate("displaytracks.vm");
} catch( Exception e ) {
System.out.println("Error " + e);
}
}
} catch(Exception e) {
e.printStackTrace();
}
} else if (req.getParameter("submit").equals("addtrack")) {
try {
if (tracksHome == null) {
context.put("message", "Sorry we had an error");
} else {
int id= Integer.parseInt(req.getParameter("id"));
TracksRecord track = tracksHome.create(id,
req.getParameter("name"),
req.getParameter("length"));
if (track!= null) {
context.put("thanks",
"Thank you for the new track<BR>");
} else {
context.put("thanks",
"We are sorry but your request failed<BR>");
}
try {
template = getTemplate("thanks.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
} else {
}
return template;
}
}
Listing 12.11 The controller servlet for the CD example
这个控制器组件是一个Velocity Servlets,它使用VelocityServlet类的handleRequest()方法。在这里有一些让Servlets访问Velocity模板和实体EJB的步骤。
Velocity模板被放置在Servlets应用程序的根目录,这个目录是Servlets必须的。为了确定Servlets能够访问这个应用程序的根目录,需要为FILE_RESOURCE_LOADER_PATH属性设置真实的目录。你可以在loadConfiguration()方法里完成这个设置操作,这个方法将在handleRequest()方法调用之前被自动调用。
接着,你需要获得实体EJB home接口的访问权,你可以在init()方法里完成这个任务,init()方法将Servlets第一次执行时被调用,这个方法将为comp/env/cmp JNDI引用获得一个naming.Context上下文对象。这个引用将用于在系统里访问EJB。接下来,你需要查找每一个bean,并从这两个bean中返回适当的home对象。这些home对象将用于构建实体bean。
在handleRequest()方法里的代码用于检查用户请求的是这四个操作的哪一个操作(增加CD、搜索特定艺术家、查看某CD的全部歌曲、为CD增加歌曲)。让我分别看一下这四个操作:
增加新CD
当用户为一张新的CD输入标题、艺术家和该CD的所有歌曲时,代码必须能够完全把这些数据放到数据库表里。代码首先确定home接口对象是有效的,如果该接口无效,一个错误信息将会指派给“message”引用,该引用将增加到上下文中。
如果home接口对象是有效的,则从HTML的<form>传递过来的歌曲字符串被转换成integer。接着,歌曲和从<form>传递过来的值的剩余部分一同被传递给CD bean的home接口的create()方法。调用该方法的结果有可能是null,也可能是一个用于呈现CD表行的新的实体EJB。如果该方法的值不是null,那么“thanks”引用被指向一个文本字符串,同时被加入到上下文对象中。否则,一个失败的信息将会加入到上下文中。
在任意一种情况下,模板对象都会被设置成从getTemplate()方法(使用thanks.vm文件名参数)返回的值。如果没有异常发生,这个新模板对象会从handleRequest()方法返回,同时用户会看到适当的输出。
增加新CD歌曲(Track)
增加新CD歌曲的代码基本上和增加新CD的代码一样,只是接口不同,在此用的是track接口。在一个生产型(production)系统里,你必须确定歌曲应该加入到的CD表,以保证CD和歌曲能够正确关联。
通过艺术家搜索CD
通过特定的艺术家来获取CD列表和增加CD只有一小点的不同就是获取CD列表需要查询数据库。你应该记得这两个实体EJB的EJB文件里都定义了一个<query>元素。在CD数据表的情况下,我们定义的查询将返回特定艺术家所有的CD表行。
代码通过findByArtist(String)方法来调用这个查询。从这个方法返回的值是一个Collection对象,它包含了从CD数据表返回的零到若干个实体对象。不管有多少个对象在collection里,我们将使用下面的命令把这个Collection加入到上下文中
context.put ("cds", cds);
当Collection对象被加入到上下文中后,displaycds.vm模板就从服务器的磁盘取出,之后,这个模板对象被显示给用户。
列出CD歌曲
当用户想显示特定CD的歌曲时,代码将获取从<form>里的ID变量传递过来的ID,以及将这个ID传递给findByCdID(int)方法。这个方法执行TracksRecordBean的EJB文件里的<query>元素,该方法的结果是一个Collection对象,它包含了这张特定CD的所有歌曲。这个Collection对象通过$tracks引用被加入到上下文中。
高级Servlet功能
作为VelocityServlet基础类,许多附加的方法可以被重载。这些方法如下:
■ Properties loadConfiguration(ServletConfig)—一个允许附加的属性被加入到Servlets的属性中的方法。这些目前被定义到Velocity运行时的属性是:
l static java.lang.String--COUNTER_INITIAL_VALUE—初始化#foreach指令的计数器值
l static java.lang.String--COUNTER_NAME—初始化#foreach指令的计数器名称
l static java.lang.String--DEBUG_PREFIX—日志信息前缀
l static java.lang.String--DEFAULT_RUNTIME_DIRECTIVES—默认运行时指令
l static java.lang.String--DEFAULT_RUNTIME_PROPERTIES—默认运行时属性
l static java.lang.String--ENCODING_DEFAULT—默认编码类型
l static java.lang.String--ERROR_PREFIX—错误信息前缀
l static java.lang.String--ERRORMSG_END—错误信息的结束标记,通过在#include指令里传递一个不允许参数时触发
l static java.lang.String--ERRORMSG_START—错误信息的开始标记,通过在#include指令里传递一个不允许参数时触发
l static java.lang.String--FILE_RESOURCE_LOADER_CACHE—在FileResourceLoader里打开一个公用的缓存
l static java.lang.String--FILE_RESOURCE_LOADER_PATH—在FileResourceLoader里设置一个公用的路径
l static java.lang.String--INFO_PREFIX—消息通知前缀
l static java.lang.String--INPUT_ENCODING—模板编码集
l static java.lang.String--INTERPOLATE_STRINGLITERALS—字符串篡改开关
l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_BUFFER_SIZE--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_FROM--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_SERVER--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_SUBJECT--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_EMAIL_TO--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_FILE_BACKUPS--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_FILE_SIZE--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_PATTERN--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_REMOTE_HOST--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_REMOTE_PORT--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_SYSLOGD_FACILITY--log4J配置
l static java.lang.String--LOGSYSTEM_LOG4J_SYSLOGD_HOST--log4J配置
l static int--NUMBER_OF_PARSERS—你想创建的解析器数量
l static java.lang.String--OUTPUT_ENCODING—输出流编码集
l static java.lang.String--PARSE_DIRECTIVE_MAXDEPTH—#parse指令允许的最大递归深度
l static java.lang.String--PARSER_POOL_SIZE—解析器在池里的总数
l static java.lang.String--RESOURCE_LOADER—用于重新找回资源加载器名称的关键字
l static java.lang.String--RESOURCE_MANAGER_CACHE_CLASS—实现资源管理缓存的类
l static java.lang.String--RESOURCE_MANAGER_CLASS—实现资源管理器的类
l static java.lang.String--RESOURCE_MANAGER_LOGWHENFOUND—用于确定是否对找到资源进行日志
l static java.lang.String--RUNTIME_LOG—定位Velocity日志文件
l static java.lang.String--RUNTIME_LOG_ERROR_STACKTRACE—堆栈跟踪错误信息输出
l static java.lang.String--RUNTIME_LOG_INFO_STACKTRACE—堆栈跟踪报告信息输出
l static java.lang.String--RUNTIME_LOG_LOGSYSTEM—用于指定外面的日志系统
l static java.lang.String--RUNTIME_LOG_LOGSYSTEM_CLASS—你想要使用的日志系统类
l static java.lang.String--RUNTIME_LOG_REFERENCE_LOG_INVALID—非法引用日志
l static java.lang.String--RUNTIME_LOG_WARN_STACKTRACE—堆栈跟踪警告信息输出
l static java.lang.String--UNKNOWN_PREFIX—未知信息前缀
l static java.lang.String--VM_CONTEXT_LOCALSCOPE—VM本地作用域开关,默认为false
l static java.lang.String--VM_LIBRARY—本地Velocimacro库模板名称
l static java.lang.String--VM_LIBRARY_AUTORELOAD—一个自动加载VM资源库的开关,用于开发阶段
l static java.lang.String--VM_MESSAGES_ON—VM消息开关,默认为true
l static java.lang.String--VM_PERM_ALLOW_INLINE—布尔值,默认为true,既是否允许内联(在模板里)宏定义
l static java.lang.String--VM_PERM_ALLOW_INLINE_REPLACE_GLOBAL—布尔值,默认为false,即是否允许内联(在模板里)宏定义替换现有的定义
l static java.lang.String--VM_PERM_INLINE_LOCAL—是否强迫内联宏为本地的,默认为false
l static java.lang.String--WARN_PREFIX—警告信息前缀
■ Context createContext(HttpServletRequest, HttpServletResponse)—该方法允许开发者创建他们自己的上下文对象,可以被用作私有merge()方法。
■ void setContentType( HttpServletRequest,HttpServletResponse)—默认情况下,handleRequest()方法将输出HTML格式的文本,但是,你可以更改成其他格式,比如XML,甚至是图片文件。
■ void mergeTemplate(Template, Context, HttpServletResponse)—如果你想自己控制输出来代替handleRequest()方法,那么你可以使用createContext()方法得到你自己的上下文,并且把它们和模板进行合并,之后通过handleRequest()方法输出给response对象。mergeTemplate()方法得到所有的三个对象,并且产生输出。
■ void requestCleanup(HttpServletRequest, HttpServletResponse, Context)—如果你想自己处理输出,你应该重载requestCleanup()方法,来处理任何最后的结果。默认情况下,这个方法并没有实现。
■ protected void error(HttpServletRequest, HttpServletResponse, Exception)—当在处理用户请求发生一个异常时,该方法将被调用。你可以重载这个方法来提供更多高级的错误处理能力。默认实现只是发送一个错误信息和一个堆栈跟踪信息给用户。
增加报表(report)
我们的应用程序在此已经把注意力集中到产生一个HTML窗体格式的输出。但是,如果你不想要任何HTML样式的表,而是只想得到一个基于文本格式的所有数据库里CD的报表该怎么办?Ok,现在让我们考虑一下Listing 12.12里的Velocity模板。
Listing 12.12 A Velocity template that produces a text report.
Listing 12.12里的Velocity模板用于输出一个页面的头部,最多列出50张数据库里的CD,之后,产生另一个页面头部。在两个头部之间的CD数量可以改变,以适应于不同的输出。为了产生的完整的CD报表,你需要在主CD index.html页面增加一个按钮。下面给出这个按钮的代码:
<h3>Reports</h3>
<form action="http://localhost:8080/cd/cdVelocityHandler" method="post">
<input type="submit" name="submit" value="fullreport"> -
download 'report.txt' to your local system
</form>
这个新窗体在index页面上显示了一个名叫FullReport的按钮。当用户单击这个按钮的时候,控件被传递到cdVelocityHandler servlet(在Listing 12.11里定义的)。Listing 12.13的代码将用于处理这个新按钮。
else if (req.getParameter("submit").equals("fullreport")) {
try {
if (cdHome == null) {
context.put("message", "Sorry we had an error");
} else {
Collection cds = cdHome.findAllCDs();
context.put ("cds", cds);
try {
template = getTemplate("fullreport.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
Listing 12.13 The control servlet report task.
除了配合CDRecordBean实体bean的findAllCDs查询外,这些代码没有什么特别的地方。新报表的查询见Listing 12.14。
<query>
<query-method>
<method-name>findAllCDs</method-name>
</query-method>
<ejb-ql>SELECT o FROM CDTable o</ejb-ql>
</query>
Listing 12.14 The bean query.
新查询从CDTable数据库表从提出所有的行。一旦所有的行被提出,结果Collection对象就会被放入上下文中。最后,Velocity模板fullreport.vm被调用,用于输出这个查询的结果。输出的结果见Figure 12.5
Figure 12.5 The report output in a browser.
正如你所看见的一样,这个输出并不像我们希望的一样,只输出文本样式,而不需要HTML进行润色。如果我们想生成一个文本格式的文件并可以进行下载该怎么做?让我们来考虑一下Listing 12.15里的模板。
Listing 12.15 The Velocity template for text only.
Listing 12.15里Velocity模板使用tab占位符来控制从数据库生成数据的输出格式。这也就意味着,Artis和Title对应的字符串将拥有一个适当的格式和对齐方式,同时不再顾及这些字符串的长度。你肯定不想看到任何参差不齐的行排列样式。为了完成这个任务,在知道Artist的值后,你必须生成一个正确的tab个数。如果你看一下模板代码,你将会看到下面这行:
$value.artist$stringlength.tabs($value.artist)$value.title
这一个行是由下面三行代码构成的:
$value.artist
$stringlength.tabs($value.artist)
$value.title
$value.artist和$value.title命令只简单为当前行产生将要显示的artist和title数据。这个命令里最有趣的部分是$stringlength.tabs($value.artist)。这个$stringlength引用和一个放置在上下文的对象(使用StringLength类)关联。StringLength类代码见Listing 12.16
public class StringLength {
public StringLength(){}
public String tabs(String st) {
String s = new String();
for (int i=3;i>st.length()/5;i--)
s = s + ""t";
return s;
}
}
Listing 12.16 The StringLength class.
StringLength类只有一个简单的任务:暴露一个名叫tabs()的方法。这个方法的作用是计算并返回一个确定个数的tabs字符串,以用于排列artist和title的值。艺术家的名称被传递给tabs()方法,之后确定数量的tabs被返回。
现在,你的Velocity模板已经能够返回正确的输出,OK,让我们考虑一下如何让你的控件Servlets生成一个文件,以便用户下载。Listing 12.17的代码显示了Full-Report按钮的控件Servlets代码。
else if (req.getParameter("submit").equals("fullreport")) {
try {
if (cdHome == null) {
context.put("message", "Sorry we had an error");
} else {
Collection cds = cdHome.findAllCDs();
context.put ("cds", cds);
try {
res.setContentType("APPLICATION/OCTET-STREAM");
res.setHeader("Content-Disposition","attachment;
filename=report.txt");
template = getTemplate("fullreport.vm");
} catch( Exception e ) {
e.printStackTrace();
}
}
} catch(Exception e) {
e.printStackTrace();
}
}
Listing 12.17 The servlet code for downloading the report.
正如你所看到的一样,findAllCDs查询被用于从数据库中提取所有的CD信息,同时,模板fullreport.vm被用于输出。其中增加了两个新命令:
res.setContentType("APPLICATION/OCTET-STREAM");
res.setHeader("Content-Disposition","attachment; filename=report.txt");
这两个命令用于告诉用户浏览器,从<form>返回的信息是一个名叫report.txt的文件,这个信息将导致出现一个下载对话框,用于用户下载文件。Figure 12.6显示了使用这个应用程序产生的文件下载情况。
Figure 12.6 The report output.
本章小节和下章介绍
在这一章里,我们介绍了一种可能对开发者有用的混合Velocity和Servlets的技术。在下一章里,我们将演示如何扩展Velocity驱动的站点,以适当国际化需要的技术。