CHATPER 12 在Servlets里使用 Velocity
通過前面章節的學習,我們了解到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)作為主機??梢宰鳛?/span>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代碼?,F在讓我們回憶一下需要列表顯示的兩個部分:顯示某藝術家的所有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驅動的站點,以適當國際化需要的技術。
posted on 2008-10-24 16:35 KINGWEE 閱讀(845) 評論(0) 編輯 收藏 所屬分類: Velocity