Scala Web开发全解析:从邮件通知到Web服务搭建
1. 邮件通知程序示例
运行某程序会产生如下输出:
Account: imap.yahoo.com, USERNAME, PASSWORD
Users: barney,betty,wilma
Account: imap.gmail.com, USER, PASS
Users: pebbles,bam-bam
在某个应用(如SARAH)中,使用类似代码来在收到感兴趣用户列表中的邮件时进行通知。SARAH会定期扫描收件箱,当发现来自该列表中的邮件时,会语音提示“你有来自Barney和Betty的新邮件”。
示例从存储在名为
jsonString
的字符串中的示例JSON开始。通过
parse
函数将该字符串转换为名为
json
的
JValue
对象。然后使用
\\
方法在
json
对象中搜索所有名为
emailAccount
的元素,这种语法与Scala的XML库中类似XPath的方法一致。
通过
for
循环遍历找到的元素,将每个元素提取为
EmailAccount
对象,并打印该对象中的数据。
EmailAccount
类有一个
usersOfInterest
字段,定义为
List[String]
,Lift - JSON库可以轻松转换该序列,无需额外编码。
2. 使用Scalatra创建Web服务
2.1 问题描述
希望使用Scalatra(一个轻量级的Scala Web框架,类似于Ruby的Sinatra库)构建新的Web服务。
2.2 解决方案
推荐使用Giter8工具为新的Scalatra项目创建SBT目录结构。假设已安装Giter8,使用以下命令创建一个基于Scalatra模板的新项目:
$ g8 scalatra/scalatra-sbt
organization [com.example]: com.alvinalexander
package [com.example.app]: com.alvinalexander.app
name [My Scalatra Web App]:
scalatra_version [2.2.0]:
servlet_name [MyScalatraServlet]:
scala_version [2.10.0]:
version [0.1.0-SNAPSHOT]:
Template applied in ./my-scalatra-web-app
Giter8完成后,进入新创建的目录:
$ cd my-scalatra-web-app
在该目录中启动SBT,并使用
container:start
命令启动Jetty服务器:
$ sbt
> container:start
// 大量输出...
[info] Started SelectChannelConnector@0.0.0.0:8080
[success] Total time: 11 s, completed May 13, 2013 4:32:08 PM
使用以下命令启用连续编译:
> ~ ;copy-resources;aux-compile
1. Waiting for source changes... (press enter to interrupt)
此命令会在源代码更改时自动重新编译。Jetty服务器默认在端口8080上启动。在浏览器中访问
http://localhost:8080/
,应看到默认的“Hello, world”输出,表明Scalatra正在运行。该URL显示的内容来自位于项目
src/main/scala/com/alvinalexander/app
目录下的
MyScalatraServlet
类:
package com.alvinalexander.app
import org.scalatra._
import scalate.ScalateSupport
class MyScalatraServlet extends MyScalatraWebAppStack {
get("/") {
<html>
<body>
<h1>Hello, world!</h1>
Say <a href="hello-scalate">hello to Scalate</a>.
</body>
</html>
}
}
get
方法表明它在监听
/
URI的GET请求。如果尝试访问
http://localhost:8080/foo
,浏览器会显示如下输出:
Requesting "GET /foo" on servlet "" but only have:
GET /
这是因为
MyScalatraServlet
只有一个方法,且只监听
/
URI的GET请求。
2.3 添加新服务
为演示添加新Web服务的过程,添加一个监听
/hello
URI的GET请求的新方法。只需在
servlet
中添加以下方法:
get("/hello") {
<p>Hello, world!</p>
}
保存对
MyScalatraServlet
的更改后,SBT控制台会显示一些输出,简略输出如下:
[info] Compiling 1 Scala source to target/scala-2.10/classes...
[success] Total time: 8 s
[info] Generating target/scala-2.10/resource_managed/main/rebel.xml.
[info] Compiling Templates in Template Directory:
src/main/webapp/WEB-INF/templates
[success] Total time: 1 s, completed May 28, 2013 1:56:36 PM
2. Waiting for source changes... (press enter to interrupt)
由于
~ aux-compile
命令,SBT会自动重新编译源代码。编译完成后,在浏览器中访问
http://localhost:8080/hello
,即可看到新的输出。
2.4 项目重要文件
除了
MyScalatraServlet
类,项目中还有一些重要文件:
| 文件路径 | 说明 |
| — | — |
|
project/build.scala
| 包含Scalatra的依赖信息 |
|
project/plugins.sbt
| 插件配置文件 |
|
src/main/resources/logback.xml
| 日志配置文件 |
|
src/main/scala/com/alvinalexander/app/MyScalatraServlet.scala
| 主Servlet类 |
|
src/main/scala/com/alvinalexander/app/MyScalatraWebAppStack.scala
| 相关栈类 |
|
src/main/scala/ScalatraBootstrap.scala
| 启动配置类 |
|
src/main/webapp/WEB-INF/web.xml
| Web配置文件 |
|
src/main/webapp/WEB-INF/templates/layouts/default.jade
| 模板布局文件 |
|
src/main/webapp/WEB-INF/templates/views/hello-scalate.jade
| 视图模板文件 |
|
src/test/scala/com/alvinalexander/app/MyScalatraServletSpec.scala
| 测试文件 |
其中,
WEB-INF/web.xml
文件内容如下:
<listener>
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener>
通常很少需要编辑该文件。
2.5 流程图
graph TD;
A[使用g8命令创建项目] --> B[进入项目目录];
B --> C[启动SBT并启动Jetty服务器];
C --> D[启用连续编译];
D --> E[访问默认URL测试];
E --> F[添加新服务方法];
F --> G[保存更改,SBT自动编译];
G --> H[访问新服务URL测试];
3. 用Scalatra替换XML Servlet映射
3.1 问题描述
想要向Scalatra应用程序中添加新的servlet,并定义它们的URI命名空间。
3.2 解决方案
Scalatra提供了一种避免在
web.xml
文件中声明servlet和servlet映射的方法。在
src/main/webapp/WEB-INF
目录下创建一个样板
web.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<listener>
<listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener>
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/img/*</url-pattern>
<url-pattern>/css/*</url-pattern>
<url-pattern>/js/*</url-pattern>
<url-pattern>/assets/*</url-pattern>
</servlet-mapping>
</web-app>
编辑
src/main/scala/ScalatraBootstrap.scala
文件,内容如下:
import org.scalatra._
import javax.servlet.ServletContext
import com.alvinalexander.app._
class ScalatraBootstrap extends LifeCycle {
override def init(context: ServletContext) {
// 默认创建
context.mount(new MyScalatraServlet, "/*")
// 新增
context.mount(new StockServlet, "/stocks/*")
context.mount(new BondServlet, "/bonds/*")
}
}
这两行新的
context.mount
代码告诉Scalatra,
StockServlet
类应处理所有以
/stocks/
开头的URI请求,
BondServlet
类应处理所有以
/bonds/
开头的URI请求。
在
src/main/scala/com/alvinalexander/app/OtherServlets.scala
文件中定义
StockServlet
和
BondServlet
类:
package com.alvinalexander.app
import org.scalatra._
import scalate.ScalateSupport
class StockServlet extends MyScalatraWebAppStack {
get("/") {
<p>Hello from StockServlet</p>
}
}
class BondServlet extends MyScalatraWebAppStack {
get("/") {
<p>Hello from BondServlet</p>
}
}
假设项目仍配置为自动重新编译,访问
http://localhost:8080/stocks/
和
http://localhost:8080/bonds/
URL时,应看到新servlet的内容。
3.3 讨论
Scalatra将此配置过程称为“挂载”servlet,类似于使用NFS等文件系统技术挂载远程文件系统的过程。配置后,
StockServlet
和
BondServlet
中的新方法将在
/stocks/
和
/bonds/
URIs下可用。例如,在
StockServlet
中定义一个新方法:
get("/foo") {
<p>Foo!</p>
}
可以通过
/stocks/foo
URI(如
http://localhost:8080/stocks/foo
)访问该方法。这种方法提供了与servlet映射相同的功能,但更简洁,且在Scala代码中工作,而不是在XML中,初始配置后通常可以忽略
web.xml
文件。
4. 访问Scalatra Web服务的GET参数
4.1 问题描述
创建Scalatra Web服务时,希望能够处理通过GET请求传递给方法的参数。
4.2 解决方案
4.2.1 传统参数传递方式
如果希望通过使用传统的
?
和
&
字符分隔数据元素的URI传递参数,如
http://localhost:8080/saveName?fname=Alvin&lname=Alexander
,可以在
get
方法中通过隐式的
params
变量访问它们:
/**
* The URL
* http://localhost:8080/saveName?fname=Alvin&lname=Alexander
* prints: Some(Alvin), Some(Alexander)
*/
get("/saveName") {
val firstName = params.get("fname")
val lastName = params.get("lname")
<p>{firstName}, {lastName}</p>
}
4.2.2 命名参数方式
Scalatra还支持“命名参数”方式,更方便且能记录方法期望接收的参数。调用者可以访问类似
http://localhost:8080/hello/Alvin/Alexander
的URL,在
get
方法中处理这些参数:
get("/hello/:fname/:lname") {
<p>Hello, {params("fname")}, {params("lname")}</p>
}
这种方式的优点是方法签名记录了期望的参数。
4.2.3 通配符使用
可以使用通配符处理其他需求,例如客户端传递文件名路径,而事先不知道路径深度的情况:
get("/getFilename/*.*") {
val data = multiParams("splat")
<p>{data.mkString("[", ", ", "]")}</p>
}
以
http://localhost:8080/getFilename/Users/Al/tmp/file.txt
为例,代码处理过程如下:
/**
* (1) GET http://localhost:8080/getFilename/Users/Al/tmp/file.txt
*/
get("/getFilename/*.*") {
// (2) creates a Vector(Users/Al/tmp/file, txt)
val data = multiParams("splat")
// (3) prints: [Users/Al/tmp/file, txt]
<pre>{data.mkString("[", ", ", "]")}</pre>
}
multiParams
方法使用
splat
参数创建一个包含两个元素的
Vector
,然后通过
data.mkString
将信息打印回浏览器。在实际程序中,可以将文件名重新组合并使用。Scalatra还有更多解析GET请求参数的方法,可查看最新文档获取更多信息。
5. 用Scalatra访问POST请求数据
5.1 问题描述
希望编写一个Scalatra Web服务方法来处理POST数据,例如处理作为POST请求发送的JSON数据。
5.2 解决方案
在Scalatra servlet中编写
post
方法,指定该方法应监听的URI:
post("/saveJsonStock") {
val jsonString = request.body
// deserialize the JSON ...
}
通过调用
request.body
方法访问传递给POST请求的数据。
5.3 示例代码
以下是
StockServlet
中的
post
方法示例,展示如何将接收到的JSON字符串反序列化为
Stock
对象:
package com.alvinalexander.app
import org.scalatra._
import scalate.ScalateSupport
import net.liftweb.json._
class StockServlet extends MyScalatraWebAppStack {
/**
* Expects an incoming JSON string like this:
* {"symbol":"GOOG","price":"600.00"}
*/
post("/saveJsonStock") {
// get the POST request data
val jsonString = request.body
// needed for Lift-JSON
implicit val formats = DefaultFormats
// convert the JSON string to a JValue object
val jValue = parse(jsonString)
// deserialize the string into a Stock object
val stock = jValue.extract[Stock]
// for debugging
println(stock)
// you can send information back to the client
// in the response header
response.addHeader("ACK", "GOT IT")
}
}
// a simple Stock class
class Stock (var symbol: String, var price: Double) {
override def toString = symbol + ", " + price
}
最后,需要将Lift - JSON依赖添加到项目中。假设项目是按照之前的方式创建的SBT项目,在
project/build.scala
文件的
libraryDependencies
中添加以下依赖:
"net.liftweb" %% "lift-json" % "2.5+"
5.4 测试POST方法
5.4.1 使用Scala代码测试
import net.liftweb.json._
import net.liftweb.json.Serialization.write
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.DefaultHttpClient
object PostTester extends App {
// create a Stock and convert it to a JSON string
val stock = new Stock("AAPL", 500.00)
implicit val formats = DefaultFormats
val stockAsJsonString = write(stock)
// add the JSON string as a StringEntity to a POST request
val post = new HttpPost("http://localhost:8080/stocks/saveJsonStock")
post.setHeader("Content-type", "application/json")
post.setEntity(new StringEntity(stockAsJsonString))
// send the POST request
val response = (new DefaultHttpClient).execute(post)
// print the response
println("--- HEADERS ---")
response.getAllHeaders.foreach(arg => println(arg))
}
class Stock (var symbol: String, var price: Double)
运行该测试对象时,会得到如下输出:
--- HEADERS ---
ACK: GOT IT
Content-Type: text/html;charset=UTF-8
Content-Length: 0
Server: Jetty(8.1.8.v20121106)
5.4.2 使用curl命令测试
curl \
--header "Content-type: application/json" \
--request POST \
--data '{"symbol":"GOOG", "price":600.00}' \
http://localhost:8080/stocks/saveJsonStock
在Unix系统上,将此命令保存到
postJson.sh
文件中,使其可执行并运行:
$ chmod +x postJson.sh
$ ./postJson.sh
虽然该命令没有输出,但应该能在
StockServlet
的输出窗口中看到正确的调试输出。
5.5 注意事项
Scalatra的最新版本使用Json4s库来反序列化JSON,该库目前基于Lift - JSON,反序列化代码类似。需要将相应库作为依赖添加到项目中。处理POST请求的关键要点包括:
- 使用
post
方法处理POST请求。
- 使用
request.body
获取POST数据。
- 可使用
response.addHeader("ACK", "GOT IT")
向客户端返回成功或失败消息(可选)。
- 拥有可用于测试POST请求的客户端程序。
6. 创建简单的GET请求客户端
6.1 问题描述
需要一个HTTP客户端来进行GET请求调用。
6.2 解决方案
6.2.1 简单使用
scala.io.Source.fromURL
方法
/**
* Returns the text (content) from a URL as a String.
* Warning: This method does not time out when the service is non-responsive.
*/
def get(url: String) = scala.io.Source.fromURL(url).mkString
此方法可用于调用RESTful URL并检索其内容,但在服务无响应时不会超时。由于
Source.fromURL
方法使用
java.net.URL
和
java.io.InputStream
类,可能会抛出
java.io.IOException
异常,可对方法进行注解:
@throws(classOf[java.io.IOException])
def get(url: String) = io.Source.fromURL(url).mkString
6.2.2 添加超时包装
/**
* Returns the text (content) from a REST URL as a String.
* Inspired by http://matthewkwong.blogspot.com/2009/09/
scala-scalaiosource-fromurl-blockshangs.html
* and http://alvinalexander.com/blog/post/java/how-open-url-
read-contents-httpurl-connection-java
*
* The `connectTimeout` and `readTimeout` comes from the Java URLConnection
* class Javadoc.
* @param url The full URL to connect to.
* @param connectTimeout Sets a specified timeout value, in milliseconds,
* to be used when opening a communications link to the resource referenced
* by this URLConnection. If the timeout expires before the connection can
* be established, a java.net.SocketTimeoutException
* is raised. A timeout of zero is interpreted as an infinite timeout.
* Defaults to 5000 ms.
* @param readTimeout If the timeout expires before there is data available
* for read, a java.net.SocketTimeoutException is raised. A timeout of zero
* is interpreted as an infinite timeout. Defaults to 5000 ms.
* @param requestMethod Defaults to "GET". (Other methods have not been tested.)
*
* @example get("http://www.example.com/getInfo")
* @example get("http://www.example.com/getInfo", 5000)
* @example get("http://www.example.com/getInfo", 5000, 5000)
*/
@throws(classOf[java.io.IOException])
@throws(classOf[java.net.SocketTimeoutException])
def get(url: String,
connectTimeout:Int =5000,
readTimeout:Int =5000,
requestMethod: String = "GET") = {
import java.net.{URL, HttpURLConnection}
val connection = (new URL(url)).openConnection.asInstanceOf[HttpURLConnection]
connection.setConnectTimeout(connectTimeout)
connection.setReadTimeout(readTimeout)
connection.setRequestMethod(requestMethod)
val inputStream = connection.getInputStream
val content = io.Source.fromInputStream(inputStream).mkString
if (inputStream != null) inputStream.close
content
}
该方法可设置连接和读取超时值,调用示例如下:
try {
val content = get("http://localhost:8080/waitForever")
println(content)
} catch {
case ioe: java.io.IOException => // handle this
case ste: java.net.SocketTimeoutException => // handle this
}
6.2.3 使用Apache HttpClient库
import java.io._
import org.apache.http.{HttpEntity, HttpResponse}
import org.apache.http.client._
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.DefaultHttpClient
import scala.collection.mutable.StringBuilder
import scala.xml.XML
import org.apache.http.params.HttpConnectionParams
import org.apache.http.params.HttpParams
/**
* Returns the text (content) from a REST URL as a String.
* Returns a blank String if there was a problem.
* This function will also throw exceptions if there are problems trying
* to connect to the url.
*
* @param url A complete URL, such as "http://foo.com/bar"
* @param connectionTimeout The connection timeout, in ms.
* @param socketTimeout The socket timeout, in ms.
*/
def getRestContent(url: String,
connectionTimeout: Int,
socketTimeout: Int): String = {
val httpClient = buildHttpClient(connectionTimeout, socketTimeout)
val httpResponse = httpClient.execute(new HttpGet(url))
val entity = httpResponse.getEntity
var content = ""
// 后续处理代码...
}
综上所述,通过以上多种方法,可以在Scala中实现从邮件通知到Web服务搭建、参数处理以及请求客户端创建等一系列功能,为Scala Web开发提供了丰富的解决方案。
6. 创建简单的GET请求客户端(续)
6.2.3 使用Apache HttpClient库(续)
import java.io._
import org.apache.http.{HttpEntity, HttpResponse}
import org.apache.http.client._
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.DefaultHttpClient
import scala.collection.mutable.StringBuilder
import scala.xml.XML
import org.apache.http.params.HttpConnectionParams
import org.apache.http.params.HttpParams
/**
* Returns the text (content) from a REST URL as a String.
* Returns a blank String if there was a problem.
* This function will also throw exceptions if there are problems trying
* to connect to the url.
*
* @param url A complete URL, such as "http://foo.com/bar"
* @param connectionTimeout The connection timeout, in ms.
* @param socketTimeout The socket timeout, in ms.
*/
def getRestContent(url: String,
connectionTimeout: Int,
socketTimeout: Int): String = {
val httpClient = buildHttpClient(connectionTimeout, socketTimeout)
val httpResponse = httpClient.execute(new HttpGet(url))
val entity = httpResponse.getEntity
var content = ""
if (entity != null) {
val inputStream = entity.getContent
try {
val reader = new BufferedReader(new InputStreamReader(inputStream))
val stringBuilder = new StringBuilder()
var line: String = null
while ({line = reader.readLine(); line!= null}) {
stringBuilder.append(line)
}
content = stringBuilder.toString
} catch {
case e: IOException =>
e.printStackTrace()
} finally {
try {
inputStream.close()
} catch {
case e: IOException =>
e.printStackTrace()
}
}
}
content
}
private def buildHttpClient(connectionTimeout: Int, socketTimeout: Int): HttpClient = {
val httpClient = new DefaultHttpClient()
val params: HttpParams = httpClient.getParams()
HttpConnectionParams.setConnectionTimeout(params, connectionTimeout)
HttpConnectionParams.setSoTimeout(params, socketTimeout)
httpClient
}
使用该方法时,可按如下方式调用:
val url = "http://example.com/api/data"
val connectionTimeout = 5000
val socketTimeout = 5000
val result = getRestContent(url, connectionTimeout, socketTimeout)
println(result)
6.3 三种方法对比
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
scala.io.Source.fromURL
| 代码简单,使用方便 | 无超时控制,服务无响应时会阻塞 | 对响应时间要求不高,服务稳定性好的场景 |
| 添加超时包装 | 可设置连接和读取超时,增强了健壮性 | 代码相对复杂 | 需要控制超时时间,避免程序长时间阻塞的场景 |
| Apache HttpClient库 | 功能强大,可定制性高 | 依赖第三方库,代码复杂 | 需要处理复杂请求,如设置请求头、处理响应状态码等场景 |
6.4 流程图
graph TD;
A[选择GET请求方法] --> B{是否需要超时控制};
B -- 否 --> C[使用scala.io.Source.fromURL];
B -- 是 --> D{是否需要复杂请求处理};
D -- 否 --> E[添加超时包装];
D -- 是 --> F[使用Apache HttpClient库];
总结
7.1 关键技术点回顾
本文围绕Scala Web开发展开,涵盖了多个重要方面:
-
邮件通知
:通过解析JSON数据,结合Lift - JSON库,实现对感兴趣用户邮件的通知功能。
-
Web服务搭建
:使用Scalatra框架,借助Giter8工具创建项目,配置Jetty服务器,轻松搭建Web服务。
-
Servlet映射
:利用Scalatra的“挂载”机制,替代传统的XML Servlet映射,使代码更简洁。
-
参数处理
:支持传统和命名参数方式处理GET请求参数,使用通配符处理复杂情况;使用
post
方法和
request.body
处理POST请求数据,并进行JSON反序列化。
-
请求客户端
:介绍了三种创建GET请求客户端的方法,各有优缺点,可根据不同场景选择。
7.2 实践建议
- 项目搭建 :在创建Scalatra项目时,使用Giter8工具能快速生成规范的项目结构,提高开发效率。
- 参数处理 :根据实际需求选择合适的参数传递和处理方式,命名参数方式可提高代码的可读性和可维护性。
-
请求客户端
:对于对响应时间要求不高的简单请求,可使用
scala.io.Source.fromURL;对于需要控制超时的场景,使用添加超时包装的方法;对于复杂请求,建议使用Apache HttpClient库。
7.3 未来拓展
- 性能优化 :可进一步研究Scalatra和相关库的性能优化技巧,如缓存机制、异步处理等。
- 功能扩展 :结合数据库、消息队列等技术,拓展Web服务的功能,实现更复杂的业务逻辑。
- 安全防护 :加强Web服务的安全防护,如身份验证、授权、防止SQL注入等。
通过本文的介绍,相信读者能够对Scala Web开发有更深入的了解,并在实际项目中灵活运用这些技术。不断实践和探索,将能开发出高效、稳定、安全的Scala Web应用。
超级会员免费看
702

被折叠的 条评论
为什么被折叠?



