本章讨论与Lift内的其他系统进行交互,如发送电子邮件,呼叫URL或调度任务。
本章中的许多配方都有一个项目中的代码示例,网址为https://github.com/LiftCookbook/cookbook_around。
发送纯文本电子邮件
解
使用Mailer
:
import
net.liftweb.util.Mailer
import
net.liftweb.util.Mailer._
Mailer
.
sendMail
(
From
(
"you@example.org"
),
Subject
(
"Hello"
),
To
(
"other@example.org"
),
PlainMailBodyType
(
"Hello from Lift"
)
)
讨论
Mailer
以异步方式发送消息,意思sendMail
将立即返回,因此您不必担心与SMTP服务器进行协商的时间成本。但是,blockingSendMail
如果您需要等待,还有一种方法。
默认情况下,使用的SMTP服务器将是localhost。您可以通过设置mail.smtp.host
属性来更改此值。例如,编辑src / mail / resources / props / default.props并添加行:
.
smtp
.
host
=
smtp
.
example
.
org
签名sendMail
需要一个From
,Subject
然后任意数量MailTypes
:
- 收件人电子邮件地址
- 邮件客户端应该用于回复的地址
- 键/值对以在消息中包括为头
- 以UTF-8编码发送的纯文本电子邮件
- 一个纯文本电子邮件,您可以在其中指定编码
- 对于HTML电子邮件( “HTML电子邮件” )
- 对于附件( “使用附件发送电子邮件” )
To
,CC
和BCC
ReplyTo
MessageHeader
PlainMailBodyType
PlainPlusBodyType
XHTMLMailBodyType
XHTMLPlusImages
在前面的例子中,我们增加了两个类型:PlainMailBodyType
和To
。添加更多是你所期望的:
Mailer
.
sendMail
(
From
(
"you@example.org"
),
Subject
(
"Hello"
),
To
(
"other@example.org"
),
To
(
"someone@example.org"
),
MessageHeader
(
"X-Ignore-This"
,
"true"
),
PlainMailBodyType
(
"Hello from Lift"
)
)
地址类MailTypes
(To
,CC
,BCC
,ReplyTo
)可以给出一个可选的“人名”:
From
(
"you@example.org"
,
Full
(
"Example Corporation"
))
这将显示在您的邮箱中:
来自:Example Corporation <you@example.org>
默认字符集为UTF-8。如果您需要进行更改,替换使用的PlainMailBodyType
用 PlainPlusBodyType("Hello from Lift", "ISO8859_1")
。
记录电子邮件而不是发送
解
Mailer.devModeSend
在Boot.scala中分配日志功能:
import
net.liftweb.util.Mailer._
import
javax.mail.internet.
{
MimeMessage
,
MimeMultipart
}
Mailer
.
devModeSend
.
default
.
set
(
(
m
:
MimeMessage
)
=>
logger
.
info
(
"Would have sent: "
+
m
.
getContent
)
)
当您发送电子邮件时Mailer
,不会联系到SMTP服务器,相反,您会看到输出到您的日志中:
会发送:Lift你好
讨论
这个配方的关键部分是设置一个 MimeMessage => Unit
功能Mailer.devModeSend
。我们恰好是日志记录,但您可以使用此功能以任何方式处理电子邮件。
该LiftMailer
允许您控制电子邮件的方式在每次运行模式发送:默认情况下,电子邮件已发送devModeSend
,profileModeSend
,pilotModeSend
,stagingModeSend
,和productionModeSend
; 而默认情况下,testModeSend
仅记录消息将被发送的日志。
该testModeSend
日志的一个参考MimeMessage
,这意味着你的日志会显示这样的消息:
发送javax.mail.internet.MimeMessage@4a91a883
此配方已更改了Mailer
Lift应用程序处于开发人员模式(默认情况下)的行为。我们正在记录消息的正文部分。
Java Mail不包含用于显示电子邮件的所有部分的实用程序,因此如果需要更多信息,则需要滚动自己的功能。例如:
def
display
(
m
:
MimeMessage
)
:
String
=
{
val
nl
=
System
.
getProperty
(
"line.separator"
)
val
from
=
"From: "
+
m
.
getFrom
.
map
(
_
.
toString
).
mkString
(
","
)
val
subj
=
"Subject: "
+
m
.
getSubject
def
parts
(
mm
:
MimeMultipart
)
=
(
0
until
mm
.
getCount
).
map
(
mm
.
getBodyPart
)
val
body
=
m
.
getContent
match
{
case
mm
:
MimeMultipart
=>
val
bodyParts
=
for
(
part
<-
parts
(
mm
))
yield
part
.
getContent
.
toString
bodyParts
.
mkString
(
nl
)
case
otherwise
=>
otherwise
.
toString
}
val
to
=
for
{
rt
<-
List
(
RecipientType
.
TO
,
RecipientType
.
CC
,
RecipientType
.
BCC
)
address
<-
Option
(
m
.
getRecipients
(
rt
))
getOrElse
Array
()
}
yield
rt
.
toString
+
": "
+
address
.
toString
List
(
from
,
to
.
mkString
(
nl
),
subj
,
body
)
mkString
nl
}
Mailer
.
devModeSend
.
default
.
set
(
(
m
:
MimeMessage
)
=>
logger
.
info
(
"Would have sent: "
+
display
(
m
))
)
这将产生以下形式的输出:
将发送:From:you@example.org 至:other@example.org 至:someone@example.org 主题:你好 Lift你好
此示例display
函数很长,但最简单。该body
值通过提取每个主体部分来处理多部分消息。发送更多结构化的电子邮件(例如HTML电子邮件)时触发在“HTML电子邮件”中描述。
如果要在实际发送邮件时调试邮件系统,请启用Java Mail调试模式。在default.props中添加:
mail.debug
=
true
当发送电子邮件时,这会产生Java Mail系统的低级别输出:
DEBUG:JavaMail版本1.4.4
DEBUG:成功加载资源:/META-INF/javamail.default.providers
DEBUG SMTP:useEhlo true,useAuth false
DEBUG SMTP:尝试连接到主机“localhost”,端口25,isSSL为false
...
也可以看看
运行模式在Lift wiki上描述。
HTML电子邮件
解
给Mailer
一个NodeSeq
包含你的HTML消息:
import
net.liftweb.util.Mailer
import
net.liftweb.util.Mailer._
val
msg
=
<
html
>
<
head
>
<
title
>
Hello
</
title
>
</
head
>
<
body
>
<
h1
>
Hello
</
h1
>
</
body
>
</
html
>
Mailer
.
sendMail
(
From
(
"me@example.org"
),
Subject
(
"Hello"
),
To
(
"you@example.org"
),
msg
)
讨论
一个隐含的转换NodeSeq
成一个XHTMLMailBodyType
。这确保电子邮件的MIME类型text/html
。尽管名称为“XHTML”,但是使用HTML5语义将消息转换为传输。
可以通过mail.charset
在Lift属性文件中进行设置来更改HTML电子邮件(UTF-8)的字符编码 。
如果要同时设置消息的文本和HTML版本,请提供包含在相应BodyType
类中的每个正文:
val
html
=
<
html
>
<
head
>
<
title
>
Hello
</
title
>
</
head
>
<
body
>
<
h1
>
Hello
!</
h1
>
</
body
>
</
html
>
var
text
=
"Hello!"
Mailer
.
sendMail
(
From
(
"me@example.org"
),
Subject
(
"Hello"
),
To
(
"you@example.org"
),
PlainMailBodyType
(
text
),
XHTMLMailBodyType
(
html
)
)
此消息将作为multipart/alternative
:
Content-Type:multipart / alternative; boundary =“---- = _ Part_1_1197390963.1360226660982” 日期:2013年2月7日,02:44:22 -0600(CST) ------ = _ Part_1_1197390963.1360226660982 Content-Type:text / plain; charset = UTF-8 内容传输编码:7bit 你好! ------ = _ Part_1_1197390963.1360226660982 Content-Type:text / html; charset = UTF-8 内容传输编码:7bit <html> <head> <title> Hello </ title> </ head> <body> <h1>你好!</ h1> </ body> </ html> ------ = _ Part_1_1197390963.1360226660982--
当收到含有此内容的邮件时,由邮件客户端决定要显示哪个版本(文本或HTML)。
也可以看看
要发送附件,请参阅“使用附件发送电子邮件”。
发送认证电子邮件
解
设置Mailer.authenticator
在Boot
与您的SMTP服务器的凭据,并启用mail.smtp.auth
标志在提升属性文件。
修改Boot.scala以包括:
import
net.liftweb.util.
{
Props
,
Mailer
}
import
javax.mail.
{
Authenticator
,
PasswordAuthentication
}
Mailer
.
authenticator
=
for
{
user
<-
Props
.
get
(
"mail.user"
)
pass
<-
Props
.
get
(
"mail.password"
)
}
yield
new
Authenticator
{
override
def
getPasswordAuthentication
=
new
PasswordAuthentication
(
user
,
pass
)
}
在这个例子中,我们期望用户名和密码来自Lift属性,所以我们需要修改 src / main / resources / props / default.props来包含它们:
.
smtp
.
auth
=
true
.
user
=
me
@example
.
org
.
password
=
correct
horse
battery
staple
.
smtp
.
host
=
smtp
.
sendgrid
.
net
当您发送电子邮件,在凭证default.props将用于与SMTP服务器进行身份验证。
讨论
我们已经使用Lift属性来配置SMTP身份验证。这有利于允许我们为一些运行模式启用身份验证。例如,如果我们的default.props不包含身份验证设置,但是我们的production.default.props没有,那么在开发模式下不会发生身份验证,确保我们无法在生产环境之外不小心发送电子邮件。
您不必为此使用属性文件:Lift Mailer
还支持JNDI,或者您可以使用其他方式查找用户名和密码,并Mailer.authenticator
在有值时设置。
但是,一些邮件服务(如SendGrid)需要mail.smtp.auth=true
设置,并且应该进入Lift属性文件或设置为JVM参数:-Dmail.smtp.auth=true
。
也可以看看
除此之外mail.smtp.auth
,还有一系列设置来控制Java Mail API。示例包括控制端口号和超时。
使用附件发送电子邮件
解
使用Mailer
XHTMLPlusImages
包装附件的邮件。
假设我们要构建一个CSV文件并通过电子邮件发送:
val
content
=
"Planet,Discoverer\r\n"
+
"HR 8799 c, Marois et al\r\n"
+
"Kepler-22b, Kepler Science Team\r\n"
case
class
CSVFile
(
bytes
:
Array
[
Byte
],
filename
:
String
=
"file.csv"
,
mime
:
String
=
"text/csv; charset=utf8; header=present"
)
val
attach
=
CSVFile
(
content
.
mkString
.
getBytes
(
"utf8"
))
val
body
=
<
p
>
Please
research
the
enclosed
.</
p
>
val
msg
=
XHTMLPlusImages
(
body
,
PlusImageHolder
(
attach
.
filename
,
attach
.
mime
,
attach
.
bytes
))
Mailer
.
sendMail
(
From
(
"me@example.org"
,
Subject
(
"Planets"
),
To
(
"you@example.org"
),
msg
)
这里发生的是我们的消息是一个XHTMLPlusImages
实例,它接受一个正文消息和附件。附件,the PlusImageHolder
,是一个Array[Byte]
,mime类型和一个文件名。
讨论
XHTMLPlusImages
PlusImageHolder
如果您有多个文件要附加,也可以接受多个。虽然名称PlusImageHolder
可能表明它是附件图像,您可以附加任何类型的数据作为Array[Byte]
适当的MIME类型。
默认情况下,附件以配置方式发送inline
。这将控制Content-Disposition
消息中的标题,并且inline
意味着当显示消息时,内容将自动显示。替代方案是attachment
,并且可以使用可选的最终参数来指示PlusImageHolder
:
PlusImageHolder
(
attach
.
filename
,
attach
.
mime
,
attach
.
bytes
,
attachment
=
true
)
实际上,邮件客户端将显示消息的方式,但这个额外的参数可能会给你一些更多的控制权。
要附加预制文件,可以使用LiftRules.loadResource
从类路径中获取内容。如果我们的项目在src / main / resources /文件夹中包含一个名为Kepler-22b_System_Diagram.jpg的文件,我们可以加载并附加它:
val
filename
=
"Kepler-22b_System_Diagram.jpg"
val
msg
=
for
(
bytes
<-
LiftRules
.
loadResource
(
"/"
+
filename
)
)
yield
XHTMLPlusImages
(
<
p
>
Please
research
this
planet
.</
p
>,
PlusImageHolder
(
filename
,
"image/jpg"
,
bytes
)
)
msg
match
{
case
Full
(
m
)
=>
Mailer
.
sendMail
(
From
(
"me@example.org"
),
Subject
(
"Planet attachment"
),
To
(
"you@example.org"
),
m
)
case
_
=>
logger
.
error
(
"Planet file not found"
)
}
由于src / main / resources的内容包含在类路径中,所以我们将文件名传递给loadResource
一个前导/
字符,以便该文件可以在类路径的正确位置找到。
的loadResource
回报Box[Array[Byte]]
,因为我们不能保证该文件将存在。我们将其映射到Box[XHTMLPlusImages]
该结果并匹配,以发送电子邮件或记录该文件未找到。
稍后执行任务
解
使用net.liftweb.util.Schedule
:
import
net.liftweb.util.Schedule
import
net.liftweb.util.Helpers._
Schedule
(()
=>
println
(
"doing it"
),
30
seconds
)
这将导致“做”在30秒后从控制台上打印出来。
讨论
以前Schedule
使用的签名预期是类型的功能() => Unit
,这是我们以后想要发生的事情,而TimeSpan
来自Lift的TimeHelpers
,这是当我们想要发生的时候。该30 seconds
值TimeSpan
通过Helpers._
导入给我们,但是如果您喜欢,则会有一个称为perform
接受Long
毫秒值的变体:
Schedule
.
perform
(()
=>
println
(
"doing it"
),
30
*
1000L
)
在幕后,Lift正在利用ScheduledExecutorService
来自java.util.concurrent
和,因此返回a ScheduledFuture[Unit]
。cancel
在运行之前,可以使用这个未来的操作。
可能会发现您只能Schedule
使用函数作为参数调用,而不是延迟值。此版本立即运行该功能,但在工作线程上运行。这是一种方便地异步运行其他任务的方法,而不用为此目的创建一个演员的麻烦。
还有一种Schedule.schedule
方法可以在给定的延迟之后向演员发送指定的消息。这需要一个TimeSpan
延迟,但是也有一个Schedule.perform
版本接受Long
一个延迟。
在Helpers._
进口与它带来了一些隐式转换TimeSpan
。例如,Period
可以给出JodaTime ,schedule
并在执行函数之前将其用作延迟。在这种情况下不要试图使用JodaTime DateTime
。这将被转换为一个TimeSpan
,但没有任何延迟的意义。
也可以看看
“定期运行任务”包括与演员进行调度的示例。
ScheduledFuture
通过Java文档记录Future
。如果您正在构建复杂的,低级别的可撤销并发函数,建议您将Java并发实践的副本(Goetz 等人,Addison-Wesley Professional)撰写。
定期运行任务
解
使用net.liftweb.util.Schedule
确保schedule
在您的任务期间再次呼叫重新安排。例如,使用演员:
import
net.liftweb.util.Schedule
import
net.liftweb.actor.LiftActor
import
net.liftweb.util.Helpers._
object
MyScheduledTask
extends
LiftActor
{
case
class
DoIt
()
case
class
Stop
()
private
var
stopped
=
false
def
messageHandler
=
{
case
DoIt
if
!
stopped
=>
Schedule
.
schedule
(
this
,
DoIt
,
10
minutes
)
// ... do useful work here
case
Stop
=>
stopped
=
true
}
}
该示例LiftActor
为要完成的工作创建一个。在接收到DoIt
消息之后,演员在做任何有用的工作需要完成之前重新调整自己。以这种方式,演员将每10分钟一次。
讨论
该Schedule.schedule
呼叫确保this
演员在发送 DoIt
10分钟后消息。
要启动此过程,可能在Boot.scala中,只需将 DoIt
消息发送给演员:
MyScheduledTask
!
MyScheduledTask
.
DoIt
为了确保在Lift关闭时进程正确停止,我们在Boot.scala中注册一个关机挂钩来发送Stop
消息以防止将来重新安排:
LiftRules
.
unloadHooks
.
append
(
()
=>
MyScheduledTask
!
MyScheduledTask
.
Stop
)
没有Stop
消息,演员将继续重新安排,直到JVM退出。这可能是可以接受的,但请注意,在使用SBT开发过程中,没有Stop
消息,您将在发出container:stop
命令后继续安排任务。
计划ScheduledFuture[Unit]
从Java并发库中返回一个,它允许您进行cancel
活动。
也可以看看
“ 提升行动”(Perrett,Manning Publications,Co.)的第1章包括使用的彗星演员时钟示例Schedule
。
获取网址
解
使用Dispatch “,用于异步HTTP交互的库”。
在开始之前,在build.sbt中包含Dispatch依赖关系:
libraryDependencies
+=
"net.databinder.dispatch"
%%
"dispatch-core"
%
"0.9.5"
使用Dispatch文档中的示例,我们可以发出一个HTTP请求,以从http://www.hostip.info/use.html尝试从服务中确定国家/地区:
import
dispatch._
val
svc
=
url
(
"http://api.hostip.info/country.php"
)
val
country
:
Promise
[
String
]
=
Http
(
svc
OK
as
.
String
)
println
(
country
())
注意,结果country
不是一个,String
而是Promise[String]
我们apply
用来等待结果值。
打印的结果将是一个国家代码,例如GB
,或者XX
如果不能从您的IP地址确定该国家。
讨论
这个简短的例子预计会有一个200(OK)状态结果,并将结果变成一个String
,但这是Dispatch能够实现的一小部分。我们将在本节进一步探讨。
如果请求不返回200怎么办?在这种情况下,使用我们的代码,我们会收到例外:“意外的响应状态:404.” 有几种方法可以改变。
我们可以要求Option
:
val
result
:
Option
[
String
]
=
country
.
option
()
正如你所料,这会给一个None
或者Some[String]
。但是,如果您的应用程序中启用了调试级别日志记录,那么您将看到底层Netty库中的请求和响应和错误消息。您可以通过将记录器设置添加到default.logback.xml文件来调整这些消息:
<logger
name=
"com.ning.http.client"
level=
"WARN"
/>
第二种可能性是either
与通常惯例一起使用,这Right
是预期的结果,Left
意味着失败:
country
.
either
()
match
{
case
Left
(
status
)
=>
println
(
status
.
getMessage
)
case
Right
(
cc
)
=>
println
(
cc
)
}
这将打印结果,因为我们强制评估与应用通过either()
。
Promise[T]
农具map
,flatMap
,filter
,fold
,和所有你希望它让您能够在通常的方法。这意味着你可以用理解的承诺for
:
val
codeLength
=
for
(
cc
<-
country
)
yield
cc
.
length
注意codeLength
是a Promise[Int]
。要获得这个价值,你可以评估codeLength()
,你会得到一个结果2
。
除了提取字符串值as.String
之外,还有其他选项,包括:
-
跟...共事
Promise[Array[Byte]]
-
写入一个文件,如同
Http(svc > as.File(new File("/tmp/cc")))
-
允许您提供一个
client.Response => T
功能来使用响应 - 解析XML响应
as.Bytes
as.File
as.Response
as.xml.Elem
例如as.xml.Elem
:
val
svc
=
url
(
"http://api.hostip.info/?ip=12.215.42.19"
)
val
country
=
Http
(
svc
>
as
.
xml
.
Elem
)
println
(
country
.
map
(
_
\\
"description"
)())
此示例解析对请求的XML响应,该请求返回Promise[scala.xml.Elem]
。我们通过a选择XML的描述节点map
,这将是Promise[NodeSeq]
我们强制评估的一个。输出结果如下:
<gml:description
xmlns:xsi=
"http://www.w3.org/2001/XMLSchema-instance"
xmlns:gml=
"http://www.opengis.net/gml"
>
这是Hostip Lookup Service</gml:description>
这个例子假设这个请求要形成良好。除了核心Databinder库之外,还有JSoup和TagSoup的扩展,可帮助解析不一定形成良好的HTML。
例如,要使用JSoup,请包括依赖关系:
libraryDependencies
+=
"net.databinder.dispatch"
%%
"dispatch-jsoup"
%
"0.9.5"
然后,您可以使用JSoup的功能,例如使用CSS选择器选择页面的元素:
import
org.jsoup.nodes.Document
val
svc
=
url
(
"http://www.example.org"
).
setFollowRedirects
(
true
)
val
title
=
Http
(
svc
>
as
.
jsoup
.
Document
).
map
(
_
.
select
(
"h1"
).
text
).
option
println
(
title
()
getOrElse
"unknown title"
)
在这里,我们应用JSoup的select
功能来选择<h1>
页面上的元素,获取元素的文本,我们将其转换为Promise[Option[String]]
。结果,除非example.org已经改变,否则将是“示例域”。
作为使用Dispatch的最后一个例子,我们可以将一个请求传递给Lift的JSON库:
import
net.liftweb.json._
import
com.ning.http.client
object
asJson
extends
(
client
.
Response
=>
JValue
)
{
def
apply
(
r
:
client.Response
)
=
JsonParser
.
parse
(
r
.
getResponseBody
)
}
val
svc
=
url
(
"http://api.hostip.info/get_json.php?ip=212.58.241.131"
)
val
json
:
Promise
[
JValue
]
=
Http
(
svc
>
asJson
)
case
class
HostInfo
(
country_name
:
String
,
country_code
:
String
)
implicit
val
formats
=
DefaultFormats
val
hostInfo
=
json
.
map
(
_
.
extract
[
HostInfo
])()
我们正在调用的URL为我们已经通过的IP地址的位置信息返回JSON表示。
通过提供一个Response => JValue
Dispatch,我们可以将响应体传递给JSON解析器。然后我们可以映射Promise[JValue]
以应用我们想要的任何Lift JSON函数。在这种情况下,我们正在提取一个简单的case类。
结果将显示hostInfo
为:
HostInfo
(
UNITED
KINGDOM
,
GB
)
也可以看看
Dispatch文档写得很好,并指导您完成Dispatch接近HTTP的方式。花一些时间。
有关Dispatch的问题,最好的地方是Dispatch Google Group。
以前的主要版本Dispatch,0.8.x(“Dispatch Classic”)与0.9版本的“重新启动”项目完全不同。因此,您可能会看到使用0.8.x的示例将需要一些转换才能运行0.9.x。Nathan Hamblen的博客描述了这个变化。
要与JSoup合作,请查看JSoup Cookbook。