Scala 顯然是一門有趣的語言,很適合體現語言理論和創新方面的新思想,但最終它要用在 “現實” 環境中,它必須能滿足開發人員的某些需求並在 “現實” 環境中有一定的實用性。
了解 Scala 語言的一些核心功能之後,就能認識到 Scala 語言的一些靈活性,並能放心使用 Scala 創建 DSL。現在我們進入實際應用程序使用的環境,看看 Scala 如何適應環境。在本系列的新階段中,我們將首先討論大部分 Java™ 應用程序的核心:Servlet API。
servlet 回顧
回憶一下 Servlet 101 課程和教程,servlet 環境的核心實際上就是通過一個套接字(通常是端口 80)使用 HTTP 協議的客戶機-服務器交換。客戶機可以是任何 “用戶-代理”(由 HTTP 規范定義),服務器是一個 servlet 容器。servlet 容器在我編寫的一個類上查找、加載和執行方法,該類最終必須實現 javax.servlet.Servlet 接口。
通常,實際的 Java 開發人員不會編寫直接實現接口的類。因為最初的 servlet 規范是用於為 HTTP 之外的其他協議提供一個通用 API,所以 servlet 命名空間被分為了兩部分:
一個 “通用” 包(javax.servlet)
一個特定於 HTTP 的包(javax.servlet.http)
這樣,將在一個稱為 javax.servlet.GenericServlet 的抽象基類的通用包中實現一些基本的功能;然後在派生類 javax.servlet.http.HttpServlet 中實現其他特定於 HTTP 的功能,該類通常用作 servlet 實際 “內容” 的基類。HttpServlet 提供了一個 Servlet 的完整實現,將 GET 請求委托給一個將要被覆蓋的 doGet 方法,將 POST 請求委托給一個將要被覆蓋的 doPut 方法,依此類推。
Hello, Scala 與 Hello, Servlet
顯然,任何人編寫的第一個 servlet 都是普遍的 “Hello, World” servlet;Scala 的第一個 servlet 示例也是如此。回憶一下許多年之前介紹的 servlet 教程,當時基本的 Java “Hello, World” servlet 只是輸出清單 1 所示的 HTML 響應:
清單 1. 預期的 HTML 響應
<HTML>
<HEAD><TITLE>Hello, Scala!</TITLE></HEAD>
<BODY>Hello, Scala! This is a servlet.</BODY>
</HTML>
用 Scala 編寫一個簡單的 servlet 來實現這個操作非常簡單,而且這個 servlet 與其相應的 Java 形式幾乎一樣,如清單 2 所示:
清單 2. Hello, Scala servlet!
import javax.servlet.http.{HttpServlet,
HttpServletRequest => HSReq, HttpServletResponse => HSResp}
class HelloScalaServlet extends HttpServlet
{
override def doGet(req : HSReq, resp : HSResp) =
resp.getWriter().print("<HTML>" +
"<HEAD><TITLE>Hello, Scala!</TITLE></HEAD>" +
"<BODY>Hello, Scala! This is a servlet.</BODY>" +
"</HTML>")
}
注意,我使用了一些適當的導入別名來縮短請求的類型名稱和相應類型;除此之外,這個 servlet 幾乎與其 Java servlet 形式一樣。編譯時請記得在 servlet-api.jar(通常隨 servlet 容器一起發布;在 Tomcat 6.0 發行版中,它隱藏在 lib 子目錄中)中包含一個引用,否則將找不到 servlet API 類型。
這還准備得不夠充分;根據 servlet 規范,它必須使用一個 web.xml 部署描述符部署到 Web 應用程序目錄中(或一個 .war 文件中),該描述符描述 servlet 應該與哪個 URL 結合。對於這樣一個簡單的例子,使用一個相當簡單的 URL 來配合它最容易,如清單 3 所示:
清單 3. 部署描述符 web.xml
<!DOCTYPE web-app
PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>helloWorld</servlet-name>
<servlet-class>HelloScalaServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloWorld</servlet-name>
<url-pattern>/sayHello</url-pattern>
</servlet-mapping>
</web-app>
從這裡開始,我假設讀者會在必要時調整/修改部署描述符,因為這跟 Scala 沒有關系。
當然,格式良好的 HTML 與格式良好的 XML 非常相似;鑒於這一點,Scala 對 XML 字面值的支持使編寫這個 servlet 簡單得多(參閱 參考資料 中的 “Scala 和 XML” 一文)。Scala 不是在傳遞給 HttpServletResponse 的 String 中直接嵌入消息,它可以分離邏輯和表示形式(非常簡單),方法是利用此支持將消息放在 XML 實例中,然後再傳遞回去:
清單 4. Hello, Scala servlet!
import javax.servlet.http.{HttpServlet,
HttpServletRequest => HSReq, HttpServletResponse => HSResp}
class HelloScalaServlet extends HttpServlet
{
def message =
<HTML>
<HEAD><TITLE>Hello, Scala!</TITLE></HEAD>
<BODY>Hello, Scala! This is a servlet.</BODY>
</HTML>
override def doGet(req : HSReq, resp : HSResp) =
resp.getWriter().print(message)
}
Scala 的內聯表達式求值工具使用 XML 字面值,這意味著能夠輕松地使 servlet 更有趣。例如,將當前日期添加到消息中與將 Calendar 表達式添加到 XML 中一樣簡單,不過增加了幾行 { Text(java.util.Calendar.getInstance().getTime().toString() ) }。這似乎顯得有點冗長,如清單 5 所示:
清單 5. Hello, timed Scala servlet!
import javax.servlet.http.{HttpServlet,
HttpServletRequest => HSReq, HttpServletResponse => HSResp}
class HelloScalaServlet extends HttpServlet
{
def message =
<HTML>
<HEAD><TITLE>Hello, Scala!</TITLE></HEAD>
<BODY>Hello, Scala! It's now { currentDate }</BODY>
</HTML>
def currentDate = java.util.Calendar.getInstance().getTime()
override def doGet(req : HSReq, resp : HSResp) =
resp.getWriter().print(message)
}
實際上,Scala 編譯器與 XML 對象消息一起整合到一個 scala.xml.Node 中,然後在將它傳遞給響應的 Writer 的 print 方法時將其轉換為一個 String。
不要小看這一點 — 表達形式從邏輯中分離出來完全在一個類內部進行。這條 XML 消息將進行編譯時檢查,以確保語法正確和格式良好,並獲得一些標准 servlet(或 JSP)不具備的好處。由於 Scala 可以進行類型推斷,因此可以省略有關 message 和 currentDate 的實際類型消息,使得這就像動態語言 Groovy/Grails 一樣。初次使用效果不錯。
當然,只讀 servlet 相當麻煩。
Hello, Scala。這些是參數。
大多數 servlet 不會只返回類似靜態內容或者當前日期和時間的簡單消息,它們帶有 POST 形式的內容,檢查內容並進行相應的響應。例如,可能 Web 應用程序需要知道使用者的身份,並詢問其姓名:
清單 6. 挑戰!
<HTML>
<HEAD><TITLE>Who are you?</TITLE></HEAD>
<BODY>
Who are you? Please answer:
<FORM action="/scalaExamples/sayMyName" method="POST">
Your first name: <INPUT type="text" name="firstName" />
Your last name: <INPUT type="text" name="lastName" />
<INPUT type="submit" />
</FORM>
</BODY>
</HTML>
這個方法不會在任何用戶界面設計大賽中奪冠,但是它達到了目的:這是一個 HTML 表單,它會將數據發送給一個新的 Scala servlet(綁定到 sayMyName 的相對 URL)。這些數據將根據 servlet 規范存儲在一個名稱-值對集合中,可通過 HttpServletRequest.getParameter() API 調用獲得。在此調用中,我們將 FORM 元素的名稱作為一個參數傳遞給 API 調用。
從 Java 代碼直接轉換相對容易一些,如清單 7 中的 servlet 所示:
清單 7. 響應(v1)
class NamedHelloWorldServlet1 extends HttpServlet
{
def message(firstName : String, lastName : String) =
<HTML>
<HEAD><TITLE>Hello, {firstName} {lastName}!</TITLE></HEAD>
<BODY>Hello, {firstName} {lastName}! It is now {currentTime}.</BODY>
</HTML>
def currentTime =
java.util.Calendar.getInstance().getTime()
override def doPost(req : HSReq, resp : HSResp) =
{
val firstName = req.getParameter("firstName")
val lastName = req.getParameter("lastName")
resp.getWriter().print(message(firstName, lastName))
}
}
但這缺少了我之前提到的消息分離的一些好處,因為現在消息定義必須顯式使用參數 firstName 和 lastName;如果響應 get 中使用的元素個數超過 3 個或 4 個,情況就會變得比較復雜。此外,doPost 方法在將參數傳遞給消息進行顯示之前,必須自行提取每一個參數 — 這樣的編碼很繁瑣,並且容易出錯。
一種解決方法是將參數提取和 doPost 方法本身的調用添加到一個基類,如清單 8 中的版本 2 所示:
清單 8. 響應(v2)
abstract class BaseServlet extends HttpServlet
{
import scala.collection.mutable.{Map => MMap}
def message : scala.xml.Node;
protected var param : Map[String, String] = Map.empty
protected var header : Map[String, String] = Map.empty
override def doPost(req : HSReq, resp : HSResp) =
{
// Extract parameters
//
val m = MMap[String, String]()
val e = req.getParameterNames()
while (e.hasMoreElements())
{
val name = e.nextElement().asInstanceOf[String]
m += (name -> req.getParameter(name))
}
param = Map.empty ++ m
// Repeat for headers (not shown)
//
resp.getWriter().print(message)
}
}
class NamedHelloWorldServlet extends BaseServlet
{
override def message =
<HTML>
<HEAD><TITLE>Hello, {param("firstName")} {param("lastName")}!</TITLE></HEAD>
<BODY>Hello, {param("firstName")} {param("lastName")}! It is now {currentTime}.
</BODY>
</HTML>
def currentTime = java.util.Calendar.getInstance().getTime()
}
這個版本使 servlet 顯示變得比較簡單(相對上一版本而言),而且增加了一個優點,即 param 和 header 映射保持不變(注意,我們可以將 param 定義為一個引用請求對象的方法,但這個請求對象必須已經定義為一個字段,這將引發大規模的並發性問題,因為 servlet 容器認為每一個 do 方法都是可重入的)。
當然,錯誤處理是處理 Web 應用程序 FORM 的重要部分,而 Scala 作為一種函數性語言,保存的內容都是表達式,這意味著我們可以將消息編寫為結果頁面(假設我們喜歡這個輸入),或編寫為錯誤頁面(如果我們不喜歡這個輸入)。因此,檢查 firstName 和 lastName 的非空狀態的驗證函數可能如清單 9 所示:
清單 9. 響應(v3)
class NamedHelloWorldServlet extends BaseServlet
{
override def message =
if (validate(param))
<HTML>
<HEAD><TITLE>Hello, {param("firstName")} {param("lastName")}!
</TITLE></HEAD>
<BODY>Hello, {param("firstName")} {param("lastName")}!
It is now {currentTime}.</BODY>
</HTML>
else
<HTML>
<HEAD><TITLE>Error!</TITLE></HEAD>
<BODY>How can we be friends if you don't tell me your name?!?</BODY>
</HTML>
def validate(p : Map[String, String]) : Boolean =
{
p foreach {
case ("firstName", "") => return false
case ("lastName", "") => return false
//case ("lastName", v) => if (v.contains("e")) return false
case (_, _) => ()
}
true
}
def currentTime = java.util.Calendar.getInstance().getTime()
}
注意,模式匹配可以使編寫比較簡單的驗證規則變得很容易。利用模式匹配綁定到原始值(比如上一個例子),或者綁定到一個本地變量(比如我們要排除任何姓名中有 “e” 的人,比如上一個注釋)。
顯然,還有事情需要做!困擾 Web 應用程序的典型問題之一是 SQL 注入攻擊,它由通過 FORM 傳入的未轉義 SQL 命令字符引入,並且在數據庫中執行之前連接到包含 SQL 結構的原始字符串。使用 scala.regex 包中的正則表達式支持,或者一些解析器組合子(在本系列最後三篇文章中討論)可以確認 FORM 驗證是否正確。事實上,整個驗證過程會提交給使用默認驗證實現的基類,該驗證實現默認情況下只返回 true(因為 Scala 是函數性語言,所以不要忽略好的對象設計方法)。
結束語
雖然 Scala servlet 框架的功能不像其他一些 Java Web 框架的那樣完整,但是我這裡創建的這個小 Scala servlet 有兩個基本用途:
展示以有趣的方式利用 Scala 的功能,使 JVM 編程更簡單。
簡單介紹將 Scala 用於 Web 應用程序,這自然會引入 “lift” 框架(參見 參考資料 小節)。
本期到此結束,我們下一期再見!
本文配套源碼